@cdn365/p2p-media-loader-hlsjs 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 +202 -0
- package/README.md +104 -0
- package/dist/p2p-media-loader-hlsjs.es.js +480 -0
- package/dist/p2p-media-loader-hlsjs.es.js.map +1 -0
- package/dist/p2p-media-loader-hlsjs.es.min.js +202 -0
- package/dist/p2p-media-loader-hlsjs.es.min.js.map +1 -0
- package/lib/engine-static.d.ts +2 -0
- package/lib/engine-static.js +34 -0
- package/lib/engine-static.js.map +1 -0
- package/lib/engine.d.ts +189 -0
- package/lib/engine.js +342 -0
- package/lib/engine.js.map +1 -0
- package/lib/fragment-loader.d.ts +12 -0
- package/lib/fragment-loader.js +140 -0
- package/lib/fragment-loader.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -0
- package/lib/playlist-loader.d.ts +10 -0
- package/lib/playlist-loader.js +43 -0
- package/lib/playlist-loader.js.map +1 -0
- package/lib/segment-mananger.d.ts +8 -0
- package/lib/segment-mananger.js +61 -0
- package/lib/segment-mananger.js.map +1 -0
- package/lib/utils.d.ts +3 -0
- package/lib/utils.js +13 -0
- package/lib/utils.js.map +1 -0
- package/package.json +55 -0
- package/src/engine-static.ts +43 -0
- package/src/engine.ts +400 -0
- package/src/fragment-loader.ts +167 -0
- package/src/index.ts +9 -0
- package/src/playlist-loader.ts +37 -0
- package/src/segment-mananger.ts +80 -0
- package/src/utils.ts +22 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segment-mananger.js","sourceRoot":"","sources":["../src/segment-mananger.ts"],"names":[],"mappings":"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"}
|
package/lib/utils.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { ByteRange } from "p2p-media-loader-core";
|
|
2
|
+
export declare function getSegmentRuntimeId(segmentRequestUrl: string, byteRange?: ByteRange): string;
|
|
3
|
+
export declare function getByteRange(rangeStart: number | undefined, rangeEnd: number | undefined): ByteRange | undefined;
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function getSegmentRuntimeId(segmentRequestUrl, byteRange) {
|
|
2
|
+
if (!byteRange)
|
|
3
|
+
return segmentRequestUrl;
|
|
4
|
+
return `${segmentRequestUrl}|${byteRange.start}-${byteRange.end}`;
|
|
5
|
+
}
|
|
6
|
+
export function getByteRange(rangeStart, rangeEnd) {
|
|
7
|
+
if (rangeStart !== undefined &&
|
|
8
|
+
rangeEnd !== undefined &&
|
|
9
|
+
rangeStart <= rangeEnd) {
|
|
10
|
+
return { start: rangeStart, end: rangeEnd };
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=utils.js.map
|
package/lib/utils.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,mBAAmB,CACjC,iBAAyB,EACzB,SAAqB;IAErB,IAAI,CAAC,SAAS;QAAE,OAAO,iBAAiB,CAAC;IACzC,OAAO,GAAG,iBAAiB,IAAI,SAAS,CAAC,KAAK,IAAI,SAAS,CAAC,GAAG,EAAE,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,UAA8B,EAC9B,QAA4B;IAE5B,IACE,UAAU,KAAK,SAAS;QACxB,QAAQ,KAAK,SAAS;QACtB,UAAU,IAAI,QAAQ,EACtB,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC;IAC9C,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cdn365/p2p-media-loader-hlsjs",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "P2P Media Loader hls.js integration - CDN365 fork",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "CDN365 (original by Novage)",
|
|
7
|
+
"homepage": "https://github.com/cdn365/p2p-media-loader",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/cdn365/p2p-media-loader"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/cdn365/p2p-media-loader/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"p2p",
|
|
17
|
+
"peer-to-peer",
|
|
18
|
+
"hls",
|
|
19
|
+
"webrtc",
|
|
20
|
+
"video",
|
|
21
|
+
"mse",
|
|
22
|
+
"player",
|
|
23
|
+
"torrent",
|
|
24
|
+
"bittorrent",
|
|
25
|
+
"webtorrent",
|
|
26
|
+
"hlsjs",
|
|
27
|
+
"ecdn",
|
|
28
|
+
"cdn",
|
|
29
|
+
"cdn365"
|
|
30
|
+
],
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"lib",
|
|
34
|
+
"src",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"exports": "./lib/index.js",
|
|
39
|
+
"types": "./lib/index.d.ts",
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"type": "module",
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@cdn365/p2p-media-loader-core": "^1.0.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"hls.js": "^1.6.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@rollup/plugin-terser": "^0.4.4",
|
|
53
|
+
"hls.js": "^1.6.14"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HlsJsP2PEngine,
|
|
3
|
+
PartialHlsJsP2PEngineConfig,
|
|
4
|
+
HlsWithP2PInstance,
|
|
5
|
+
HlsWithP2PConfig,
|
|
6
|
+
} from "./engine.js";
|
|
7
|
+
|
|
8
|
+
export function injectMixin<
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
HlsJsConstructor extends new (...args: any[]) => any,
|
|
11
|
+
>(HlsJsClass: HlsJsConstructor) {
|
|
12
|
+
return class HlsJsWithP2PClass extends HlsJsClass {
|
|
13
|
+
#p2pEngine: HlsJsP2PEngine;
|
|
14
|
+
|
|
15
|
+
get p2pEngine() {
|
|
16
|
+
return this.#p2pEngine;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
constructor(...args: any[]) {
|
|
21
|
+
const config = args[0] as
|
|
22
|
+
| ({
|
|
23
|
+
p2p?: PartialHlsJsP2PEngineConfig & {
|
|
24
|
+
onHlsJsCreated?: (hls: InstanceType<HlsJsConstructor>) => void;
|
|
25
|
+
};
|
|
26
|
+
} & Record<string, unknown>)
|
|
27
|
+
| undefined;
|
|
28
|
+
|
|
29
|
+
const { p2p, ...hlsJsConfig } = config ?? {};
|
|
30
|
+
|
|
31
|
+
const p2pEngine = new HlsJsP2PEngine(p2p);
|
|
32
|
+
|
|
33
|
+
super({ ...hlsJsConfig, ...p2pEngine.getConfigForHlsJs() });
|
|
34
|
+
|
|
35
|
+
p2pEngine.bindHls(this);
|
|
36
|
+
|
|
37
|
+
this.#p2pEngine = p2pEngine;
|
|
38
|
+
p2p?.onHlsJsCreated?.(this as InstanceType<HlsJsConstructor>);
|
|
39
|
+
}
|
|
40
|
+
} as new (
|
|
41
|
+
config?: HlsWithP2PConfig<HlsJsConstructor>,
|
|
42
|
+
) => HlsWithP2PInstance<InstanceType<HlsJsConstructor>>;
|
|
43
|
+
}
|
package/src/engine.ts
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import type Hls from "hls.js";
|
|
2
|
+
import type {
|
|
3
|
+
AudioTrackLoadedData,
|
|
4
|
+
LevelUpdatedData,
|
|
5
|
+
ManifestLoadedData,
|
|
6
|
+
LevelSwitchingData,
|
|
7
|
+
PlaylistLevelType,
|
|
8
|
+
HlsConfig,
|
|
9
|
+
Events,
|
|
10
|
+
} from "hls.js";
|
|
11
|
+
import { FragmentLoaderBase } from "./fragment-loader.js";
|
|
12
|
+
import { PlaylistLoaderBase } from "./playlist-loader.js";
|
|
13
|
+
import { SegmentManager } from "./segment-mananger.js";
|
|
14
|
+
import {
|
|
15
|
+
CoreConfig,
|
|
16
|
+
Core,
|
|
17
|
+
CoreEventMap,
|
|
18
|
+
DynamicCoreConfig,
|
|
19
|
+
debug,
|
|
20
|
+
DefinedCoreConfig,
|
|
21
|
+
} from "p2p-media-loader-core";
|
|
22
|
+
import { injectMixin } from "./engine-static.js";
|
|
23
|
+
|
|
24
|
+
/** Represents the complete configuration for HlsJsP2PEngine. */
|
|
25
|
+
export type HlsJsP2PEngineConfig = {
|
|
26
|
+
/** Complete core configuration settings. */
|
|
27
|
+
core: DefinedCoreConfig;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Allows for partial configuration of HlsJsP2PEngine, useful for providing overrides or partial updates. */
|
|
31
|
+
export type PartialHlsJsP2PEngineConfig = Partial<
|
|
32
|
+
Omit<HlsJsP2PEngineConfig, "core">
|
|
33
|
+
> & {
|
|
34
|
+
/** Partial core config */
|
|
35
|
+
core?: Partial<CoreConfig>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Type for specifying dynamic configuration options that can be changed at runtime for the P2P engine's core. */
|
|
39
|
+
export type DynamicHlsJsP2PEngineConfig = {
|
|
40
|
+
/** Dynamic core config */
|
|
41
|
+
core?: DynamicCoreConfig;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extends a generic HLS type to include the P2P engine, integrating P2P capabilities directly into the HLS instance.
|
|
46
|
+
* @template HlsType The base HLS type that is being extended.
|
|
47
|
+
*/
|
|
48
|
+
export type HlsWithP2PInstance<HlsType> = HlsType & {
|
|
49
|
+
/** HlsJsP2PEngine instance */
|
|
50
|
+
readonly p2pEngine: HlsJsP2PEngine;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Configuration type for HLS instances that includes P2P settings, augmenting standard HLS configuration with P2P capabilities.
|
|
55
|
+
* @template HlsType A constructor type that produces an HLS instance.
|
|
56
|
+
*/
|
|
57
|
+
export type HlsWithP2PConfig<HlsType extends abstract new () => unknown> =
|
|
58
|
+
ConstructorParameters<HlsType>[0] & {
|
|
59
|
+
p2p?: PartialHlsJsP2PEngineConfig & {
|
|
60
|
+
onHlsJsCreated?: (hls: HlsWithP2PInstance<HlsType>) => void;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const MAX_LIVE_SYNC_DURATION = 120;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Represents a P2P (peer-to-peer) engine for HLS (HTTP Live Streaming) to enhance media streaming efficiency.
|
|
68
|
+
* This class integrates P2P technologies into HLS.js, enabling the distribution of media segments via a peer network
|
|
69
|
+
* alongside traditional HTTP fetching. It reduces server bandwidth costs and improves scalability by sharing the load
|
|
70
|
+
* across multiple clients.
|
|
71
|
+
*
|
|
72
|
+
* The engine manages core functionalities such as segment fetching, segment management, peer connection management,
|
|
73
|
+
* and event handling related to the P2P and HLS processes.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Creating an instance of HlsJsP2PEngine with custom configuration
|
|
77
|
+
* const hlsP2PEngine = new HlsJsP2PEngine({
|
|
78
|
+
* core: {
|
|
79
|
+
* highDemandTimeWindow: 30, // 30 seconds
|
|
80
|
+
* simultaneousHttpDownloads: 3,
|
|
81
|
+
* webRtcMaxMessageSize: 64 * 1024, // 64 KB
|
|
82
|
+
* p2pNotReceivingBytesTimeoutMs: 10000, // 10 seconds
|
|
83
|
+
* p2pInactiveLoaderDestroyTimeoutMs: 15000, // 15 seconds
|
|
84
|
+
* httpNotReceivingBytesTimeoutMs: 8000, // 8 seconds
|
|
85
|
+
* httpErrorRetries: 2,
|
|
86
|
+
* p2pErrorRetries: 2,
|
|
87
|
+
* announceTrackers: ["wss://personal.tracker.com"],
|
|
88
|
+
* rtcConfig: {
|
|
89
|
+
* iceServers: [{ urls: "stun:personal.stun.com" }]
|
|
90
|
+
* },
|
|
91
|
+
* swarmId: "example-swarm-id"
|
|
92
|
+
* }
|
|
93
|
+
* });
|
|
94
|
+
*
|
|
95
|
+
*/
|
|
96
|
+
export class HlsJsP2PEngine {
|
|
97
|
+
private readonly core: Core;
|
|
98
|
+
private readonly segmentManager: SegmentManager;
|
|
99
|
+
private hlsInstanceGetter?: () => Hls;
|
|
100
|
+
private currentHlsInstance?: Hls;
|
|
101
|
+
private readonly debug = debug("p2pml-hlsjs:engine");
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Enhances a given Hls.js class by injecting additional P2P (peer-to-peer) functionalities.
|
|
105
|
+
*
|
|
106
|
+
* @returns {HlsWithP2PInstance} - The enhanced Hls.js class with P2P functionalities.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* const HlsWithP2P = HlsJsP2PEngine.injectMixin(Hls);
|
|
110
|
+
*
|
|
111
|
+
* const hls = new HlsWithP2P({
|
|
112
|
+
* // Hls.js configuration
|
|
113
|
+
* startLevel: 0, // Example of Hls.js config parameter
|
|
114
|
+
* p2p: {
|
|
115
|
+
* core: {
|
|
116
|
+
* // P2P core configuration
|
|
117
|
+
* },
|
|
118
|
+
* onHlsJsCreated(hls) {
|
|
119
|
+
* // Do something with the Hls.js instance
|
|
120
|
+
* },
|
|
121
|
+
* },
|
|
122
|
+
* });
|
|
123
|
+
*/
|
|
124
|
+
static injectMixin(hls: typeof Hls) {
|
|
125
|
+
return injectMixin(hls);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Constructs an instance of HlsJsP2PEngine.
|
|
130
|
+
* @param config Optional configuration for P2P engine setup.
|
|
131
|
+
*/
|
|
132
|
+
constructor(config?: PartialHlsJsP2PEngineConfig) {
|
|
133
|
+
this.core = new Core(config?.core);
|
|
134
|
+
this.segmentManager = new SegmentManager(this.core);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Adds an event listener for the specified event.
|
|
139
|
+
* @param eventName The name of the event to listen for.
|
|
140
|
+
* @param listener The callback function to be invoked when the event is triggered.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* // Listening for a segment being successfully loaded
|
|
144
|
+
* p2pEngine.addEventListener('onSegmentLoaded', (details) => {
|
|
145
|
+
* console.log('Segment Loaded:', details);
|
|
146
|
+
* });
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* // Handling segment load errors
|
|
150
|
+
* p2pEngine.addEventListener('onSegmentError', (errorDetails) => {
|
|
151
|
+
* console.error('Error loading segment:', errorDetails);
|
|
152
|
+
* });
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* // Tracking data downloaded from peers
|
|
156
|
+
* p2pEngine.addEventListener('onChunkDownloaded', (bytesLength, downloadSource, peerId) => {
|
|
157
|
+
* console.log(`Downloaded ${bytesLength} bytes from ${downloadSource} ${peerId ? 'from peer ' + peerId : 'from server'}`);
|
|
158
|
+
* });
|
|
159
|
+
*/
|
|
160
|
+
addEventListener<K extends keyof CoreEventMap>(
|
|
161
|
+
eventName: K,
|
|
162
|
+
listener: CoreEventMap[K],
|
|
163
|
+
) {
|
|
164
|
+
this.core.addEventListener(eventName, listener);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Removes an event listener for the specified event.
|
|
169
|
+
* @param eventName The name of the event.
|
|
170
|
+
* @param listener The callback function that was previously added.
|
|
171
|
+
*/
|
|
172
|
+
removeEventListener<K extends keyof CoreEventMap>(
|
|
173
|
+
eventName: K,
|
|
174
|
+
listener: CoreEventMap[K],
|
|
175
|
+
) {
|
|
176
|
+
this.core.removeEventListener(eventName, listener);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* provides the Hls.js P2P specific configuration for Hls.js loaders.
|
|
181
|
+
* @returns An object with fragment loader (fLoader) and playlist loader (pLoader).
|
|
182
|
+
*/
|
|
183
|
+
getConfigForHlsJs(): { fLoader: unknown; pLoader: unknown } {
|
|
184
|
+
return {
|
|
185
|
+
fLoader: this.createFragmentLoaderClass(),
|
|
186
|
+
pLoader: this.createPlaylistLoaderClass(),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Returns the configuration of the HLS.js P2P engine.
|
|
192
|
+
* @returns A readonly version of the HlsJsP2PEngineConfig.
|
|
193
|
+
*/
|
|
194
|
+
getConfig(): HlsJsP2PEngineConfig {
|
|
195
|
+
return { core: this.core.getConfig() };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Applies dynamic configuration updates to the P2P engine.
|
|
200
|
+
* @param dynamicConfig Configuration changes to apply.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* // Assuming `hlsP2PEngine` is an instance of HlsJsP2PEngine
|
|
204
|
+
*
|
|
205
|
+
* const newDynamicConfig = {
|
|
206
|
+
* core: {
|
|
207
|
+
* // Increase the number of cached segments to 1000
|
|
208
|
+
* cachedSegmentsCount: 1000,
|
|
209
|
+
* // 50 minutes of segments will be downloaded further through HTTP connections if P2P fails
|
|
210
|
+
* httpDownloadTimeWindow: 3000,
|
|
211
|
+
* // 100 minutes of segments will be downloaded further through P2P connections
|
|
212
|
+
* p2pDownloadTimeWindow: 6000,
|
|
213
|
+
* };
|
|
214
|
+
*
|
|
215
|
+
* hlsP2PEngine.applyDynamicConfig(newDynamicConfig);
|
|
216
|
+
*/
|
|
217
|
+
applyDynamicConfig(dynamicConfig: DynamicHlsJsP2PEngineConfig) {
|
|
218
|
+
if (dynamicConfig.core) this.core.applyDynamicConfig(dynamicConfig.core);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Sets the HLS instance for handling media.
|
|
223
|
+
* @param hls The HLS instance or a function that returns an HLS instance.
|
|
224
|
+
*/
|
|
225
|
+
bindHls<T = unknown>(hls: T | (() => T)) {
|
|
226
|
+
this.hlsInstanceGetter =
|
|
227
|
+
typeof hls === "function" ? (hls as () => Hls) : () => hls as Hls;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private initHlsEvents() {
|
|
231
|
+
const hlsInstance = this.hlsInstanceGetter?.();
|
|
232
|
+
if (this.currentHlsInstance === hlsInstance) return;
|
|
233
|
+
if (this.currentHlsInstance) this.destroy();
|
|
234
|
+
this.currentHlsInstance = hlsInstance;
|
|
235
|
+
this.updateHlsEventsHandlers("register");
|
|
236
|
+
this.updateMediaElementEventHandlers("register");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private updateHlsEventsHandlers(type: "register" | "unregister") {
|
|
240
|
+
const hls = this.currentHlsInstance;
|
|
241
|
+
if (!hls) return;
|
|
242
|
+
const method = type === "register" ? "on" : "off";
|
|
243
|
+
|
|
244
|
+
hls[method](
|
|
245
|
+
"hlsManifestLoaded" as Events.MANIFEST_LOADED,
|
|
246
|
+
this.handleManifestLoaded,
|
|
247
|
+
);
|
|
248
|
+
hls[method](
|
|
249
|
+
"hlsLevelSwitching" as Events.LEVEL_SWITCHING,
|
|
250
|
+
this.handleLevelSwitching,
|
|
251
|
+
);
|
|
252
|
+
hls[method](
|
|
253
|
+
"hlsLevelUpdated" as Events.LEVEL_UPDATED,
|
|
254
|
+
this.handleLevelUpdated,
|
|
255
|
+
);
|
|
256
|
+
hls[method](
|
|
257
|
+
"hlsAudioTrackLoaded" as Events.AUDIO_TRACK_LOADED,
|
|
258
|
+
this.handleLevelUpdated,
|
|
259
|
+
);
|
|
260
|
+
hls[method]("hlsDestroying" as Events.DESTROYING, this.destroy);
|
|
261
|
+
hls[method](
|
|
262
|
+
"hlsMediaAttaching" as Events.MEDIA_ATTACHING,
|
|
263
|
+
this.destroyCore,
|
|
264
|
+
);
|
|
265
|
+
hls[method](
|
|
266
|
+
"hlsManifestLoading" as Events.MANIFEST_LOADING,
|
|
267
|
+
this.destroyCore,
|
|
268
|
+
);
|
|
269
|
+
hls[method](
|
|
270
|
+
"hlsMediaDetached" as Events.MEDIA_DETACHED,
|
|
271
|
+
this.handleMediaDetached,
|
|
272
|
+
);
|
|
273
|
+
hls[method](
|
|
274
|
+
"hlsMediaAttached" as Events.MEDIA_ATTACHED,
|
|
275
|
+
this.handleMediaAttached,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private updateMediaElementEventHandlers = (
|
|
280
|
+
type: "register" | "unregister",
|
|
281
|
+
) => {
|
|
282
|
+
const media = this.currentHlsInstance?.media;
|
|
283
|
+
if (!media) return;
|
|
284
|
+
const method =
|
|
285
|
+
type === "register" ? "addEventListener" : "removeEventListener";
|
|
286
|
+
media[method]("timeupdate", this.handlePlaybackUpdate);
|
|
287
|
+
media[method]("seeking", this.handlePlaybackUpdate);
|
|
288
|
+
media[method]("ratechange", this.handlePlaybackUpdate);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
private handleManifestLoaded = (event: string, data: ManifestLoadedData) => {
|
|
292
|
+
// eslint-disable-next-line prefer-destructuring
|
|
293
|
+
const networkDetails: unknown = data.networkDetails;
|
|
294
|
+
if (networkDetails instanceof XMLHttpRequest) {
|
|
295
|
+
this.core.setManifestResponseUrl(networkDetails.responseURL);
|
|
296
|
+
} else if (networkDetails instanceof Response) {
|
|
297
|
+
this.core.setManifestResponseUrl(networkDetails.url);
|
|
298
|
+
}
|
|
299
|
+
this.segmentManager.processMainManifest(data);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
private handleLevelSwitching = (event: string, data: LevelSwitchingData) => {
|
|
303
|
+
if (data.bitrate) this.core.setActiveLevelBitrate(data.bitrate);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
private handleLevelUpdated = (
|
|
307
|
+
event: string,
|
|
308
|
+
data: LevelUpdatedData | AudioTrackLoadedData,
|
|
309
|
+
) => {
|
|
310
|
+
if (
|
|
311
|
+
this.currentHlsInstance &&
|
|
312
|
+
data.details.live &&
|
|
313
|
+
data.details.fragments[0].type === ("main" as PlaylistLevelType) &&
|
|
314
|
+
!this.currentHlsInstance.userConfig.liveSyncDuration &&
|
|
315
|
+
!this.currentHlsInstance.userConfig.liveSyncDurationCount &&
|
|
316
|
+
data.details.fragments.length > 4
|
|
317
|
+
) {
|
|
318
|
+
this.updateLiveSyncDurationCount(data);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
this.core.setIsLive(data.details.live);
|
|
322
|
+
this.segmentManager.updatePlaylist(data);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
private updateLiveSyncDurationCount(
|
|
326
|
+
data: LevelUpdatedData | AudioTrackLoadedData,
|
|
327
|
+
) {
|
|
328
|
+
const fragmentDuration = data.details.targetduration;
|
|
329
|
+
|
|
330
|
+
const maxLiveSyncCount = Math.floor(
|
|
331
|
+
MAX_LIVE_SYNC_DURATION / fragmentDuration,
|
|
332
|
+
);
|
|
333
|
+
const newLiveSyncDurationCount = Math.min(
|
|
334
|
+
data.details.fragments.length - 1,
|
|
335
|
+
maxLiveSyncCount,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (
|
|
339
|
+
this.currentHlsInstance &&
|
|
340
|
+
this.currentHlsInstance.config.liveSyncDurationCount !==
|
|
341
|
+
newLiveSyncDurationCount
|
|
342
|
+
) {
|
|
343
|
+
this.debug(
|
|
344
|
+
`Setting liveSyncDurationCount to ${newLiveSyncDurationCount}`,
|
|
345
|
+
);
|
|
346
|
+
this.currentHlsInstance.config.liveSyncDurationCount =
|
|
347
|
+
newLiveSyncDurationCount;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private handleMediaAttached = () => {
|
|
352
|
+
this.updateMediaElementEventHandlers("register");
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
private handleMediaDetached = () => {
|
|
356
|
+
this.updateMediaElementEventHandlers("unregister");
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
private handlePlaybackUpdate = (event: Event) => {
|
|
360
|
+
const media = event.target as HTMLMediaElement;
|
|
361
|
+
this.core.updatePlayback(media.currentTime, media.playbackRate);
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
private destroyCore = () => this.core.destroy();
|
|
365
|
+
|
|
366
|
+
/** Clean up and release all resources. Unregister all event handlers. */
|
|
367
|
+
destroy = () => {
|
|
368
|
+
this.destroyCore();
|
|
369
|
+
this.updateHlsEventsHandlers("unregister");
|
|
370
|
+
this.updateMediaElementEventHandlers("unregister");
|
|
371
|
+
this.currentHlsInstance = undefined;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
private createFragmentLoaderClass() {
|
|
375
|
+
const { core } = this;
|
|
376
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
377
|
+
const engine = this;
|
|
378
|
+
|
|
379
|
+
return class FragmentLoader extends FragmentLoaderBase {
|
|
380
|
+
constructor(config: HlsConfig) {
|
|
381
|
+
super(config, core);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
static getEngine() {
|
|
385
|
+
return engine;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private createPlaylistLoaderClass() {
|
|
391
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
392
|
+
const engine = this;
|
|
393
|
+
return class PlaylistLoader extends PlaylistLoaderBase {
|
|
394
|
+
constructor(config: HlsConfig) {
|
|
395
|
+
super(config);
|
|
396
|
+
engine.initHlsEvents();
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FragmentLoaderContext,
|
|
3
|
+
HlsConfig,
|
|
4
|
+
Loader,
|
|
5
|
+
LoaderCallbacks,
|
|
6
|
+
LoaderConfiguration,
|
|
7
|
+
LoaderContext,
|
|
8
|
+
LoaderStats,
|
|
9
|
+
} from "hls.js";
|
|
10
|
+
import * as Utils from "./utils.js";
|
|
11
|
+
import { Core, SegmentResponse, CoreRequestError } from "p2p-media-loader-core";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_DOWNLOAD_LATENCY = 10;
|
|
14
|
+
|
|
15
|
+
export class FragmentLoaderBase implements Loader<FragmentLoaderContext> {
|
|
16
|
+
context!: FragmentLoaderContext;
|
|
17
|
+
config!: LoaderConfiguration | null;
|
|
18
|
+
stats: LoaderStats;
|
|
19
|
+
#callbacks!: LoaderCallbacks<FragmentLoaderContext> | null;
|
|
20
|
+
#createDefaultLoader: () => Loader<LoaderContext>;
|
|
21
|
+
#defaultLoader?: Loader<LoaderContext>;
|
|
22
|
+
#core: Core;
|
|
23
|
+
#response?: SegmentResponse;
|
|
24
|
+
#segmentId?: string;
|
|
25
|
+
|
|
26
|
+
constructor(config: HlsConfig, core: Core) {
|
|
27
|
+
this.#core = core;
|
|
28
|
+
this.#createDefaultLoader = () => new config.loader(config);
|
|
29
|
+
this.stats = {
|
|
30
|
+
aborted: false,
|
|
31
|
+
chunkCount: 0,
|
|
32
|
+
loading: { start: 0, first: 0, end: 0 },
|
|
33
|
+
buffering: { start: 0, first: 0, end: 0 },
|
|
34
|
+
parsing: { start: 0, end: 0 },
|
|
35
|
+
// set total and loaded to 1 to prevent hls.js
|
|
36
|
+
// on progress loading monitoring in AbrController
|
|
37
|
+
total: 1,
|
|
38
|
+
loaded: 1,
|
|
39
|
+
bwEstimate: 0,
|
|
40
|
+
retry: 0,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
load(
|
|
45
|
+
context: FragmentLoaderContext,
|
|
46
|
+
config: LoaderConfiguration,
|
|
47
|
+
callbacks: LoaderCallbacks<LoaderContext>,
|
|
48
|
+
) {
|
|
49
|
+
this.context = context;
|
|
50
|
+
this.config = config;
|
|
51
|
+
this.#callbacks = callbacks;
|
|
52
|
+
const { stats } = this;
|
|
53
|
+
|
|
54
|
+
const { rangeStart: start, rangeEnd: end } = context;
|
|
55
|
+
const byteRange = Utils.getByteRange(
|
|
56
|
+
start,
|
|
57
|
+
end !== undefined ? end - 1 : undefined,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
this.#segmentId = Utils.getSegmentRuntimeId(context.url, byteRange);
|
|
61
|
+
const isSegmentDownloadableByP2PCore = this.#core.isSegmentLoadable(
|
|
62
|
+
this.#segmentId,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
!this.#core.hasSegment(this.#segmentId) ||
|
|
67
|
+
!isSegmentDownloadableByP2PCore
|
|
68
|
+
) {
|
|
69
|
+
this.#defaultLoader = this.#createDefaultLoader();
|
|
70
|
+
this.#defaultLoader.stats = this.stats;
|
|
71
|
+
this.#defaultLoader.load(context, config, callbacks);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const onSuccess = (response: SegmentResponse) => {
|
|
76
|
+
this.#response = response;
|
|
77
|
+
const loadedBytes = this.#response.data.byteLength;
|
|
78
|
+
stats.loading = getLoadingStat(
|
|
79
|
+
this.#response.bandwidth,
|
|
80
|
+
loadedBytes,
|
|
81
|
+
performance.now(),
|
|
82
|
+
);
|
|
83
|
+
stats.total = loadedBytes;
|
|
84
|
+
stats.loaded = loadedBytes;
|
|
85
|
+
|
|
86
|
+
if (callbacks.onProgress) {
|
|
87
|
+
callbacks.onProgress(
|
|
88
|
+
this.stats,
|
|
89
|
+
context,
|
|
90
|
+
this.#response.data,
|
|
91
|
+
undefined,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
callbacks.onSuccess(
|
|
95
|
+
{ data: this.#response.data, url: context.url },
|
|
96
|
+
this.stats,
|
|
97
|
+
context,
|
|
98
|
+
undefined,
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const onError = (error: unknown) => {
|
|
103
|
+
if (
|
|
104
|
+
error instanceof CoreRequestError &&
|
|
105
|
+
error.type === "aborted" &&
|
|
106
|
+
this.stats.aborted
|
|
107
|
+
) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
this.#handleError(error);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
void this.#core.loadSegment(this.#segmentId, { onSuccess, onError });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#handleError(thrownError: unknown) {
|
|
117
|
+
const error = { code: 0, text: "" };
|
|
118
|
+
if (
|
|
119
|
+
thrownError instanceof CoreRequestError &&
|
|
120
|
+
thrownError.type === "failed"
|
|
121
|
+
) {
|
|
122
|
+
// error.code = thrownError.code;
|
|
123
|
+
error.text = thrownError.message;
|
|
124
|
+
} else if (thrownError instanceof Error) {
|
|
125
|
+
error.text = thrownError.message;
|
|
126
|
+
}
|
|
127
|
+
this.#callbacks?.onError(error, this.context, null, this.stats);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#abortInternal() {
|
|
131
|
+
if (!this.#response && this.#segmentId) {
|
|
132
|
+
this.stats.aborted = true;
|
|
133
|
+
this.#core.abortSegmentLoading(this.#segmentId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
abort() {
|
|
138
|
+
if (this.#defaultLoader) {
|
|
139
|
+
this.#defaultLoader.abort();
|
|
140
|
+
} else {
|
|
141
|
+
this.#abortInternal();
|
|
142
|
+
this.#callbacks?.onAbort?.(this.stats, this.context, {});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
destroy() {
|
|
147
|
+
if (this.#defaultLoader) {
|
|
148
|
+
this.#defaultLoader.destroy();
|
|
149
|
+
} else {
|
|
150
|
+
if (!this.stats.aborted) this.#abortInternal();
|
|
151
|
+
this.#callbacks = null;
|
|
152
|
+
this.config = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getLoadingStat(
|
|
158
|
+
targetBitrate: number,
|
|
159
|
+
loadedBytes: number,
|
|
160
|
+
loadingEndTime: number,
|
|
161
|
+
) {
|
|
162
|
+
const timeForLoading = (loadedBytes * 8000) / targetBitrate;
|
|
163
|
+
const first = loadingEndTime - timeForLoading;
|
|
164
|
+
const start = first - DEFAULT_DOWNLOAD_LATENCY;
|
|
165
|
+
|
|
166
|
+
return { start, first, end: loadingEndTime };
|
|
167
|
+
}
|