p2p-media-loader-core
Version:
P2P Media Loader core functionality
361 lines • 15.2 kB
JavaScript
import debug from "debug";
import { RequestError, } from "../types.js";
import * as Utils from "../utils/utils.js";
import * as Command from "./commands/index.js";
import { PeerProtocol } from "./peer-protocol.js";
const { PeerCommandType } = Command;
export class Peer {
constructor(connection, eventHandlers, peerConfig, streamType, eventTarget) {
Object.defineProperty(this, "connection", {
enumerable: true,
configurable: true,
writable: true,
value: connection
});
Object.defineProperty(this, "eventHandlers", {
enumerable: true,
configurable: true,
writable: true,
value: eventHandlers
});
Object.defineProperty(this, "peerConfig", {
enumerable: true,
configurable: true,
writable: true,
value: peerConfig
});
Object.defineProperty(this, "streamType", {
enumerable: true,
configurable: true,
writable: true,
value: streamType
});
Object.defineProperty(this, "eventTarget", {
enumerable: true,
configurable: true,
writable: true,
value: eventTarget
});
Object.defineProperty(this, "id", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "peerProtocol", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "downloadingContext", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "loadedSegments", {
enumerable: true,
configurable: true,
writable: true,
value: new Set()
});
Object.defineProperty(this, "httpLoadingSegments", {
enumerable: true,
configurable: true,
writable: true,
value: new Set()
});
Object.defineProperty(this, "downloadingErrors", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "logger", {
enumerable: true,
configurable: true,
writable: true,
value: debug("p2pml-core:peer")
});
Object.defineProperty(this, "onPeerClosed", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "onCommandReceived", {
enumerable: true,
configurable: true,
writable: true,
value: async (command) => {
switch (command.c) {
case PeerCommandType.SegmentsAnnouncement:
this.loadedSegments = new Set(command.l);
this.httpLoadingSegments = new Set(command.p);
this.eventHandlers.onSegmentsAnnouncement();
break;
case PeerCommandType.SegmentRequest:
this.peerProtocol.stopUploadingSegmentData();
this.eventHandlers.onSegmentRequested(this, command.i, command.r, command.b);
break;
case PeerCommandType.SegmentData:
{
if (!this.downloadingContext)
break;
if (this.downloadingContext.isSegmentDataCommandReceived)
break;
const { request, controls, requestId } = this.downloadingContext;
if (request.segment.externalId !== command.i ||
requestId !== command.r) {
break;
}
this.downloadingContext.isSegmentDataCommandReceived = true;
controls.firstBytesReceived();
if (request.totalBytes === undefined) {
request.setTotalBytes(command.s);
}
else if (request.totalBytes - request.loadedBytes !== command.s) {
request.clearLoadedBytes();
this.sendCancelSegmentRequestCommand(request.segment, requestId);
this.cancelSegmentDownloading("peer-response-bytes-length-mismatch");
this.destroy();
}
}
break;
case PeerCommandType.SegmentDataSendingCompleted: {
const { downloadingContext } = this;
if (!downloadingContext?.isSegmentDataCommandReceived)
return;
const { request, controls } = downloadingContext;
const isWrongSegment = downloadingContext.request.segment.externalId !== command.i ||
downloadingContext.requestId !== command.r;
if (isWrongSegment) {
request.clearLoadedBytes();
this.cancelSegmentDownloading("peer-protocol-violation");
this.destroy();
return;
}
const isWrongBytes = request.loadedBytes !== request.totalBytes;
if (isWrongBytes) {
request.clearLoadedBytes();
this.cancelSegmentDownloading("peer-response-bytes-length-mismatch");
this.destroy();
return;
}
const isValid = (await this.peerConfig.validateP2PSegment?.(request.segment.url, request.segment.byteRange, request.data)) ?? true;
if (this.downloadingContext !== downloadingContext)
return;
if (!isValid) {
request.clearLoadedBytes();
this.cancelSegmentDownloading("p2p-segment-validation-failed");
this.destroy();
return;
}
this.downloadingErrors = [];
controls.completeOnSuccess();
this.downloadingContext = undefined;
break;
}
case PeerCommandType.SegmentAbsent:
if (this.downloadingContext?.request.segment.externalId === command.i &&
this.downloadingContext.requestId === command.r) {
this.cancelSegmentDownloading("peer-segment-absent");
this.loadedSegments.delete(command.i);
}
break;
case PeerCommandType.CancelSegmentRequest: {
const uploadingRequestId = this.peerProtocol.getUploadingRequestId();
if (uploadingRequestId !== command.r)
break;
this.peerProtocol.stopUploadingSegmentData();
break;
}
}
}
});
Object.defineProperty(this, "onSegmentChunkReceived", {
enumerable: true,
configurable: true,
writable: true,
value: (chunk) => {
if (!this.downloadingContext?.isSegmentDataCommandReceived)
return;
const { request, controls } = this.downloadingContext;
const isOverflow = request.totalBytes !== undefined &&
request.loadedBytes + chunk.byteLength > request.totalBytes;
if (isOverflow) {
request.clearLoadedBytes();
this.cancelSegmentDownloading("peer-response-bytes-length-mismatch");
this.destroy();
return;
}
controls.addLoadedChunk(chunk);
}
});
Object.defineProperty(this, "onPeerConnectionClosed", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
this.destroy();
}
});
Object.defineProperty(this, "onConnectionError", {
enumerable: true,
configurable: true,
writable: true,
value: (error) => {
this.logger(`peer connection error ${this.id} %O`, error);
this.eventTarget.getEventDispatcher("onPeerError")({
peerId: this.id,
streamType: this.streamType,
error,
});
const { code } = error;
if (code === "ERR_DATA_CHANNEL") {
this.destroy();
}
else if (code === "ERR_CONNECTION_FAILURE") {
this.destroy();
}
}
});
Object.defineProperty(this, "destroy", {
enumerable: true,
configurable: true,
writable: true,
value: () => {
this.cancelSegmentDownloading("peer-closed");
this.connection.destroy();
this.eventHandlers.onPeerClosed(this);
this.onPeerClosed({
peerId: this.id,
streamType: this.streamType,
});
this.logger(`peer closed ${this.id}`);
}
});
this.onPeerClosed = eventTarget.getEventDispatcher("onPeerClose");
this.id = Peer.getPeerIdFromConnection(connection);
this.peerProtocol = new PeerProtocol(connection, peerConfig, {
onSegmentChunkReceived: this.onSegmentChunkReceived,
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onCommandReceived: this.onCommandReceived,
}, eventTarget);
eventTarget.getEventDispatcher("onPeerConnect")({
peerId: this.id,
streamType,
});
connection.on("error", this.onConnectionError);
connection.on("close", this.onPeerConnectionClosed);
connection.on("end", this.onPeerConnectionClosed);
connection.on("finish", this.onPeerConnectionClosed);
}
get downloadingSegment() {
return this.downloadingContext?.request.segment;
}
getSegmentStatus(segment) {
const { externalId } = segment;
if (this.loadedSegments.has(externalId))
return "loaded";
if (this.httpLoadingSegments.has(externalId))
return "http-loading";
}
downloadSegment(segmentRequest) {
if (this.downloadingContext) {
throw new Error("Some segment already is downloading");
}
this.downloadingContext = {
request: segmentRequest,
requestId: Math.floor(Math.random() * 1000),
isSegmentDataCommandReceived: false,
controls: segmentRequest.start({ downloadSource: "p2p", peerId: this.id }, {
notReceivingBytesTimeoutMs: this.peerConfig.p2pNotReceivingBytesTimeoutMs,
abort: (error) => {
if (!this.downloadingContext)
return;
const { request, requestId } = this.downloadingContext;
this.sendCancelSegmentRequestCommand(request.segment, requestId);
this.downloadingErrors.push(error);
this.downloadingContext = undefined;
const timeoutErrors = this.downloadingErrors.filter((error) => error.type === "bytes-receiving-timeout");
if (timeoutErrors.length >= this.peerConfig.p2pErrorRetries) {
this.destroy();
}
},
}),
};
const command = {
c: PeerCommandType.SegmentRequest,
r: this.downloadingContext.requestId,
i: segmentRequest.segment.externalId,
};
if (segmentRequest.loadedBytes)
command.b = segmentRequest.loadedBytes;
this.peerProtocol.sendCommand(command);
}
async uploadSegmentData(segment, requestId, data) {
const { externalId } = segment;
this.logger(`send segment ${segment.externalId} to ${this.id}`);
const command = {
c: PeerCommandType.SegmentData,
i: externalId,
r: requestId,
s: data.byteLength,
};
this.peerProtocol.sendCommand(command);
try {
await this.peerProtocol.splitSegmentDataToChunksAndUploadAsync(data, requestId);
this.sendSegmentDataSendingCompletedCommand(segment, requestId);
this.logger(`segment ${externalId} has been sent to ${this.id}`);
}
catch {
this.logger(`cancel segment uploading ${externalId}`);
}
}
cancelSegmentDownloading(type) {
if (!this.downloadingContext)
return;
const { request, controls } = this.downloadingContext;
const { segment } = request;
this.logger(`cancel segment request ${segment.externalId} (${type})`);
const error = new RequestError(type);
controls.abortOnError(error);
this.downloadingContext = undefined;
this.downloadingErrors.push(error);
}
sendSegmentsAnnouncementCommand(loadedSegmentsIds, httpLoadingSegmentsIds) {
const command = {
c: PeerCommandType.SegmentsAnnouncement,
p: httpLoadingSegmentsIds,
l: loadedSegmentsIds,
};
this.peerProtocol.sendCommand(command);
}
sendSegmentAbsentCommand(segmentExternalId, requestId) {
this.peerProtocol.sendCommand({
c: PeerCommandType.SegmentAbsent,
i: segmentExternalId,
r: requestId,
});
}
sendCancelSegmentRequestCommand(segment, requestId) {
this.peerProtocol.sendCommand({
c: PeerCommandType.CancelSegmentRequest,
i: segment.externalId,
r: requestId,
});
}
sendSegmentDataSendingCompletedCommand(segment, requestId) {
this.peerProtocol.sendCommand({
c: PeerCommandType.SegmentDataSendingCompleted,
r: requestId,
i: segment.externalId,
});
}
static getPeerIdFromConnection(connection) {
return Utils.hexToUtf8(connection.id);
}
}
//# sourceMappingURL=peer.js.map