@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.
@@ -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
@@ -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
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { HlsJsP2PEngine } from "./engine.js";
2
+
3
+ export type {
4
+ DynamicHlsJsP2PEngineConfig,
5
+ HlsJsP2PEngineConfig,
6
+ PartialHlsJsP2PEngineConfig,
7
+ HlsWithP2PInstance,
8
+ HlsWithP2PConfig,
9
+ } from "./engine.js";