hls.js 1.5.9 → 1.5.10-0.canary.10321
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/README.md +4 -3
- package/dist/hls-demo.js +41 -38
- package/dist/hls-demo.js.map +1 -1
- package/dist/hls.js +3479 -2197
- package/dist/hls.js.d.ts +108 -85
- package/dist/hls.js.map +1 -1
- package/dist/hls.light.js +2407 -1761
- package/dist/hls.light.js.map +1 -1
- package/dist/hls.light.min.js +1 -1
- package/dist/hls.light.min.js.map +1 -1
- package/dist/hls.light.mjs +1994 -1321
- package/dist/hls.light.mjs.map +1 -1
- package/dist/hls.min.js +1 -1
- package/dist/hls.min.js.map +1 -1
- package/dist/hls.mjs +2868 -1563
- package/dist/hls.mjs.map +1 -1
- package/dist/hls.worker.js +1 -1
- package/dist/hls.worker.js.map +1 -1
- package/package.json +35 -35
- package/src/config.ts +3 -2
- package/src/controller/abr-controller.ts +24 -20
- package/src/controller/audio-stream-controller.ts +68 -74
- package/src/controller/audio-track-controller.ts +1 -1
- package/src/controller/base-playlist-controller.ts +27 -10
- package/src/controller/base-stream-controller.ts +160 -38
- package/src/controller/buffer-controller.ts +230 -92
- package/src/controller/buffer-operation-queue.ts +16 -19
- package/src/controller/cap-level-controller.ts +3 -2
- package/src/controller/cmcd-controller.ts +51 -14
- package/src/controller/content-steering-controller.ts +29 -15
- package/src/controller/eme-controller.ts +10 -23
- package/src/controller/error-controller.ts +6 -8
- package/src/controller/fps-controller.ts +8 -3
- package/src/controller/fragment-tracker.ts +15 -11
- package/src/controller/gap-controller.ts +43 -16
- package/src/controller/id3-track-controller.ts +7 -7
- package/src/controller/latency-controller.ts +9 -11
- package/src/controller/level-controller.ts +37 -19
- package/src/controller/stream-controller.ts +37 -32
- package/src/controller/subtitle-stream-controller.ts +28 -40
- package/src/controller/subtitle-track-controller.ts +5 -3
- package/src/controller/timeline-controller.ts +19 -21
- package/src/crypt/aes-crypto.ts +21 -2
- package/src/crypt/decrypter-aes-mode.ts +4 -0
- package/src/crypt/decrypter.ts +32 -16
- package/src/crypt/fast-aes-key.ts +28 -5
- package/src/demux/audio/aacdemuxer.ts +2 -2
- package/src/demux/audio/ac3-demuxer.ts +4 -3
- package/src/demux/audio/adts.ts +9 -4
- package/src/demux/audio/base-audio-demuxer.ts +16 -14
- package/src/demux/audio/mp3demuxer.ts +4 -3
- package/src/demux/audio/mpegaudio.ts +1 -1
- package/src/demux/mp4demuxer.ts +7 -7
- package/src/demux/sample-aes.ts +2 -0
- package/src/demux/transmuxer-interface.ts +4 -12
- package/src/demux/transmuxer-worker.ts +4 -4
- package/src/demux/transmuxer.ts +16 -3
- package/src/demux/tsdemuxer.ts +71 -37
- package/src/demux/video/avc-video-parser.ts +208 -119
- package/src/demux/video/base-video-parser.ts +147 -18
- package/src/demux/video/exp-golomb.ts +0 -208
- package/src/demux/video/hevc-video-parser.ts +749 -0
- package/src/empty-es.js +5 -0
- package/src/events.ts +8 -1
- package/src/exports-named.ts +1 -1
- package/src/hls.ts +61 -38
- package/src/loader/fragment-loader.ts +10 -3
- package/src/loader/key-loader.ts +3 -1
- package/src/loader/level-key.ts +10 -9
- package/src/loader/playlist-loader.ts +4 -5
- package/src/remux/mp4-generator.ts +196 -1
- package/src/remux/mp4-remuxer.ts +24 -8
- package/src/task-loop.ts +5 -2
- package/src/types/component-api.ts +3 -1
- package/src/types/demuxer.ts +4 -0
- package/src/types/events.ts +4 -0
- package/src/types/remuxer.ts +1 -1
- package/src/utils/buffer-helper.ts +12 -31
- package/src/utils/cea-608-parser.ts +1 -3
- package/src/utils/codecs.ts +34 -5
- package/src/utils/encryption-methods-util.ts +21 -0
- package/src/utils/fetch-loader.ts +1 -1
- package/src/utils/imsc1-ttml-parser.ts +1 -1
- package/src/utils/keysystem-util.ts +1 -6
- package/src/utils/logger.ts +58 -23
- package/src/utils/mp4-tools.ts +5 -3
- package/src/utils/utf8-utils.ts +18 -0
- package/src/utils/webvtt-parser.ts +1 -1
- package/src/utils/xhr-loader.ts +5 -5
- package/src/demux/id3.ts +0 -411
@@ -1,15 +1,15 @@
|
|
1
1
|
import { Events } from '../events';
|
2
|
-
import Hls from '../hls';
|
2
|
+
import type Hls from '../hls';
|
3
3
|
import { Cmcd } from '@svta/common-media-library/cmcd/Cmcd';
|
4
4
|
import { CmcdObjectType } from '@svta/common-media-library/cmcd/CmcdObjectType';
|
5
5
|
import { CmcdStreamingFormat } from '@svta/common-media-library/cmcd/CmcdStreamingFormat';
|
6
6
|
import { appendCmcdHeaders } from '@svta/common-media-library/cmcd/appendCmcdHeaders';
|
7
7
|
import { appendCmcdQuery } from '@svta/common-media-library/cmcd/appendCmcdQuery';
|
8
|
+
import type { CmcdEncodeOptions } from '@svta/common-media-library/cmcd/CmcdEncodeOptions';
|
8
9
|
import { uuid } from '@svta/common-media-library/utils/uuid';
|
9
10
|
import { BufferHelper } from '../utils/buffer-helper';
|
10
|
-
import { logger } from '../utils/logger';
|
11
11
|
import type { ComponentAPI } from '../types/component-api';
|
12
|
-
import type { Fragment } from '../loader/fragment';
|
12
|
+
import type { Fragment, Part } from '../loader/fragment';
|
13
13
|
import type { BufferCreatedData, MediaAttachedData } from '../types/events';
|
14
14
|
import type {
|
15
15
|
FragmentLoaderContext,
|
@@ -81,7 +81,7 @@ export default class CMCDController implements ComponentAPI {
|
|
81
81
|
// @ts-ignore
|
82
82
|
this.hls = this.config = this.audioBuffer = this.videoBuffer = null;
|
83
83
|
// @ts-ignore
|
84
|
-
this.onWaiting = this.onPlaying = null;
|
84
|
+
this.onWaiting = this.onPlaying = this.media = null;
|
85
85
|
}
|
86
86
|
|
87
87
|
private onMediaAttached(
|
@@ -165,7 +165,7 @@ export default class CMCDController implements ComponentAPI {
|
|
165
165
|
data.su = this.buffering;
|
166
166
|
}
|
167
167
|
|
168
|
-
// TODO: Implement rtp, nrr,
|
168
|
+
// TODO: Implement rtp, nrr, dl
|
169
169
|
|
170
170
|
const { includeKeys } = this;
|
171
171
|
if (includeKeys) {
|
@@ -175,14 +175,18 @@ export default class CMCDController implements ComponentAPI {
|
|
175
175
|
}, {});
|
176
176
|
}
|
177
177
|
|
178
|
+
const options: CmcdEncodeOptions = {
|
179
|
+
baseUrl: context.url,
|
180
|
+
};
|
181
|
+
|
178
182
|
if (this.useHeaders) {
|
179
183
|
if (!context.headers) {
|
180
184
|
context.headers = {};
|
181
185
|
}
|
182
186
|
|
183
|
-
appendCmcdHeaders(context.headers, data);
|
187
|
+
appendCmcdHeaders(context.headers, data, options);
|
184
188
|
} else {
|
185
|
-
context.url = appendCmcdQuery(context.url, data);
|
189
|
+
context.url = appendCmcdQuery(context.url, data, options);
|
186
190
|
}
|
187
191
|
}
|
188
192
|
|
@@ -196,7 +200,7 @@ export default class CMCDController implements ComponentAPI {
|
|
196
200
|
su: !this.initialized,
|
197
201
|
});
|
198
202
|
} catch (error) {
|
199
|
-
logger.warn('Could not generate manifest CMCD data.', error);
|
203
|
+
this.hls.logger.warn('Could not generate manifest CMCD data.', error);
|
200
204
|
}
|
201
205
|
};
|
202
206
|
|
@@ -205,11 +209,11 @@ export default class CMCDController implements ComponentAPI {
|
|
205
209
|
*/
|
206
210
|
private applyFragmentData = (context: FragmentLoaderContext) => {
|
207
211
|
try {
|
208
|
-
const
|
209
|
-
const level = this.hls.levels[
|
210
|
-
const ot = this.getObjectType(
|
212
|
+
const { frag, part } = context;
|
213
|
+
const level = this.hls.levels[frag.level];
|
214
|
+
const ot = this.getObjectType(frag);
|
211
215
|
const data: Cmcd = {
|
212
|
-
d:
|
216
|
+
d: (part || frag).duration * 1000,
|
213
217
|
ot,
|
214
218
|
};
|
215
219
|
|
@@ -223,12 +227,45 @@ export default class CMCDController implements ComponentAPI {
|
|
223
227
|
data.bl = this.getBufferLength(ot);
|
224
228
|
}
|
225
229
|
|
230
|
+
const next = part ? this.getNextPart(part) : this.getNextFrag(frag);
|
231
|
+
|
232
|
+
if (next?.url && next.url !== frag.url) {
|
233
|
+
data.nor = next.url;
|
234
|
+
}
|
235
|
+
|
226
236
|
this.apply(context, data);
|
227
237
|
} catch (error) {
|
228
|
-
logger.warn('Could not generate segment CMCD data.', error);
|
238
|
+
this.hls.logger.warn('Could not generate segment CMCD data.', error);
|
229
239
|
}
|
230
240
|
};
|
231
241
|
|
242
|
+
private getNextFrag(fragment: Fragment): Fragment | undefined {
|
243
|
+
const levelDetails = this.hls.levels[fragment.level]?.details;
|
244
|
+
if (levelDetails) {
|
245
|
+
const index = (fragment.sn as number) - levelDetails.startSN;
|
246
|
+
return levelDetails.fragments[index + 1];
|
247
|
+
}
|
248
|
+
|
249
|
+
return undefined;
|
250
|
+
}
|
251
|
+
|
252
|
+
private getNextPart(part: Part): Part | undefined {
|
253
|
+
const { index, fragment } = part;
|
254
|
+
const partList = this.hls.levels[fragment.level]?.details?.partList;
|
255
|
+
|
256
|
+
if (partList) {
|
257
|
+
const { sn } = fragment;
|
258
|
+
for (let i = partList.length - 1; i >= 0; i--) {
|
259
|
+
const p = partList[i];
|
260
|
+
if (p.index === index && p.fragment.sn === sn) {
|
261
|
+
return partList[i + 1];
|
262
|
+
}
|
263
|
+
}
|
264
|
+
}
|
265
|
+
|
266
|
+
return undefined;
|
267
|
+
}
|
268
|
+
|
232
269
|
/**
|
233
270
|
* The CMCD object type.
|
234
271
|
*/
|
@@ -287,7 +324,7 @@ export default class CMCDController implements ComponentAPI {
|
|
287
324
|
* Get the buffer length for a media type in milliseconds
|
288
325
|
*/
|
289
326
|
private getBufferLength(type: CmcdObjectType) {
|
290
|
-
const media = this.
|
327
|
+
const media = this.media;
|
291
328
|
const buffer =
|
292
329
|
type === CmcdObjectType.AUDIO ? this.audioBuffer : this.videoBuffer;
|
293
330
|
|
@@ -3,7 +3,7 @@ import { Level } from '../types/level';
|
|
3
3
|
import { reassignFragmentLevelIndexes } from '../utils/level-helper';
|
4
4
|
import { AttrList } from '../utils/attr-list';
|
5
5
|
import { ErrorActionFlags, NetworkErrorAction } from './error-controller';
|
6
|
-
import {
|
6
|
+
import { Logger } from '../utils/logger';
|
7
7
|
import {
|
8
8
|
PlaylistContextType,
|
9
9
|
type Loader,
|
@@ -48,13 +48,15 @@ export type UriReplacement = {
|
|
48
48
|
|
49
49
|
const PATHWAY_PENALTY_DURATION_MS = 300000;
|
50
50
|
|
51
|
-
export default class ContentSteeringController
|
51
|
+
export default class ContentSteeringController
|
52
|
+
extends Logger
|
53
|
+
implements NetworkComponentAPI
|
54
|
+
{
|
52
55
|
private readonly hls: Hls;
|
53
|
-
private log: (msg: any) => void;
|
54
56
|
private loader: Loader<LoaderContext> | null = null;
|
55
57
|
private uri: string | null = null;
|
56
58
|
private pathwayId: string = '.';
|
57
|
-
private
|
59
|
+
private _pathwayPriority: string[] | null = null;
|
58
60
|
private timeToLoad: number = 300;
|
59
61
|
private reloadTimer: number = -1;
|
60
62
|
private updated: number = 0;
|
@@ -66,8 +68,8 @@ export default class ContentSteeringController implements NetworkComponentAPI {
|
|
66
68
|
private penalizedPathways: { [pathwayId: string]: number } = {};
|
67
69
|
|
68
70
|
constructor(hls: Hls) {
|
71
|
+
super('content-steering', hls.logger);
|
69
72
|
this.hls = hls;
|
70
|
-
this.log = logger.log.bind(logger, `[content-steering]:`);
|
71
73
|
this.registerListeners();
|
72
74
|
}
|
73
75
|
|
@@ -90,6 +92,23 @@ export default class ContentSteeringController implements NetworkComponentAPI {
|
|
90
92
|
hls.off(Events.ERROR, this.onError, this);
|
91
93
|
}
|
92
94
|
|
95
|
+
pathways() {
|
96
|
+
return (this.levels || []).reduce((pathways, level) => {
|
97
|
+
if (pathways.indexOf(level.pathwayId) === -1) {
|
98
|
+
pathways.push(level.pathwayId);
|
99
|
+
}
|
100
|
+
return pathways;
|
101
|
+
}, [] as string[]);
|
102
|
+
}
|
103
|
+
|
104
|
+
get pathwayPriority(): string[] | null {
|
105
|
+
return this._pathwayPriority;
|
106
|
+
}
|
107
|
+
|
108
|
+
set pathwayPriority(pathwayPriority: string[]) {
|
109
|
+
this.updatePathwayPriority(pathwayPriority);
|
110
|
+
}
|
111
|
+
|
93
112
|
startLoad() {
|
94
113
|
this.started = true;
|
95
114
|
this.clearTimeout();
|
@@ -176,7 +195,7 @@ export default class ContentSteeringController implements NetworkComponentAPI {
|
|
176
195
|
errorAction.flags === ErrorActionFlags.MoveAllAlternatesMatchingHost
|
177
196
|
) {
|
178
197
|
const levels = this.levels;
|
179
|
-
let pathwayPriority = this.
|
198
|
+
let pathwayPriority = this._pathwayPriority;
|
180
199
|
let errorPathway = this.pathwayId;
|
181
200
|
if (data.context) {
|
182
201
|
const { groupId, pathwayId, type } = data.context;
|
@@ -191,19 +210,14 @@ export default class ContentSteeringController implements NetworkComponentAPI {
|
|
191
210
|
}
|
192
211
|
if (!pathwayPriority && levels) {
|
193
212
|
// If PATHWAY-PRIORITY was not provided, list pathways for error handling
|
194
|
-
pathwayPriority =
|
195
|
-
if (pathways.indexOf(level.pathwayId) === -1) {
|
196
|
-
pathways.push(level.pathwayId);
|
197
|
-
}
|
198
|
-
return pathways;
|
199
|
-
}, [] as string[]);
|
213
|
+
pathwayPriority = this.pathways();
|
200
214
|
}
|
201
215
|
if (pathwayPriority && pathwayPriority.length > 1) {
|
202
216
|
this.updatePathwayPriority(pathwayPriority);
|
203
217
|
errorAction.resolved = this.pathwayId !== errorPathway;
|
204
218
|
}
|
205
219
|
if (!errorAction.resolved) {
|
206
|
-
|
220
|
+
this.warn(
|
207
221
|
`Could not resolve ${data.details} ("${
|
208
222
|
data.error.message
|
209
223
|
}") with content-steering for Pathway: ${errorPathway} levels: ${
|
@@ -245,7 +259,7 @@ export default class ContentSteeringController implements NetworkComponentAPI {
|
|
245
259
|
}
|
246
260
|
|
247
261
|
private updatePathwayPriority(pathwayPriority: string[]) {
|
248
|
-
this.
|
262
|
+
this._pathwayPriority = pathwayPriority;
|
249
263
|
let levels: Level[] | undefined;
|
250
264
|
|
251
265
|
// Evaluate if we should remove the pathway from the penalized list
|
@@ -442,7 +456,7 @@ export default class ContentSteeringController implements NetworkComponentAPI {
|
|
442
456
|
) => {
|
443
457
|
this.log(`Loaded steering manifest: "${url}"`);
|
444
458
|
const steeringData = response.data as SteeringManifest;
|
445
|
-
if (steeringData
|
459
|
+
if (steeringData?.VERSION !== 1) {
|
446
460
|
this.log(`Steering VERSION ${steeringData.VERSION} not supported!`);
|
447
461
|
return;
|
448
462
|
}
|
@@ -5,7 +5,7 @@
|
|
5
5
|
*/
|
6
6
|
import { Events } from '../events';
|
7
7
|
import { ErrorTypes, ErrorDetails } from '../errors';
|
8
|
-
import {
|
8
|
+
import { Logger } from '../utils/logger';
|
9
9
|
import {
|
10
10
|
getKeySystemsForConfig,
|
11
11
|
getSupportedMediaKeySystemConfigurations,
|
@@ -19,7 +19,7 @@ import {
|
|
19
19
|
KeySystems,
|
20
20
|
requestMediaKeySystemAccess,
|
21
21
|
} from '../utils/mediakeys-helper';
|
22
|
-
import { strToUtf8array } from '../utils/
|
22
|
+
import { strToUtf8array } from '../utils/utf8-utils';
|
23
23
|
import { base64Decode } from '../utils/numeric-encoding-utils';
|
24
24
|
import { DecryptData, LevelKey } from '../loader/level-key';
|
25
25
|
import Hex from '../utils/hex';
|
@@ -41,9 +41,6 @@ import type {
|
|
41
41
|
LoaderConfiguration,
|
42
42
|
LoaderContext,
|
43
43
|
} from '../types/loader';
|
44
|
-
|
45
|
-
const LOGGER_PREFIX = '[eme]';
|
46
|
-
|
47
44
|
interface KeySystemAccessPromises {
|
48
45
|
keySystemAccess: Promise<MediaKeySystemAccess>;
|
49
46
|
mediaKeys?: Promise<MediaKeys>;
|
@@ -68,7 +65,7 @@ export interface MediaKeySessionContext {
|
|
68
65
|
* @class
|
69
66
|
* @constructor
|
70
67
|
*/
|
71
|
-
class EMEController implements ComponentAPI {
|
68
|
+
class EMEController extends Logger implements ComponentAPI {
|
72
69
|
public static CDMCleanupPromise: Promise<void> | void;
|
73
70
|
|
74
71
|
private readonly hls: Hls;
|
@@ -90,15 +87,9 @@ class EMEController implements ComponentAPI {
|
|
90
87
|
private setMediaKeysQueue: Promise<void>[] = EMEController.CDMCleanupPromise
|
91
88
|
? [EMEController.CDMCleanupPromise]
|
92
89
|
: [];
|
93
|
-
private onMediaEncrypted = this._onMediaEncrypted.bind(this);
|
94
|
-
private onWaitingForKey = this._onWaitingForKey.bind(this);
|
95
|
-
|
96
|
-
private debug: (msg: any) => void = logger.debug.bind(logger, LOGGER_PREFIX);
|
97
|
-
private log: (msg: any) => void = logger.log.bind(logger, LOGGER_PREFIX);
|
98
|
-
private warn: (msg: any) => void = logger.warn.bind(logger, LOGGER_PREFIX);
|
99
|
-
private error: (msg: any) => void = logger.error.bind(logger, LOGGER_PREFIX);
|
100
90
|
|
101
91
|
constructor(hls: Hls) {
|
92
|
+
super('eme', hls.logger);
|
102
93
|
this.hls = hls;
|
103
94
|
this.config = hls.config;
|
104
95
|
this.registerListeners();
|
@@ -113,13 +104,9 @@ class EMEController implements ComponentAPI {
|
|
113
104
|
config.licenseXhrSetup = config.licenseResponseCallback = undefined;
|
114
105
|
config.drmSystems = config.drmSystemOptions = {};
|
115
106
|
// @ts-ignore
|
116
|
-
this.hls =
|
117
|
-
this.onMediaEncrypted =
|
118
|
-
this.onWaitingForKey =
|
119
|
-
this.keyIdToKeySessionPromise =
|
120
|
-
null as any;
|
107
|
+
this.hls = this.config = this.keyIdToKeySessionPromise = null;
|
121
108
|
// @ts-ignore
|
122
|
-
this.
|
109
|
+
this.onMediaEncrypted = this.onWaitingForKey = null;
|
123
110
|
}
|
124
111
|
|
125
112
|
private registerListeners() {
|
@@ -523,7 +510,7 @@ class EMEController implements ComponentAPI {
|
|
523
510
|
return this.attemptKeySystemAccess(keySystemsToAttempt);
|
524
511
|
}
|
525
512
|
|
526
|
-
private
|
513
|
+
private onMediaEncrypted = (event: MediaEncryptedEvent) => {
|
527
514
|
const { initDataType, initData } = event;
|
528
515
|
this.debug(`"${event.type}" event: init data type: "${initDataType}"`);
|
529
516
|
|
@@ -639,11 +626,11 @@ class EMEController implements ComponentAPI {
|
|
639
626
|
);
|
640
627
|
}
|
641
628
|
keySessionContextPromise.catch((error) => this.handleError(error));
|
642
|
-
}
|
629
|
+
};
|
643
630
|
|
644
|
-
private
|
631
|
+
private onWaitingForKey = (event: Event) => {
|
645
632
|
this.log(`"${event.type}" event`);
|
646
|
-
}
|
633
|
+
};
|
647
634
|
|
648
635
|
private attemptSetMediaKeys(
|
649
636
|
keySystem: KeySystems,
|
@@ -8,7 +8,7 @@ import {
|
|
8
8
|
} from '../utils/error-helper';
|
9
9
|
import { findFragmentByPTS } from './fragment-finders';
|
10
10
|
import { HdcpLevel, HdcpLevels } from '../types/level';
|
11
|
-
import {
|
11
|
+
import { Logger } from '../utils/logger';
|
12
12
|
import type Hls from '../hls';
|
13
13
|
import type { RetryConfig } from '../config';
|
14
14
|
import type { NetworkComponentAPI } from '../types/component-api';
|
@@ -50,19 +50,17 @@ type PenalizedRendition = {
|
|
50
50
|
|
51
51
|
type PenalizedRenditions = { [key: number]: PenalizedRendition };
|
52
52
|
|
53
|
-
export default class ErrorController
|
53
|
+
export default class ErrorController
|
54
|
+
extends Logger
|
55
|
+
implements NetworkComponentAPI
|
56
|
+
{
|
54
57
|
private readonly hls: Hls;
|
55
58
|
private playlistError: number = 0;
|
56
59
|
private penalizedRenditions: PenalizedRenditions = {};
|
57
|
-
private log: (msg: any) => void;
|
58
|
-
private warn: (msg: any) => void;
|
59
|
-
private error: (msg: any) => void;
|
60
60
|
|
61
61
|
constructor(hls: Hls) {
|
62
|
+
super('error-controller', hls.logger);
|
62
63
|
this.hls = hls;
|
63
|
-
this.log = logger.log.bind(logger, `[info]:`);
|
64
|
-
this.warn = logger.warn.bind(logger, `[warning]:`);
|
65
|
-
this.error = logger.error.bind(logger, `[error]:`);
|
66
64
|
this.registerListeners();
|
67
65
|
}
|
68
66
|
|
@@ -1,5 +1,4 @@
|
|
1
1
|
import { Events } from '../events';
|
2
|
-
import { logger } from '../utils/logger';
|
3
2
|
import type { ComponentAPI } from '../types/component-api';
|
4
3
|
import type Hls from '../hls';
|
5
4
|
import type { MediaAttachingData } from '../types/events';
|
@@ -28,10 +27,12 @@ class FPSController implements ComponentAPI {
|
|
28
27
|
|
29
28
|
protected registerListeners() {
|
30
29
|
this.hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
|
30
|
+
this.hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
|
31
31
|
}
|
32
32
|
|
33
33
|
protected unregisterListeners() {
|
34
34
|
this.hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
|
35
|
+
this.hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
|
35
36
|
}
|
36
37
|
|
37
38
|
destroy() {
|
@@ -65,6 +66,10 @@ class FPSController implements ComponentAPI {
|
|
65
66
|
}
|
66
67
|
}
|
67
68
|
|
69
|
+
private onMediaDetaching() {
|
70
|
+
this.media = null;
|
71
|
+
}
|
72
|
+
|
68
73
|
checkFPS(
|
69
74
|
video: HTMLVideoElement,
|
70
75
|
decodedFrames: number,
|
@@ -84,13 +89,13 @@ class FPSController implements ComponentAPI {
|
|
84
89
|
totalDroppedFrames: droppedFrames,
|
85
90
|
});
|
86
91
|
if (droppedFPS > 0) {
|
87
|
-
// logger.log('checkFPS : droppedFPS/decodedFPS:' + droppedFPS/(1000 * currentDecoded / currentPeriod));
|
92
|
+
// hls.logger.log('checkFPS : droppedFPS/decodedFPS:' + droppedFPS/(1000 * currentDecoded / currentPeriod));
|
88
93
|
if (
|
89
94
|
currentDropped >
|
90
95
|
hls.config.fpsDroppedMonitoringThreshold * currentDecoded
|
91
96
|
) {
|
92
97
|
let currentLevel = hls.currentLevel;
|
93
|
-
logger.warn(
|
98
|
+
hls.logger.warn(
|
94
99
|
'drop FPS ratio greater than max allowed value for currentLevel: ' +
|
95
100
|
currentLevel,
|
96
101
|
);
|
@@ -107,12 +107,23 @@ export class FragmentTracker implements ComponentAPI {
|
|
107
107
|
public getBufferedFrag(
|
108
108
|
position: number,
|
109
109
|
levelType: PlaylistLevelType,
|
110
|
+
): Fragment | null {
|
111
|
+
return this.getFragAtPos(position, levelType, true);
|
112
|
+
}
|
113
|
+
|
114
|
+
public getFragAtPos(
|
115
|
+
position: number,
|
116
|
+
levelType: PlaylistLevelType,
|
117
|
+
buffered?: boolean,
|
110
118
|
): Fragment | null {
|
111
119
|
const { fragments } = this;
|
112
120
|
const keys = Object.keys(fragments);
|
113
121
|
for (let i = keys.length; i--; ) {
|
114
122
|
const fragmentEntity = fragments[keys[i]];
|
115
|
-
if (
|
123
|
+
if (
|
124
|
+
fragmentEntity?.body.type === levelType &&
|
125
|
+
(!buffered || fragmentEntity.buffered)
|
126
|
+
) {
|
116
127
|
const frag = fragmentEntity.body;
|
117
128
|
if (frag.start <= position && position <= frag.end) {
|
118
129
|
return frag;
|
@@ -401,7 +412,7 @@ export class FragmentTracker implements ComponentAPI {
|
|
401
412
|
event: Events.BUFFER_APPENDED,
|
402
413
|
data: BufferAppendedData,
|
403
414
|
) {
|
404
|
-
const { frag, part, timeRanges } = data;
|
415
|
+
const { frag, part, timeRanges, type } = data;
|
405
416
|
if (frag.sn === 'initSegment') {
|
406
417
|
return;
|
407
418
|
}
|
@@ -415,15 +426,8 @@ export class FragmentTracker implements ComponentAPI {
|
|
415
426
|
}
|
416
427
|
// Store the latest timeRanges loaded in the buffer
|
417
428
|
this.timeRanges = timeRanges;
|
418
|
-
|
419
|
-
|
420
|
-
this.detectEvictedFragments(
|
421
|
-
elementaryStream,
|
422
|
-
timeRange,
|
423
|
-
playlistType,
|
424
|
-
part,
|
425
|
-
);
|
426
|
-
});
|
429
|
+
const timeRange = timeRanges[type] as TimeRanges;
|
430
|
+
this.detectEvictedFragments(type, timeRange, playlistType, part);
|
427
431
|
}
|
428
432
|
|
429
433
|
private onFragBuffered(event: Events.FRAG_BUFFERED, data: FragBufferedData) {
|
@@ -1,20 +1,22 @@
|
|
1
|
-
import
|
1
|
+
import { State } from './base-stream-controller';
|
2
2
|
import { BufferHelper } from '../utils/buffer-helper';
|
3
3
|
import { ErrorTypes, ErrorDetails } from '../errors';
|
4
4
|
import { PlaylistLevelType } from '../types/loader';
|
5
5
|
import { Events } from '../events';
|
6
|
-
import {
|
6
|
+
import { Logger } from '../utils/logger';
|
7
7
|
import type Hls from '../hls';
|
8
|
+
import type { BufferInfo } from '../utils/buffer-helper';
|
8
9
|
import type { HlsConfig } from '../config';
|
9
10
|
import type { Fragment } from '../loader/fragment';
|
10
11
|
import type { FragmentTracker } from './fragment-tracker';
|
12
|
+
import type { LevelDetails } from '../loader/level-details';
|
11
13
|
|
12
14
|
export const STALL_MINIMUM_DURATION_MS = 250;
|
13
15
|
export const MAX_START_GAP_JUMP = 2.0;
|
14
16
|
export const SKIP_BUFFER_HOLE_STEP_SECONDS = 0.1;
|
15
17
|
export const SKIP_BUFFER_RANGE_START = 0.05;
|
16
18
|
|
17
|
-
export default class GapController {
|
19
|
+
export default class GapController extends Logger {
|
18
20
|
private config: HlsConfig;
|
19
21
|
private media: HTMLMediaElement | null = null;
|
20
22
|
private fragmentTracker: FragmentTracker;
|
@@ -24,8 +26,15 @@ export default class GapController {
|
|
24
26
|
private stalled: number | null = null;
|
25
27
|
private moved: boolean = false;
|
26
28
|
private seeking: boolean = false;
|
29
|
+
private ended: number = 0;
|
27
30
|
|
28
|
-
constructor(
|
31
|
+
constructor(
|
32
|
+
config: HlsConfig,
|
33
|
+
media: HTMLMediaElement,
|
34
|
+
fragmentTracker: FragmentTracker,
|
35
|
+
hls: Hls,
|
36
|
+
) {
|
37
|
+
super('gap-controller', hls.logger);
|
29
38
|
this.config = config;
|
30
39
|
this.media = media;
|
31
40
|
this.fragmentTracker = fragmentTracker;
|
@@ -44,7 +53,12 @@ export default class GapController {
|
|
44
53
|
*
|
45
54
|
* @param lastCurrentTime - Previously read playhead position
|
46
55
|
*/
|
47
|
-
public poll(
|
56
|
+
public poll(
|
57
|
+
lastCurrentTime: number,
|
58
|
+
activeFrag: Fragment | null,
|
59
|
+
levelDetails: LevelDetails | undefined,
|
60
|
+
state: string,
|
61
|
+
) {
|
48
62
|
const { config, media, stalled } = this;
|
49
63
|
if (media === null) {
|
50
64
|
return;
|
@@ -57,6 +71,7 @@ export default class GapController {
|
|
57
71
|
|
58
72
|
// The playhead is moving, no-op
|
59
73
|
if (currentTime !== lastCurrentTime) {
|
74
|
+
this.ended = 0;
|
60
75
|
this.moved = true;
|
61
76
|
if (!seeking) {
|
62
77
|
this.nudgeRetry = 0;
|
@@ -65,7 +80,7 @@ export default class GapController {
|
|
65
80
|
// The playhead is now moving, but was previously stalled
|
66
81
|
if (this.stallReported) {
|
67
82
|
const stalledDuration = self.performance.now() - stalled;
|
68
|
-
|
83
|
+
this.warn(
|
69
84
|
`playback not stuck anymore @${currentTime}, after ${Math.round(
|
70
85
|
stalledDuration,
|
71
86
|
)}ms`,
|
@@ -128,12 +143,9 @@ export default class GapController {
|
|
128
143
|
// When joining a live stream with audio tracks, account for live playlist window sliding by allowing
|
129
144
|
// a larger jump over start gaps caused by the audio-stream-controller buffering a start fragment
|
130
145
|
// that begins over 1 target duration after the video start position.
|
131
|
-
const
|
132
|
-
? this.hls.levels[this.hls.currentLevel]
|
133
|
-
: null;
|
134
|
-
const isLive = level?.details?.live;
|
146
|
+
const isLive = !!levelDetails?.live;
|
135
147
|
const maxStartGapJump = isLive
|
136
|
-
?
|
148
|
+
? levelDetails!.targetduration * 2
|
137
149
|
: MAX_START_GAP_JUMP;
|
138
150
|
const partialOrGap = this.fragmentTracker.getPartialFragment(currentTime);
|
139
151
|
if (startJump > 0 && (startJump <= maxStartGapJump || partialOrGap)) {
|
@@ -153,6 +165,21 @@ export default class GapController {
|
|
153
165
|
|
154
166
|
const stalledDuration = tnow - stalled;
|
155
167
|
if (!seeking && stalledDuration >= STALL_MINIMUM_DURATION_MS) {
|
168
|
+
// Dispatch MEDIA_ENDED when media.ended/ended event is not signalled at end of stream
|
169
|
+
if (
|
170
|
+
state === State.ENDED &&
|
171
|
+
!levelDetails?.live &&
|
172
|
+
Math.abs(currentTime - (levelDetails?.edge || 0)) < 1
|
173
|
+
) {
|
174
|
+
if (stalledDuration < 1000 || this.ended) {
|
175
|
+
return;
|
176
|
+
}
|
177
|
+
this.ended = currentTime;
|
178
|
+
this.hls.trigger(Events.MEDIA_ENDED, {
|
179
|
+
stalled: true,
|
180
|
+
});
|
181
|
+
return;
|
182
|
+
}
|
156
183
|
// Report stalling after trying to fix
|
157
184
|
this._reportStall(bufferInfo);
|
158
185
|
if (!this.media) {
|
@@ -206,7 +233,7 @@ export default class GapController {
|
|
206
233
|
bufferInfo.nextStart - currentTime < config.maxBufferHole)) &&
|
207
234
|
stalledDurationMs > config.highBufferWatchdogPeriod * 1000
|
208
235
|
) {
|
209
|
-
|
236
|
+
this.warn('Trying to nudge playhead over buffer-hole');
|
210
237
|
// Try to nudge currentTime over a buffer hole if we've been stalling for the configured amount of seconds
|
211
238
|
// We only try to jump the hole if it's under the configured size
|
212
239
|
// Reset stalled so to rearm watchdog timer
|
@@ -230,7 +257,7 @@ export default class GapController {
|
|
230
257
|
media.currentTime
|
231
258
|
} due to low buffer (${JSON.stringify(bufferInfo)})`,
|
232
259
|
);
|
233
|
-
|
260
|
+
this.warn(error.message);
|
234
261
|
hls.trigger(Events.ERROR, {
|
235
262
|
type: ErrorTypes.MEDIA_ERROR,
|
236
263
|
details: ErrorDetails.BUFFER_STALLED_ERROR,
|
@@ -305,7 +332,7 @@ export default class GapController {
|
|
305
332
|
startTime + SKIP_BUFFER_RANGE_START,
|
306
333
|
currentTime + SKIP_BUFFER_HOLE_STEP_SECONDS,
|
307
334
|
);
|
308
|
-
|
335
|
+
this.warn(
|
309
336
|
`skipping hole, adjusting currentTime from ${currentTime} to ${targetTime}`,
|
310
337
|
);
|
311
338
|
this.moved = true;
|
@@ -348,7 +375,7 @@ export default class GapController {
|
|
348
375
|
const error = new Error(
|
349
376
|
`Nudging 'currentTime' from ${currentTime} to ${targetTime}`,
|
350
377
|
);
|
351
|
-
|
378
|
+
this.warn(error.message);
|
352
379
|
media.currentTime = targetTime;
|
353
380
|
hls.trigger(Events.ERROR, {
|
354
381
|
type: ErrorTypes.MEDIA_ERROR,
|
@@ -360,7 +387,7 @@ export default class GapController {
|
|
360
387
|
const error = new Error(
|
361
388
|
`Playhead still not moving while enough data buffered @${currentTime} after ${config.nudgeMaxRetry} nudges`,
|
362
389
|
);
|
363
|
-
|
390
|
+
this.error(error.message);
|
364
391
|
hls.trigger(Events.ERROR, {
|
365
392
|
type: ErrorTypes.MEDIA_ERROR,
|
366
393
|
details: ErrorDetails.BUFFER_STALLED_ERROR,
|
@@ -4,7 +4,6 @@ import {
|
|
4
4
|
clearCurrentCues,
|
5
5
|
removeCuesInRange,
|
6
6
|
} from '../utils/texttrack-utils';
|
7
|
-
import * as ID3 from '../demux/id3';
|
8
7
|
import {
|
9
8
|
DateRange,
|
10
9
|
isDateRangeCueAttribute,
|
@@ -19,6 +18,8 @@ import type {
|
|
19
18
|
} from '../types/events';
|
20
19
|
import type { ComponentAPI } from '../types/component-api';
|
21
20
|
import type Hls from '../hls';
|
21
|
+
import { getId3Frames } from '@svta/common-media-library/id3/getId3Frames';
|
22
|
+
import { isId3TimestampFrame } from '@svta/common-media-library/id3/isId3TimestampFrame';
|
22
23
|
|
23
24
|
declare global {
|
24
25
|
interface Window {
|
@@ -137,11 +138,10 @@ class ID3TrackController implements ComponentAPI {
|
|
137
138
|
}
|
138
139
|
|
139
140
|
protected onMediaDetaching(): void {
|
140
|
-
if (
|
141
|
-
|
141
|
+
if (this.id3Track) {
|
142
|
+
clearCurrentCues(this.id3Track);
|
143
|
+
this.id3Track = null;
|
142
144
|
}
|
143
|
-
clearCurrentCues(this.id3Track);
|
144
|
-
this.id3Track = null;
|
145
145
|
this.media = null;
|
146
146
|
this.dateRangeCuesAppended = {};
|
147
147
|
}
|
@@ -211,7 +211,7 @@ class ID3TrackController implements ComponentAPI {
|
|
211
211
|
continue;
|
212
212
|
}
|
213
213
|
|
214
|
-
const frames =
|
214
|
+
const frames = getId3Frames(samples[i].data);
|
215
215
|
if (frames) {
|
216
216
|
const startTime = samples[i].pts;
|
217
217
|
let endTime: number = startTime + samples[i].duration;
|
@@ -228,7 +228,7 @@ class ID3TrackController implements ComponentAPI {
|
|
228
228
|
for (let j = 0; j < frames.length; j++) {
|
229
229
|
const frame = frames[j];
|
230
230
|
// Safari doesn't put the timestamp frame in the TextTrack
|
231
|
-
if (!
|
231
|
+
if (!isId3TimestampFrame(frame)) {
|
232
232
|
// add a bounds to any unbounded cues
|
233
233
|
this.updateId3CueEnds(startTime, type);
|
234
234
|
const cue = createCueWithDataFields(
|