p2p-media-loader-core
Version:
P2P Media Loader core functionality
466 lines • 18.4 kB
JavaScript
import { HybridLoader } from "./hybrid-loader.js";
import * as StreamUtils from "./utils/stream.js";
import { BandwidthCalculator } from "./bandwidth-calculator.js";
import { SegmentMemoryStorage } from "./segment-storage/segment-memory-storage.js";
import { EventTarget } from "./utils/event-target.js";
import { overrideConfig, mergeAndFilterConfig, deepCopy, filterUndefinedProps, } from "./utils/utils.js";
import { TRACKER_CLIENT_VERSION_PREFIX } from "./utils/peer.js";
import { P2PTrackerClient } from "./p2p/tracker-client.js";
/** Core class for managing media streams loading via P2P. */
export class Core {
/**
* Constructs a new Core instance with optional initial configuration.
*
* @param config - Optional partial configuration to override default settings.
*
* @example
* // Create a Core instance with custom configuration for HTTP and P2P downloads.
* const core = new Core({
* simultaneousHttpDownloads: 5,
* simultaneousP2PDownloads: 5,
* httpErrorRetries: 5,
* p2pErrorRetries: 5
* });
*
* @example
* // Create a Core instance using the default configuration.
* const core = new Core();
*/
constructor(config) {
Object.defineProperty(this, "eventTarget", {
enumerable: true,
configurable: true,
writable: true,
value: new EventTarget()
});
Object.defineProperty(this, "manifestResponseUrl", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "streams", {
enumerable: true,
configurable: true,
writable: true,
value: new Map()
});
Object.defineProperty(this, "mainStreamConfig", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "secondaryStreamConfig", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "commonCoreConfig", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "bandwidthCalculators", {
enumerable: true,
configurable: true,
writable: true,
value: {
all: new BandwidthCalculator(),
http: new BandwidthCalculator(),
}
});
Object.defineProperty(this, "segmentStorage", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "mainStreamLoader", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "secondaryStreamLoader", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "streamDetails", {
enumerable: true,
configurable: true,
writable: true,
value: {
isLive: false,
activeLevelBitrate: 0,
}
});
const filteredConfig = filterUndefinedProps(config ?? {});
this.commonCoreConfig = mergeAndFilterConfig({
defaultConfig: Core.DEFAULT_COMMON_CORE_CONFIG,
baseConfig: filteredConfig,
});
this.mainStreamConfig = mergeAndFilterConfig({
defaultConfig: Core.DEFAULT_STREAM_CONFIG,
baseConfig: filteredConfig,
specificStreamConfig: filteredConfig.mainStream,
});
this.secondaryStreamConfig = mergeAndFilterConfig({
defaultConfig: Core.DEFAULT_STREAM_CONFIG,
baseConfig: filteredConfig,
specificStreamConfig: filteredConfig.secondaryStream,
});
}
/**
* Retrieves the current configuration for the core instance, ensuring immutability.
*
* @returns A deep readonly version of the core configuration.
*/
getConfig() {
return {
...deepCopy(this.commonCoreConfig),
mainStream: deepCopy(this.mainStreamConfig),
secondaryStream: deepCopy(this.secondaryStreamConfig),
};
}
/**
* Applies a set of dynamic configuration updates to the core, merging with the existing configuration.
*
* @param dynamicConfig - A set of configuration changes to apply.
*
* @example
* // Example of dynamically updating the download time windows and timeout settings.
* const dynamicConfig = {
* httpDownloadTimeWindow: 60, // Set HTTP download time window to 60 seconds
* p2pDownloadTimeWindow: 60, // Set P2P download time window to 60 seconds
* httpNotReceivingBytesTimeoutMs: 1500, // Set HTTP timeout to 1500 milliseconds
* p2pNotReceivingBytesTimeoutMs: 1500 // Set P2P timeout to 1500 milliseconds
* };
* core.applyDynamicConfig(dynamicConfig);
*/
applyDynamicConfig(dynamicConfig) {
const { mainStream, secondaryStream } = dynamicConfig;
const mainStreamConfigCopy = deepCopy(this.mainStreamConfig);
const secondaryStreamConfigCopy = deepCopy(this.secondaryStreamConfig);
this.overrideAllConfigs(dynamicConfig, mainStream, secondaryStream);
this.processSpecificDynamicConfigParams(mainStreamConfigCopy, dynamicConfig, "main");
this.processSpecificDynamicConfigParams(secondaryStreamConfigCopy, dynamicConfig, "secondary");
}
processSpecificDynamicConfigParams(prevConfig, updatedConfig, streamType) {
const isP2PDisabled = this.getUpdatedStreamProperty("isP2PDisabled", updatedConfig, streamType);
if (isP2PDisabled && prevConfig.isP2PDisabled !== isP2PDisabled) {
this.destroyStreamLoader(streamType);
}
const isP2PUploadDisabled = this.getUpdatedStreamProperty("isP2PUploadDisabled", updatedConfig, streamType);
if (isP2PUploadDisabled !== undefined &&
prevConfig.isP2PUploadDisabled !== isP2PUploadDisabled) {
const streamLoader = streamType === "main"
? this.mainStreamLoader
: this.secondaryStreamLoader;
streamLoader?.sendBroadcastAnnouncement(isP2PUploadDisabled);
}
}
getUpdatedStreamProperty(propertyName, updatedConfig, streamType) {
const updatedStreamConfig = streamType === "main"
? updatedConfig.mainStream
: updatedConfig.secondaryStream;
return updatedStreamConfig?.[propertyName] ?? updatedConfig[propertyName];
}
/**
* Adds an event listener for the specified event type on the core event target.
*
* @param eventName - The name of the event to listen for.
* @param listener - The callback function to invoke when the event is fired.
*/
addEventListener(eventName, listener) {
this.eventTarget.addEventListener(eventName, listener);
}
/**
* Removes an event listener for the specified event type on the core event target.
*
* @param eventName - The name of the event to listen for.
* @param listener - The callback function to be removed.
*/
removeEventListener(eventName, listener) {
this.eventTarget.removeEventListener(eventName, listener);
}
/**
* Sets the response URL for the manifest, stripping any query parameters.
*
* @param url - The full URL to the manifest response.
*/
setManifestResponseUrl(url) {
this.manifestResponseUrl = url.split("?")[0];
}
/**
* Checks if a segment is already stored within the core.
*
* @param segmentRuntimeId - The runtime identifier of the segment to check.
* @returns `true` if the segment is present, otherwise `false`.
*/
hasSegment(segmentRuntimeId) {
return !!StreamUtils.getSegmentFromStreamsMap(this.streams, segmentRuntimeId);
}
/**
* Retrieves a specific stream by its runtime identifier, if it exists.
*
* @param streamRuntimeId - The runtime identifier of the stream to retrieve.
* @returns The stream with its segments, or `undefined` if not found.
*/
getStream(streamRuntimeId) {
return this.streams.get(streamRuntimeId);
}
/**
* Ensures a stream exists in the map; adds it if it does not.
*
* @param stream - The stream to potentially add to the map.
*/
addStreamIfNoneExists(stream) {
if (this.streams.has(stream.runtimeId))
return;
this.streams.set(stream.runtimeId, {
...stream,
segments: new Map(),
});
}
/**
* Updates the segments associated with a specific stream.
*
* @param streamRuntimeId - The runtime identifier of the stream to update.
* @param addSegments - Optional segments to add to the stream.
* @param removeSegmentIds - Optional segment IDs to remove from the stream.
*/
updateStream(streamRuntimeId, addSegments, removeSegmentIds) {
const stream = this.streams.get(streamRuntimeId);
if (!stream)
return;
if (addSegments) {
for (const segment of addSegments) {
if (stream.segments.has(segment.runtimeId))
continue; // should not happen
stream.segments.set(segment.runtimeId, { ...segment, stream });
}
}
if (removeSegmentIds) {
for (const id of removeSegmentIds) {
stream.segments.delete(id);
}
}
this.mainStreamLoader?.updateStream(stream);
this.secondaryStreamLoader?.updateStream(stream);
}
/**
* Loads a segment given its runtime identifier and invokes the provided callbacks during the process.
* Initializes segment storage if it has not been initialized yet.
*
* @param segmentRuntimeId - The runtime identifier of the segment to load.
* @param callbacks - The callbacks to be invoked during segment loading.
* @throws {Error} - Throws if the manifest response URL is not defined.
*/
async loadSegment(segmentRuntimeId, callbacks) {
if (!this.manifestResponseUrl) {
throw new Error("Manifest response url is not defined");
}
await this.initializeSegmentStorage();
const segment = this.identifySegment(segmentRuntimeId);
const loader = this.getStreamHybridLoader(segment);
void loader.loadSegment(segment, callbacks);
}
/**
* Aborts the loading of a segment specified by its runtime identifier.
*
* @param segmentRuntimeId - The runtime identifier of the segment whose loading is to be aborted.
*/
abortSegmentLoading(segmentRuntimeId) {
this.mainStreamLoader?.abortSegmentRequest(segmentRuntimeId);
this.secondaryStreamLoader?.abortSegmentRequest(segmentRuntimeId);
}
/**
* Updates the playback parameters while play head moves, specifically position and playback rate, for stream loaders.
*
* @param position - The new position in the stream, in seconds.
* @param rate - The new playback rate.
*/
updatePlayback(position, rate) {
this.mainStreamLoader?.updatePlayback(position, rate);
this.secondaryStreamLoader?.updatePlayback(position, rate);
}
/**
* Sets the active level bitrate, used for adjusting quality levels in adaptive streaming.
* Notifies the stream loaders if a change occurs.
*
* @param bitrate - The new bitrate to set as active.
*/
setActiveLevelBitrate(bitrate) {
if (bitrate !== this.streamDetails.activeLevelBitrate) {
this.streamDetails.activeLevelBitrate = bitrate;
this.mainStreamLoader?.notifyLevelChanged();
this.secondaryStreamLoader?.notifyLevelChanged();
}
}
/**
* Updates the 'isLive' status of the stream
*
* @param isLive - Boolean indicating whether the stream is live.
*/
setIsLive(isLive) {
this.streamDetails.isLive = isLive;
}
/**
* Identify if a segment is loadable by the P2P core based on the segment's stream type and configuration.
* @param segmentRuntimeId Segment runtime identifier to check.
* @returns `true` if the segment is loadable by the P2P core, otherwise `false`.
*/
isSegmentLoadable(segmentRuntimeId) {
try {
const segment = this.identifySegment(segmentRuntimeId);
if (segment.stream.type === "main" &&
this.mainStreamConfig.isP2PDisabled) {
return false;
}
if (segment.stream.type === "secondary" &&
this.secondaryStreamConfig.isP2PDisabled) {
return false;
}
return true;
}
catch {
return false;
}
}
/**
* Cleans up resources used by the Core instance, including destroying any active stream loaders
* and clearing stored segments.
*/
destroy() {
this.streams.clear();
this.mainStreamLoader?.destroy();
this.secondaryStreamLoader?.destroy();
this.segmentStorage?.destroy();
this.mainStreamLoader = undefined;
this.secondaryStreamLoader = undefined;
this.segmentStorage = undefined;
this.manifestResponseUrl = undefined;
this.streamDetails = { isLive: false, activeLevelBitrate: 0 };
P2PTrackerClient.clearPeerIdCache();
}
async initializeSegmentStorage() {
if (this.segmentStorage)
return;
const { isLive } = this.streamDetails;
const createCustomStorage = this.commonCoreConfig.customSegmentStorageFactory;
if (createCustomStorage && typeof createCustomStorage !== "function") {
throw new Error("Storage configuration is invalid");
}
const segmentStorage = createCustomStorage
? createCustomStorage(isLive)
: new SegmentMemoryStorage();
await segmentStorage.initialize(this.commonCoreConfig, this.mainStreamConfig, this.secondaryStreamConfig);
this.segmentStorage = segmentStorage;
}
identifySegment(segmentRuntimeId) {
if (!this.manifestResponseUrl) {
throw new Error("Manifest response url is undefined");
}
const segment = StreamUtils.getSegmentFromStreamsMap(this.streams, segmentRuntimeId);
if (!segment) {
throw new Error(`Not found segment with id: ${segmentRuntimeId}`);
}
return segment;
}
overrideAllConfigs(dynamicConfig, mainStream, secondaryStream) {
overrideConfig(this.commonCoreConfig, dynamicConfig);
overrideConfig(this.mainStreamConfig, dynamicConfig);
overrideConfig(this.secondaryStreamConfig, dynamicConfig);
if (mainStream) {
overrideConfig(this.mainStreamConfig, mainStream);
}
if (secondaryStream) {
overrideConfig(this.secondaryStreamConfig, secondaryStream);
}
}
destroyStreamLoader(streamType) {
if (streamType === "main") {
this.mainStreamLoader?.destroy();
this.mainStreamLoader = undefined;
}
else {
this.secondaryStreamLoader?.destroy();
this.secondaryStreamLoader = undefined;
}
}
getStreamHybridLoader(segment) {
if (segment.stream.type === "main") {
this.mainStreamLoader ?? (this.mainStreamLoader = this.createNewHybridLoader(segment));
return this.mainStreamLoader;
}
else {
this.secondaryStreamLoader ?? (this.secondaryStreamLoader = this.createNewHybridLoader(segment));
return this.secondaryStreamLoader;
}
}
createNewHybridLoader(segment) {
if (!this.manifestResponseUrl) {
throw new Error("Manifest response url is not defined");
}
if (!this.segmentStorage) {
throw new Error("Segment storage is not initialized");
}
const streamConfig = segment.stream.type === "main"
? this.mainStreamConfig
: this.secondaryStreamConfig;
return new HybridLoader(this.manifestResponseUrl, segment, this.streamDetails, streamConfig, this.bandwidthCalculators, this.segmentStorage, this.eventTarget);
}
}
/** Default configuration for common core settings. */
Object.defineProperty(Core, "DEFAULT_COMMON_CORE_CONFIG", {
enumerable: true,
configurable: true,
writable: true,
value: {
segmentMemoryStorageLimit: undefined,
customSegmentStorageFactory: undefined,
}
});
/** Default configuration for stream settings. */
Object.defineProperty(Core, "DEFAULT_STREAM_CONFIG", {
enumerable: true,
configurable: true,
writable: true,
value: {
isP2PUploadDisabled: false,
isP2PDisabled: false,
simultaneousHttpDownloads: 2,
simultaneousP2PDownloads: 3,
highDemandTimeWindow: 15,
httpDownloadTimeWindow: 3000,
p2pDownloadTimeWindow: 6000,
webRtcMaxMessageSize: 64 * 1024 - 1,
p2pNotReceivingBytesTimeoutMs: 2000,
p2pInactiveLoaderDestroyTimeoutMs: 30 * 1000,
httpNotReceivingBytesTimeoutMs: 3000,
httpErrorRetries: 3,
p2pErrorRetries: 3,
trackerClientVersionPrefix: TRACKER_CLIENT_VERSION_PREFIX,
announceTrackers: [
"wss://tracker.novage.com.ua",
"wss://tracker.webtorrent.dev",
"wss://tracker.openwebtorrent.com",
],
rtcConfig: {
iceServers: [
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "stun:global.stun.twilio.com:3478" },
],
},
validateP2PSegment: undefined,
validateHTTPSegment: undefined,
httpRequestSetup: undefined,
swarmId: undefined,
}
});
//# sourceMappingURL=core.js.map