UNPKG

p2p-media-loader-core

Version:
571 lines 26.8 kB
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; import { HttpRequestExecutor } from "./http-loader.js"; import { P2PLoadersContainer } from "./p2p/loaders-container.js"; import { RequestsContainer } from "./requests/request-container.js"; import { EngineRequest } from "./requests/engine-request.js"; import * as QueueUtils from "./utils/queue.js"; import * as LoggerUtils from "./utils/logger.js"; import * as StreamUtils from "./utils/stream.js"; import * as Utils from "./utils/utils.js"; import debug from "debug"; const FAILED_ATTEMPTS_CLEAR_INTERVAL = 60000; const PEER_UPDATE_LATENCY = 1000; export class HybridLoader { constructor(streamManifestUrl, lastRequestedSegment, streamDetails, config, bandwidthCalculators, segmentStorage, webTorrentSocketPool, eventTarget, peerId) { var _a; Object.defineProperty(this, "streamManifestUrl", { enumerable: true, configurable: true, writable: true, value: streamManifestUrl }); Object.defineProperty(this, "lastRequestedSegment", { enumerable: true, configurable: true, writable: true, value: lastRequestedSegment }); Object.defineProperty(this, "streamDetails", { enumerable: true, configurable: true, writable: true, value: streamDetails }); Object.defineProperty(this, "config", { enumerable: true, configurable: true, writable: true, value: config }); Object.defineProperty(this, "bandwidthCalculators", { enumerable: true, configurable: true, writable: true, value: bandwidthCalculators }); Object.defineProperty(this, "segmentStorage", { enumerable: true, configurable: true, writable: true, value: segmentStorage }); Object.defineProperty(this, "webTorrentSocketPool", { enumerable: true, configurable: true, writable: true, value: webTorrentSocketPool }); Object.defineProperty(this, "eventTarget", { enumerable: true, configurable: true, writable: true, value: eventTarget }); Object.defineProperty(this, "peerId", { enumerable: true, configurable: true, writable: true, value: peerId }); Object.defineProperty(this, "requests", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "engineRequest", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "p2pLoaders", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "playback", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "segmentAvgDuration", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "logger", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "levelChangedTimestamp", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "lastQueueProcessingTimeStamp", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "randomHttpDownloadTimeout", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "initialHttpDelayTimeoutId", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "isProcessQueueMicrotaskCreated", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "swarmId", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "createdAt", { enumerable: true, configurable: true, writable: true, value: performance.now() }); Object.defineProperty(this, "requestProcessQueueMicrotask", { enumerable: true, configurable: true, writable: true, value: (force = true) => { const now = performance.now(); if ((!force && this.lastQueueProcessingTimeStamp !== undefined && now - this.lastQueueProcessingTimeStamp <= 1000) || this.isProcessQueueMicrotaskCreated) { return; } this.isProcessQueueMicrotaskCreated = true; Utils.queueMicrotask(() => { try { this.processQueue(); this.lastQueueProcessingTimeStamp = now; } finally { this.isProcessQueueMicrotaskCreated = false; } }); } }); const activeStream = this.lastRequestedSegment.stream; this.swarmId = (_a = this.config.swarmId) !== null && _a !== void 0 ? _a : this.streamManifestUrl; this.playback = { position: this.lastRequestedSegment.startTime, rate: 1 }; this.segmentAvgDuration = StreamUtils.getSegmentAvgDuration(activeStream); this.requests = new RequestsContainer(this.requestProcessQueueMicrotask, this.bandwidthCalculators, this.playback, this.config, this.eventTarget, this.swarmId); this.p2pLoaders = new P2PLoadersContainer(this.streamManifestUrl, this.lastRequestedSegment.stream, this.requests, this.segmentStorage, this.config, this.webTorrentSocketPool, this.eventTarget, this.peerId, this.requestProcessQueueMicrotask); this.logger = debug(`p2pml-core:hybrid-loader-${activeStream.type}`); this.logger.color = "coral"; this.setIntervalLoading(); } setIntervalLoading() { const peersCount = this.p2pLoaders.currentLoader.connectedPeerCount; const randomTimeout = Math.random() * PEER_UPDATE_LATENCY * peersCount + PEER_UPDATE_LATENCY; this.randomHttpDownloadTimeout = window.setTimeout(() => { this.loadRandomThroughHttp(); this.setIntervalLoading(); }, randomTimeout); } // api method for engines loadSegment(segment, callbacks) { return __awaiter(this, void 0, void 0, function* () { var _a; this.logger(`requests: ${LoggerUtils.getSegmentString(segment)}`); const { stream } = segment; if (stream !== this.lastRequestedSegment.stream) { this.logger(`stream changed to ${LoggerUtils.getStreamString(stream)}`); this.p2pLoaders.changeCurrentLoader(stream); } this.lastRequestedSegment = segment; const streamSwarmId = StreamUtils.getStreamSwarmId(this.swarmId, stream); this.segmentStorage.onSegmentRequested(this.swarmId, streamSwarmId, segment.externalId, segment.startTime, segment.endTime, stream.type, this.streamDetails.isLive); const engineRequest = new EngineRequest(segment, callbacks); try { const hasSegment = this.segmentStorage.hasSegment(this.swarmId, streamSwarmId, segment.externalId); if (hasSegment) { const data = yield this.segmentStorage.getSegmentData(this.swarmId, streamSwarmId, segment.externalId); if (data) { const { queueDownloadRatio } = this.generateQueue(); engineRequest.resolve(data, this.getBandwidth(queueDownloadRatio)); return; } } (_a = this.engineRequest) === null || _a === void 0 ? void 0 : _a.abort(); this.engineRequest = engineRequest; // If the engine explicitly requests a segment that previously failed during // background pre-fetching, clear its error history so it can be retried. const request = this.requests.get(segment); if ((request === null || request === void 0 ? void 0 : request.status) === "failed") { request.failedAttempts.clear(); } } catch (error) { this.logger(`request failed for ${LoggerUtils.getSegmentString(segment)} in ${LoggerUtils.getStreamString(stream)}`, error); engineRequest.reject(); } finally { this.requestProcessQueueMicrotask(); } }); } processRequests(queueSegmentIds, queueDownloadRatio) { var _a; const { stream } = this.lastRequestedSegment; const { httpErrorRetries } = this.config; const now = performance.now(); for (const request of this.requests.items()) { const { downloadSource: type, status, segment, isHandledByProcessQueue, } = request; const engineRequest = ((_a = this.engineRequest) === null || _a === void 0 ? void 0 : _a.segment) === segment ? this.engineRequest : undefined; switch (status) { case "loading": if (!queueSegmentIds.has(segment.runtimeId) && !engineRequest) { request.cancel(); this.requests.remove(request); } break; case "succeed": { if (!type) break; if (type === "http") { this.p2pLoaders.currentLoader.broadcastAnnouncement(); } if (engineRequest) { engineRequest.resolve(request.data, this.getBandwidth(queueDownloadRatio)); this.engineRequest = undefined; } this.requests.remove(request); this.logger(`succeed: ${LoggerUtils.getSegmentString(segment)} (byteLength: ${request.data.byteLength})`); const streamSwarmId = StreamUtils.getStreamSwarmId(this.swarmId, stream); void this.segmentStorage.storeSegment(this.swarmId, streamSwarmId, segment.externalId, request.data, segment.startTime, segment.endTime, segment.stream.type, this.streamDetails.isLive); break; } case "failed": if (type === "http" && !isHandledByProcessQueue) { this.p2pLoaders.currentLoader.broadcastAnnouncement(); } if (!engineRequest && !stream.segments.has(request.segment.runtimeId)) { this.requests.remove(request); } if (request.failedAttempts.httpAttemptsCount >= httpErrorRetries && engineRequest) { this.engineRequest = undefined; engineRequest.reject(); } break; case "not-started": this.requests.remove(request); break; case "aborted": this.requests.remove(request); break; } request.markHandledByProcessQueue(); const { lastAttempt } = request.failedAttempts; if (lastAttempt && now - lastAttempt.error.timestamp > FAILED_ATTEMPTS_CLEAR_INTERVAL) { request.failedAttempts.clear(); } } } processQueue() { var _a, _b, _c; const { queue, queueSegmentIds, queueDownloadRatio } = this.generateQueue(); this.processRequests(queueSegmentIds, queueDownloadRatio); const { simultaneousHttpDownloads, simultaneousP2PDownloads, httpErrorRetries, httpDownloadInitialTimeoutMs, } = this.config; const timeSinceStart = performance.now() - this.createdAt; const isInitialHttpWait = httpDownloadInitialTimeoutMs > 0 && timeSinceStart < httpDownloadInitialTimeoutMs; if (isInitialHttpWait) { (_a = this.initialHttpDelayTimeoutId) !== null && _a !== void 0 ? _a : (this.initialHttpDelayTimeoutId = window.setTimeout(() => { this.initialHttpDelayTimeoutId = undefined; this.requestProcessQueueMicrotask(); }, httpDownloadInitialTimeoutMs - timeSinceStart)); } const { engineRequest } = this; if (engineRequest) { const { segment } = engineRequest; const request = this.requests.get(segment); const shouldStartLoadImmediatelyEngineRequest = engineRequest.shouldBeStartedImmediately && engineRequest.status === "pending" && (!request || request.status === "not-started" || request.status === "failed" || request.status === "aborted"); if (shouldStartLoadImmediatelyEngineRequest) { // Don't abort requests when processing engine request // to avoid race condition with aborts in the requests queue const canLoadThroughHttp = !isInitialHttpWait && ((_b = request === null || request === void 0 ? void 0 : request.failedAttempts.httpAttemptsCount) !== null && _b !== void 0 ? _b : 0) < httpErrorRetries && this.requests.executingHttpCount < simultaneousHttpDownloads; if (canLoadThroughHttp) { this.loadThroughHttp(segment); } else { const canLoadThroughP2P = this.p2pLoaders.currentLoader.isSegmentLoadedBySomeone(segment) && this.requests.executingP2PCount < simultaneousP2PDownloads; if (canLoadThroughP2P) { this.loadThroughP2P(segment); } } } } for (const item of queue) { const { statuses, segment } = item; const request = this.requests.get(segment); if ((request === null || request === void 0 ? void 0 : request.status) === "succeed") continue; if (statuses.isHighDemand) { const canLoadThroughHttp = !isInitialHttpWait && ((_c = request === null || request === void 0 ? void 0 : request.failedAttempts.httpAttemptsCount) !== null && _c !== void 0 ? _c : 0) < httpErrorRetries; if ((request === null || request === void 0 ? void 0 : request.status) === "loading") { // High-demand request is loading const shouldSwitchFromP2PToHttp = canLoadThroughHttp && request.downloadSource === "p2p" && (this.requests.executingHttpCount < simultaneousHttpDownloads || this.abortLastHttpLoadingInQueueAfterItem(queue, segment)); if (shouldSwitchFromP2PToHttp) { request.cancel(); this.loadThroughHttp(segment); } continue; } // High-demand request is not loading const shouldLoadThroughHttp = canLoadThroughHttp && (this.requests.executingHttpCount < simultaneousHttpDownloads || this.abortLastHttpLoadingInQueueAfterItem(queue, segment)); if (shouldLoadThroughHttp) { this.loadThroughHttp(segment); continue; } const canLoadThroughP2P = this.p2pLoaders.currentLoader.isSegmentLoadedBySomeone(segment) && (this.requests.executingP2PCount < simultaneousP2PDownloads || this.abortLastP2PLoadingInQueueAfterItem(queue, segment)); if (canLoadThroughP2P) { this.loadThroughP2P(segment); } } else { // Regular requests load via P2P only (random HTTP loading also runs by interval) const canLoadThroughP2P = statuses.isP2PDownloadable && (request === null || request === void 0 ? void 0 : request.status) !== "loading" && this.requests.executingP2PCount < simultaneousP2PDownloads; if (canLoadThroughP2P) { this.loadThroughP2P(segment); } } } } // api method for engines abortSegmentRequest(segmentRuntimeId) { var _a; if (((_a = this.engineRequest) === null || _a === void 0 ? void 0 : _a.segment.runtimeId) !== segmentRuntimeId) return; this.engineRequest.abort(); this.logger("abort: ", LoggerUtils.getSegmentString(this.engineRequest.segment)); this.engineRequest = undefined; this.requestProcessQueueMicrotask(); } loadThroughHttp(segment) { const request = this.requests.getOrCreateRequest(segment); const executor = new HttpRequestExecutor(request, this.config, this.eventTarget); executor.execute(); this.p2pLoaders.currentLoader.broadcastAnnouncement(); } loadThroughP2P(segment) { this.p2pLoaders.currentLoader.downloadSegment(segment); } loadRandomThroughHttp() { const { httpDownloadInitialTimeoutMs } = this.config; const isInitialHttpWait = httpDownloadInitialTimeoutMs > 0 && performance.now() - this.createdAt < httpDownloadInitialTimeoutMs; if (isInitialHttpWait) return; const availableStorageCapacityPercent = this.getAvailableStorageCapacityPercent(); if (availableStorageCapacityPercent <= 10) return; const { simultaneousHttpDownloads, httpErrorRetries } = this.config; const p2pLoader = this.p2pLoaders.currentLoader; if (this.requests.executingHttpCount >= simultaneousHttpDownloads || !p2pLoader.connectedPeerCount) { return; } const segmentsToLoad = []; for (const { segment, statuses } of QueueUtils.generateQueue(this.lastRequestedSegment, this.playback, this.config, this.p2pLoaders.currentLoader, availableStorageCapacityPercent)) { const streamSwarmId = StreamUtils.getStreamSwarmId(this.swarmId, segment.stream); if (!statuses.isHttpDownloadable || statuses.isP2PDownloadable || this.segmentStorage.hasSegment(this.swarmId, streamSwarmId, segment.externalId)) { continue; } const request = this.requests.get(segment); if (request && (request.status === "loading" || request.status === "succeed" || request.failedAttempts.httpAttemptsCount >= httpErrorRetries)) { continue; } segmentsToLoad.push(segment); } if (!segmentsToLoad.length) return; const availableHttpDownloads = simultaneousHttpDownloads - this.requests.executingHttpCount; if (availableHttpDownloads === 0) return; const peersCount = p2pLoader.connectedPeerCount + 1; const safeRandomSegmentsCount = Math.min(segmentsToLoad.length, simultaneousHttpDownloads * peersCount); const randomIndices = Utils.shuffleArray(Array.from({ length: safeRandomSegmentsCount }, (_, i) => i)); let probability = safeRandomSegmentsCount / peersCount; for (const randomIndex of randomIndices) { if (this.requests.executingHttpCount >= simultaneousHttpDownloads) { break; } if (probability >= 1 || Math.random() <= probability) { const segment = segmentsToLoad[randomIndex]; this.loadThroughHttp(segment); } probability--; if (probability <= 0) break; } } abortLastHttpLoadingInQueueAfterItem(queue, segment) { for (const { segment: itemSegment } of Utils.arrayBackwards(queue)) { if (itemSegment === segment) break; const request = this.requests.get(itemSegment); if ((request === null || request === void 0 ? void 0 : request.downloadSource) === "http" && request.status === "loading") { request.cancel(); return true; } } return false; } abortLastP2PLoadingInQueueAfterItem(queue, segment) { for (const { segment: itemSegment } of Utils.arrayBackwards(queue)) { if (itemSegment === segment) break; const request = this.requests.get(itemSegment); if ((request === null || request === void 0 ? void 0 : request.downloadSource) === "p2p" && request.status === "loading") { request.cancel(); return true; } } return false; } getAvailableStorageCapacityPercent() { const { totalCapacity, usedCapacity } = this.segmentStorage.getUsage(); return 100 - (usedCapacity / totalCapacity) * 100; } generateQueue() { var _a; const queue = []; const queueSegmentIds = new Set(); let maxPossibleLength = 0; let alreadyLoadedCount = 0; const availableStorageCapacityPercent = this.getAvailableStorageCapacityPercent(); for (const item of QueueUtils.generateQueue(this.lastRequestedSegment, this.playback, this.config, this.p2pLoaders.currentLoader, availableStorageCapacityPercent)) { maxPossibleLength++; const { segment } = item; const streamSwarmId = StreamUtils.getStreamSwarmId(this.swarmId, segment.stream); if (this.segmentStorage.hasSegment(this.swarmId, streamSwarmId, segment.externalId) || ((_a = this.requests.get(segment)) === null || _a === void 0 ? void 0 : _a.status) === "succeed") { alreadyLoadedCount++; continue; } queue.push(item); queueSegmentIds.add(segment.runtimeId); } return { queue, queueSegmentIds, maxPossibleLength, alreadyLoadedCount, queueDownloadRatio: maxPossibleLength !== 0 ? alreadyLoadedCount / maxPossibleLength : 0, }; } getBandwidth(queueDownloadRatio) { const { http, all } = this.bandwidthCalculators; const { activeLevelBitrate } = this.streamDetails; if (activeLevelBitrate === 0) { return all.getBandwidthLoadingOnly(3); } const bandwidth = Math.max(all.getBandwidth(30, this.levelChangedTimestamp), all.getBandwidth(60, this.levelChangedTimestamp), all.getBandwidth(90, this.levelChangedTimestamp)); if (queueDownloadRatio >= 0.8 || bandwidth >= activeLevelBitrate * 0.9) { return Math.max(all.getBandwidthLoadingOnly(1), all.getBandwidthLoadingOnly(3), all.getBandwidthLoadingOnly(5)); } const httpRealBandwidth = Math.max(http.getBandwidthLoadingOnly(1), http.getBandwidthLoadingOnly(3), http.getBandwidthLoadingOnly(5)); return Math.max(bandwidth, httpRealBandwidth); } notifyLevelChanged() { this.levelChangedTimestamp = performance.now(); } sendBroadcastAnnouncement(sendEmptySegmentsAnnouncement = false) { this.p2pLoaders.currentLoader.broadcastAnnouncement(sendEmptySegmentsAnnouncement); } updatePlayback(position, rate) { var _a; const isRateChanged = this.playback.rate !== rate; const isPositionChanged = this.playback.position !== position; if (!isRateChanged && !isPositionChanged) return; const isPositionSignificantlyChanged = Math.abs(position - this.playback.position) / this.segmentAvgDuration > 0.5; if (isPositionChanged) this.playback.position = position; if (isRateChanged && rate !== 0) this.playback.rate = rate; if (isPositionSignificantlyChanged) { this.logger("position significantly changed"); (_a = this.engineRequest) === null || _a === void 0 ? void 0 : _a.markAsShouldBeStartedImmediately(); } this.segmentStorage.onPlaybackUpdated(position, rate); this.requestProcessQueueMicrotask(isPositionSignificantlyChanged); } updateStream(stream) { if (stream !== this.lastRequestedSegment.stream) return; this.logger(`update stream: ${LoggerUtils.getStreamString(stream)}`); this.requestProcessQueueMicrotask(); } destroy() { var _a; clearTimeout(this.randomHttpDownloadTimeout); clearTimeout(this.initialHttpDelayTimeoutId); (_a = this.engineRequest) === null || _a === void 0 ? void 0 : _a.abort(); this.requests.destroy(); this.p2pLoaders.destroy(); } } //# sourceMappingURL=hybrid-loader.js.map