UNPKG

p2p-media-loader-core

Version:
361 lines 15.2 kB
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