UNPKG

p2p-media-loader-core

Version:
1,441 lines 171 kB
//#region \0rolldown/runtime.js var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports); var __exportAll = (all, no_symbols) => { let target = {}; for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" }); return target; }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion //#region src/types.ts /** * Base class for domain-specific errors carrying a machine-readable type discriminator. * @internal */ var TypedError = class extends Error { type; cause; constructor(type, message, cause) { super(message); this.type = type; this.cause = cause; } }; /** Represents an error that occurred during a peer connection. */ var PeerError = class extends TypedError { name = "PeerError"; }; /** Represents a warning that occurred during a peer connection. */ var PeerWarning = class extends TypedError { name = "PeerWarning"; }; /** Represents an error that occurred during a tracker request. */ var TrackerError = class extends TypedError { name = "TrackerError"; }; /** Represents a warning that occurred during a tracker request. */ var TrackerWarning = class extends TypedError { name = "TrackerWarning"; }; /** Represents an error that occurred while establishing a peer connection. */ var PeerConnectError = class extends TypedError { name = "PeerConnectError"; }; /** * Represents an error that can occur during the request process, with a timestamp for when the error occurred. * @template T - The specific type of request error. */ var RequestError = class extends TypedError { name = "RequestError"; /** Error timestamp. */ timestamp; /** * Constructs a new RequestError. * @param type - The specific error type. * @param message - Optional message describing the error. * @param cause - Optional underlying cause of the error. */ constructor(type, message, cause) { super(type, message, cause); this.timestamp = performance.now(); } }; /** Custom error class for errors that occur during core network requests. */ var CoreRequestError = class extends TypedError { name = "CoreRequestError"; }; //#endregion //#region src/utils/abort-controller.ts var AbortSignalPolyfill = class { aborted = false; #listeners = /* @__PURE__ */ new Set(); addEventListener(_type, listener) { this.#listeners.add(listener); } removeEventListener(_type, listener) { this.#listeners.delete(listener); } dispatchEvent(_type) { this.aborted = true; for (const listener of this.#listeners) try { listener(); } catch {} this.#listeners.clear(); } }; var AbortControllerPolyfill = class { signal = new AbortSignalPolyfill(); abort() { this.signal.dispatchEvent("abort"); } }; var isAbortControllerSupported = typeof AbortController !== "undefined"; var SafeAbortController = isAbortControllerSupported ? AbortController : AbortControllerPolyfill; //#endregion //#region src/http-loader.ts var HttpRequestExecutor = class { request; httpConfig; abortController = new SafeAbortController(); expectedBytesLength; requestByteRange; onChunkDownloaded; isAborted() { return this.abortController.signal.aborted; } constructor(request, httpConfig, eventTarget) { this.request = request; this.httpConfig = httpConfig; this.onChunkDownloaded = eventTarget.getEventDispatcher("onChunkDownloaded"); const { byteRange } = this.request.segment; if (byteRange) this.requestByteRange = { ...byteRange }; } execute() { const startControls = { onAbort: () => this.abortController.abort(), notReceivingBytesTimeoutMs: this.httpConfig.httpNotReceivingBytesTimeoutMs }; if (this.request.tryCompleteByLoadedBytes({ downloadSource: "http" }, startControls, this.httpConfig.validateHTTPSegment, "http-segment-validation-failed")) return; if (this.request.loadedBytes !== 0) { this.requestByteRange = this.requestByteRange ?? { start: 0 }; this.requestByteRange.start = this.requestByteRange.start + this.request.loadedBytes; } if (this.request.totalBytes) this.expectedBytesLength = this.request.totalBytes - this.request.loadedBytes; const requestControls = this.request.start({ downloadSource: "http" }, startControls); this.fetch(requestControls); } async fetch(requestControls) { const { segment } = this.request; let activeReader; if (this.isAborted()) return; const onAbort = () => { try { activeReader?.cancel().catch(() => {}); } catch {} }; this.abortController.signal.addEventListener("abort", onAbort); const abortSignal = isAbortControllerSupported ? this.abortController.signal : void 0; try { let request = await this.httpConfig.httpRequestSetup?.(segment.url, segment.byteRange, abortSignal, this.requestByteRange); if (this.isAborted()) throw new DOMException("Request aborted", "AbortError"); if (!request) { const headers = new Headers(); if (this.requestByteRange) headers.set("Range", `bytes=${this.requestByteRange.start}-${this.requestByteRange.end ?? ""}`); const requestOptions = { headers }; if (abortSignal) requestOptions.signal = abortSignal; request = new Request(segment.url, requestOptions); } if (this.isAborted()) throw new DOMException("Request aborted before request fetch", "AbortError"); const response = await window.fetch(request); if (this.isAborted()) throw new DOMException("Request aborted", "AbortError"); this.handleResponseHeaders(response); requestControls.firstBytesReceived(); if (!response.body || typeof response.body.getReader !== "function") { const arrayBuffer = await response.arrayBuffer(); if (this.isAborted()) throw new DOMException("Request aborted", "AbortError"); const value = new Uint8Array(arrayBuffer); requestControls.addLoadedChunk(value); this.onChunkDownloaded(value.byteLength, "http", void 0, segment.stream.type, this.request.infoHash); } else { const reader = response.body.getReader(); activeReader = reader; for (;;) { if (this.isAborted()) throw new DOMException("Request aborted", "AbortError"); const { done, value } = await reader.read(); if (done) break; if (this.isAborted()) throw new DOMException("Request aborted", "AbortError"); requestControls.addLoadedChunk(value); this.onChunkDownloaded(value.byteLength, "http", void 0, segment.stream.type, this.request.infoHash); } } if (this.isAborted()) throw new DOMException("Request aborted", "AbortError"); if (this.request.totalBytes !== void 0 && this.request.loadedBytes !== this.request.totalBytes) throw new RequestError("http-bytes-mismatch", `HTTP response truncated: received ${this.request.loadedBytes} of ${this.request.totalBytes} bytes`); const isValid = await this.request.validateData(this.httpConfig.validateHTTPSegment); if (this.isAborted()) throw new DOMException("Request aborted", "AbortError"); if (!isValid) { this.request.clearLoadedBytes(); throw new RequestError("http-segment-validation-failed"); } requestControls.completeOnSuccess(); } catch (error) { this.handleError(error, requestControls); } finally { this.abortController.signal.removeEventListener("abort", onAbort); } } handleResponseHeaders(response) { if (!response.ok) if (response.status === 406 || response.status === 416) { this.request.clearLoadedBytes(); throw new RequestError("http-bytes-mismatch", response.statusText); } else throw new RequestError("http-error", response.statusText); const { requestByteRange } = this; if (requestByteRange) if (response.status === 200) if (this.request.segment.byteRange) throw new RequestError("http-unexpected-status-code"); else this.request.clearLoadedBytes(); else { if (response.status !== 206) throw new RequestError("http-unexpected-status-code", response.statusText); const contentLengthHeader = response.headers.get("Content-Length"); if (contentLengthHeader && this.expectedBytesLength !== void 0 && this.expectedBytesLength !== +contentLengthHeader) { this.request.clearLoadedBytes(); throw new RequestError("http-bytes-mismatch", response.statusText); } const contentRangeHeader = response.headers.get("Content-Range"); const contentRange = contentRangeHeader ? parseContentRangeHeader(contentRangeHeader) : void 0; if (contentRange) { const { from, to } = contentRange; const responseExpectedBytesLength = to !== void 0 && from !== void 0 ? to - from + 1 : void 0; if (responseExpectedBytesLength !== void 0 && this.expectedBytesLength !== responseExpectedBytesLength || from !== void 0 && requestByteRange.start !== from || to !== void 0 && requestByteRange.end !== void 0 && requestByteRange.end !== to) { this.request.clearLoadedBytes(); throw new RequestError("http-bytes-mismatch", response.statusText); } } } if (response.status === 200 && this.request.totalBytes === void 0) { const contentLengthHeader = response.headers.get("Content-Length"); if (contentLengthHeader) this.request.setTotalBytes(+contentLengthHeader); } } handleError(error, requestControls) { if (this.isAborted()) return; if (error instanceof Error) { const httpLoaderError = error instanceof RequestError ? error : new RequestError("http-error", error.message); requestControls.failWithError(httpLoaderError); } } }; var rangeHeaderRegex = /^bytes (?:(?:(\d+)|)-(?:(\d+)|)|\*)\/(?:(\d+)|\*)$/; function parseContentRangeHeader(headerValue) { const match = rangeHeaderRegex.exec(headerValue.trim()); if (!match) return; const [, from, to, total] = match; return { from: from ? parseInt(from) : void 0, to: to ? parseInt(to) : void 0, total: total ? parseInt(total) : void 0 }; } //#endregion //#region ../../node_modules/.pnpm/ms@2.1.3/node_modules/ms/index.js var require_ms = /* @__PURE__ */ __commonJSMin(((exports, module) => { /** * Helpers. */ var s = 1e3; var m = s * 60; var h = m * 60; var d = h * 24; var w = d * 7; var y = d * 365.25; /** * Parse or format the given `val`. * * Options: * * - `long` verbose formatting [false] * * @param {String|Number} val * @param {Object} [options] * @throws {Error} throw an error if val is not a non-empty string or a number * @return {String|Number} * @api public */ module.exports = function(val, options) { options = options || {}; var type = typeof val; if (type === "string" && val.length > 0) return parse(val); else if (type === "number" && isFinite(val)) return options.long ? fmtLong(val) : fmtShort(val); throw new Error("val is not a non-empty string or a valid number. val=" + JSON.stringify(val)); }; /** * Parse the given `str` and return milliseconds. * * @param {String} str * @return {Number} * @api private */ function parse(str) { str = String(str); if (str.length > 100) return; var match = /^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(str); if (!match) return; var n = parseFloat(match[1]); switch ((match[2] || "ms").toLowerCase()) { case "years": case "year": case "yrs": case "yr": case "y": return n * y; case "weeks": case "week": case "w": return n * w; case "days": case "day": case "d": return n * d; case "hours": case "hour": case "hrs": case "hr": case "h": return n * h; case "minutes": case "minute": case "mins": case "min": case "m": return n * m; case "seconds": case "second": case "secs": case "sec": case "s": return n * s; case "milliseconds": case "millisecond": case "msecs": case "msec": case "ms": return n; default: return; } } /** * Short format for `ms`. * * @param {Number} ms * @return {String} * @api private */ function fmtShort(ms) { var msAbs = Math.abs(ms); if (msAbs >= d) return Math.round(ms / d) + "d"; if (msAbs >= h) return Math.round(ms / h) + "h"; if (msAbs >= m) return Math.round(ms / m) + "m"; if (msAbs >= s) return Math.round(ms / s) + "s"; return ms + "ms"; } /** * Long format for `ms`. * * @param {Number} ms * @return {String} * @api private */ function fmtLong(ms) { var msAbs = Math.abs(ms); if (msAbs >= d) return plural(ms, msAbs, d, "day"); if (msAbs >= h) return plural(ms, msAbs, h, "hour"); if (msAbs >= m) return plural(ms, msAbs, m, "minute"); if (msAbs >= s) return plural(ms, msAbs, s, "second"); return ms + " ms"; } /** * Pluralization helper. */ function plural(ms, msAbs, n, name) { var isPlural = msAbs >= n * 1.5; return Math.round(ms / n) + " " + name + (isPlural ? "s" : ""); } })); //#endregion //#region ../../node_modules/.pnpm/debug@4.4.3/node_modules/debug/src/common.js var require_common = /* @__PURE__ */ __commonJSMin(((exports, module) => { /** * This is the common logic for both the Node.js and web browser * implementations of `debug()`. */ function setup(env) { createDebug.debug = createDebug; createDebug.default = createDebug; createDebug.coerce = coerce; createDebug.disable = disable; createDebug.enable = enable; createDebug.enabled = enabled; createDebug.humanize = require_ms(); createDebug.destroy = destroy; Object.keys(env).forEach((key) => { createDebug[key] = env[key]; }); /** * The currently active debug mode names, and names to skip. */ createDebug.names = []; createDebug.skips = []; /** * Map of special "%n" handling functions, for the debug "format" argument. * * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". */ createDebug.formatters = {}; /** * Selects a color for a debug namespace * @param {String} namespace The namespace string for the debug instance to be colored * @return {Number|String} An ANSI color code for the given namespace * @api private */ function selectColor(namespace) { let hash = 0; for (let i = 0; i < namespace.length; i++) { hash = (hash << 5) - hash + namespace.charCodeAt(i); hash |= 0; } return createDebug.colors[Math.abs(hash) % createDebug.colors.length]; } createDebug.selectColor = selectColor; /** * Create a debugger with the given `namespace`. * * @param {String} namespace * @return {Function} * @api public */ function createDebug(namespace) { let prevTime; let enableOverride = null; let namespacesCache; let enabledCache; function debug(...args) { if (!debug.enabled) return; const self = debug; const curr = Number(/* @__PURE__ */ new Date()); self.diff = curr - (prevTime || curr); self.prev = prevTime; self.curr = curr; prevTime = curr; args[0] = createDebug.coerce(args[0]); if (typeof args[0] !== "string") args.unshift("%O"); let index = 0; args[0] = args[0].replace(/%([a-zA-Z%])/g, (match, format) => { if (match === "%%") return "%"; index++; const formatter = createDebug.formatters[format]; if (typeof formatter === "function") { const val = args[index]; match = formatter.call(self, val); args.splice(index, 1); index--; } return match; }); createDebug.formatArgs.call(self, args); (self.log || createDebug.log).apply(self, args); } debug.namespace = namespace; debug.useColors = createDebug.useColors(); debug.color = createDebug.selectColor(namespace); debug.extend = extend; debug.destroy = createDebug.destroy; Object.defineProperty(debug, "enabled", { enumerable: true, configurable: false, get: () => { if (enableOverride !== null) return enableOverride; if (namespacesCache !== createDebug.namespaces) { namespacesCache = createDebug.namespaces; enabledCache = createDebug.enabled(namespace); } return enabledCache; }, set: (v) => { enableOverride = v; } }); if (typeof createDebug.init === "function") createDebug.init(debug); return debug; } function extend(namespace, delimiter) { const newDebug = createDebug(this.namespace + (typeof delimiter === "undefined" ? ":" : delimiter) + namespace); newDebug.log = this.log; return newDebug; } /** * Enables a debug mode by namespaces. This can include modes * separated by a colon and wildcards. * * @param {String} namespaces * @api public */ function enable(namespaces) { createDebug.save(namespaces); createDebug.namespaces = namespaces; createDebug.names = []; createDebug.skips = []; const split = (typeof namespaces === "string" ? namespaces : "").trim().replace(/\s+/g, ",").split(",").filter(Boolean); for (const ns of split) if (ns[0] === "-") createDebug.skips.push(ns.slice(1)); else createDebug.names.push(ns); } /** * Checks if the given string matches a namespace template, honoring * asterisks as wildcards. * * @param {String} search * @param {String} template * @return {Boolean} */ function matchesTemplate(search, template) { let searchIndex = 0; let templateIndex = 0; let starIndex = -1; let matchIndex = 0; while (searchIndex < search.length) if (templateIndex < template.length && (template[templateIndex] === search[searchIndex] || template[templateIndex] === "*")) if (template[templateIndex] === "*") { starIndex = templateIndex; matchIndex = searchIndex; templateIndex++; } else { searchIndex++; templateIndex++; } else if (starIndex !== -1) { templateIndex = starIndex + 1; matchIndex++; searchIndex = matchIndex; } else return false; while (templateIndex < template.length && template[templateIndex] === "*") templateIndex++; return templateIndex === template.length; } /** * Disable debug output. * * @return {String} namespaces * @api public */ function disable() { const namespaces = [...createDebug.names, ...createDebug.skips.map((namespace) => "-" + namespace)].join(","); createDebug.enable(""); return namespaces; } /** * Returns true if the given mode name is enabled, false otherwise. * * @param {String} name * @return {Boolean} * @api public */ function enabled(name) { for (const skip of createDebug.skips) if (matchesTemplate(name, skip)) return false; for (const ns of createDebug.names) if (matchesTemplate(name, ns)) return true; return false; } /** * Coerce `val`. * * @param {Mixed} val * @return {Mixed} * @api private */ function coerce(val) { if (val instanceof Error) return val.stack || val.message; return val; } /** * XXX DO NOT USE. This is a temporary stub function. * XXX It WILL be removed in the next major release. */ function destroy() { console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."); } createDebug.enable(createDebug.load()); return createDebug; } module.exports = setup; })); //#endregion //#region src/p2p/commands/types.ts var import_browser = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => { /** * This is the web browser implementation of `debug()`. */ exports.formatArgs = formatArgs; exports.save = save; exports.load = load; exports.useColors = useColors; exports.storage = localstorage(); exports.destroy = (() => { let warned = false; return () => { if (!warned) { warned = true; console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."); } }; })(); /** * Colors. */ exports.colors = [ "#0000CC", "#0000FF", "#0033CC", "#0033FF", "#0066CC", "#0066FF", "#0099CC", "#0099FF", "#00CC00", "#00CC33", "#00CC66", "#00CC99", "#00CCCC", "#00CCFF", "#3300CC", "#3300FF", "#3333CC", "#3333FF", "#3366CC", "#3366FF", "#3399CC", "#3399FF", "#33CC00", "#33CC33", "#33CC66", "#33CC99", "#33CCCC", "#33CCFF", "#6600CC", "#6600FF", "#6633CC", "#6633FF", "#66CC00", "#66CC33", "#9900CC", "#9900FF", "#9933CC", "#9933FF", "#99CC00", "#99CC33", "#CC0000", "#CC0033", "#CC0066", "#CC0099", "#CC00CC", "#CC00FF", "#CC3300", "#CC3333", "#CC3366", "#CC3399", "#CC33CC", "#CC33FF", "#CC6600", "#CC6633", "#CC9900", "#CC9933", "#CCCC00", "#CCCC33", "#FF0000", "#FF0033", "#FF0066", "#FF0099", "#FF00CC", "#FF00FF", "#FF3300", "#FF3333", "#FF3366", "#FF3399", "#FF33CC", "#FF33FF", "#FF6600", "#FF6633", "#FF9900", "#FF9933", "#FFCC00", "#FFCC33" ]; /** * Currently only WebKit-based Web Inspectors, Firefox >= v31, * and the Firebug extension (any Firefox version) are known * to support "%c" CSS customizations. * * TODO: add a `localStorage` variable to explicitly enable/disable colors */ function useColors() { if (typeof window !== "undefined" && window.process && (window.process.type === "renderer" || window.process.__nwjs)) return true; if (typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) return false; let m; return typeof document !== "undefined" && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance || typeof window !== "undefined" && window.console && (window.console.firebug || window.console.exception && window.console.table) || typeof navigator !== "undefined" && navigator.userAgent && (m = navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)) && parseInt(m[1], 10) >= 31 || typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/); } /** * Colorize log arguments if enabled. * * @api public */ function formatArgs(args) { args[0] = (this.useColors ? "%c" : "") + this.namespace + (this.useColors ? " %c" : " ") + args[0] + (this.useColors ? "%c " : " ") + "+" + module.exports.humanize(this.diff); if (!this.useColors) return; const c = "color: " + this.color; args.splice(1, 0, c, "color: inherit"); let index = 0; let lastC = 0; args[0].replace(/%[a-zA-Z%]/g, (match) => { if (match === "%%") return; index++; if (match === "%c") lastC = index; }); args.splice(lastC, 0, c); } /** * Invokes `console.debug()` when available. * No-op when `console.debug` is not a "function". * If `console.debug` is not available, falls back * to `console.log`. * * @api public */ exports.log = console.debug || console.log || (() => {}); /** * Save `namespaces`. * * @param {String} namespaces * @api private */ function save(namespaces) { try { if (namespaces) exports.storage.setItem("debug", namespaces); else exports.storage.removeItem("debug"); } catch (error) {} } /** * Load `namespaces`. * * @return {String} returns the previously persisted debug modes * @api private */ function load() { let r; try { r = exports.storage.getItem("debug") || exports.storage.getItem("DEBUG"); } catch (error) {} if (!r && typeof process !== "undefined" && "env" in process) r = process.env.DEBUG; return r; } /** * Localstorage attempts to return the localstorage. * * This is necessary because safari throws * when a user disables cookies/localstorage * and you attempt to access it. * * @return {LocalStorage} * @api private */ function localstorage() { try { return localStorage; } catch (error) {} } module.exports = require_common()(exports); var { formatters } = module.exports; /** * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. */ formatters.j = function(v) { try { return JSON.stringify(v); } catch (error) { return "[UnexpectedJSONParseError]: " + error.message; } }; })))(), 1); var PeerCommandType$1 = /* @__PURE__ */ function(PeerCommandType) { PeerCommandType[PeerCommandType["SegmentsAnnouncement"] = 0] = "SegmentsAnnouncement"; PeerCommandType[PeerCommandType["SegmentRequest"] = 1] = "SegmentRequest"; PeerCommandType[PeerCommandType["SegmentData"] = 2] = "SegmentData"; PeerCommandType[PeerCommandType["SegmentDataSendingCompleted"] = 3] = "SegmentDataSendingCompleted"; PeerCommandType[PeerCommandType["SegmentAbsent"] = 4] = "SegmentAbsent"; PeerCommandType[PeerCommandType["CancelSegmentRequest"] = 5] = "CancelSegmentRequest"; return PeerCommandType; }({}); //#endregion //#region src/utils/utils.ts function getPromiseWithResolvers() { let resolve; let reject; return { promise: new Promise((res, rej) => { resolve = res; reject = rej; }), resolve, reject }; } function queueMicrotask(fn) { Promise.resolve().then(fn); } function joinChunks(chunks, totalBytes) { totalBytes ??= chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); const buffer = new Uint8Array(totalBytes); let offset = 0; for (const chunk of chunks) { buffer.set(chunk, offset); offset += chunk.byteLength; } return buffer; } function getRandomItem(items) { return items[Math.floor(Math.random() * items.length)]; } function getWeightedRandomItem(items, weightAccessor) { if (items.length === 0) throw new Error("Cannot get item from empty array"); if (items.length === 1) return items[0]; let totalWeight = 0; const weights = items.map((item) => { const weight = weightAccessor(item); totalWeight += weight; return weight; }); let randomWeight = Math.random() * totalWeight; for (let i = 0; i < items.length; i++) { randomWeight -= weights[i]; if (randomWeight <= 0) return items[i]; } return items[items.length - 1]; } function utf8ToUintArray(utf8String) { return new TextEncoder().encode(utf8String); } function* arrayBackwards(arr) { for (let i = arr.length - 1; i >= 0; i--) yield arr[i]; } function isObject(item) { return !!item && typeof item === "object" && !Array.isArray(item); } function isArray(item) { return Array.isArray(item); } function filterUndefinedProps(obj) { function filter(obj) { if (isObject(obj)) { const result = {}; Object.keys(obj).forEach((key) => { if (obj[key] !== void 0) { const value = filter(obj[key]); if (value !== void 0) result[key] = value; } }); return result; } else return obj; } return filter(obj); } function deepCopy(item) { if (isArray(item)) return item.map((element) => deepCopy(element)); else if (isObject(item)) { const copy = {}; for (const key of Object.keys(item)) copy[key] = deepCopy(item[key]); return copy; } else return item; } function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } function overrideConfig(target, updates, defaults = {}) { if (typeof target !== "object" || target === null || typeof updates !== "object" || updates === null) return target; Object.keys(updates).forEach((key) => { const keyStr = typeof key === "symbol" ? key.toString() : String(key); if (key === "__proto__" || key === "constructor" || key === "prototype") throw new Error(`Attempt to modify restricted property '${keyStr}'`); const updateValue = updates[key]; const defaultValue = defaults[key]; if (key in target) if (updateValue === void 0) target[key] = defaultValue === void 0 ? void 0 : defaultValue; else target[key] = updateValue; }); return target; } function mergeAndFilterConfig(options) { const { defaultConfig, baseConfig = {}, specificStreamConfig = {} } = options; const mergedConfig = deepCopy({ ...defaultConfig, ...baseConfig, ...specificStreamConfig }); const keysOfT = Object.keys(defaultConfig); const filteredConfig = {}; keysOfT.forEach((key) => { if (key in mergedConfig) filteredConfig[key] = mergedConfig[key]; }); return filteredConfig; } //#endregion //#region src/p2p/commands/binary-serialization.ts var textEncoder = new TextEncoder(); var textDecoder = new TextDecoder("utf8"); var SerializedItem = /* @__PURE__ */ function(SerializedItem) { SerializedItem[SerializedItem["Min"] = -1] = "Min"; SerializedItem[SerializedItem["Int"] = 0] = "Int"; SerializedItem[SerializedItem["SimilarIntArray"] = 1] = "SimilarIntArray"; SerializedItem[SerializedItem["String"] = 2] = "String"; SerializedItem[SerializedItem["Max"] = 3] = "Max"; return SerializedItem; }({}); function getRequiredBytesForInt(num) { if (num === 0) return 1; const necessaryBits = Math.floor(Math.log2(Math.abs(num))) + 2; return Math.ceil(necessaryBits / 8); } function intToBytes(num) { const isNegative = num < 0; const bytesAmountNumber = getRequiredBytesForInt(num); const bytes = new Uint8Array(bytesAmountNumber); num = Math.abs(num); for (let i = 0; i < bytesAmountNumber; i++) { const shift = 8 * (bytesAmountNumber - 1 - i); bytes[i] = Math.floor(num / Math.pow(2, shift)) & 255; } if (isNegative) bytes[0] = bytes[0] | 128; return bytes; } function bytesToInt(bytes) { const byteLength = bytes.length; const getNumberPart = (byte, i) => { const shift = 8 * (byteLength - 1 - i); return byte * Math.pow(2, shift); }; let number = getNumberPart(bytes[0] & 127, 0); for (let i = 1; i < byteLength; i++) number += getNumberPart(bytes[i], i); if ((bytes[0] & 128) >> 7 !== 0) number = -number; return number; } function serializeInt(num) { const numBytes = intToBytes(num); const numberMetadata = 0 | numBytes.length; return new Uint8Array([numberMetadata, ...numBytes]); } function deserializeInt(bytes) { if (bytes.length === 0) throw new Error("Buffer is too short"); const metadata = bytes[0]; if (metadata >> 4 !== 0) throw new Error("Trying to deserialize integer with invalid serialized item code"); const numberBytesLength = metadata & 15; if (numberBytesLength === 0) throw new Error("Invalid integer: zero byte length"); if (numberBytesLength > 7) throw new Error("Invalid integer: byte length exceeds safe integer limit"); const start = 1; const end = start + numberBytesLength; if (bytes.length < end) throw new Error("Buffer is too short"); return { number: bytesToInt(bytes.subarray(start, end)), byteLength: numberBytesLength + 1 }; } function serializeUniqueSimilarIntArray(numbers) { const commonPartNumbersMap = /* @__PURE__ */ new Map(); for (const number of numbers) { const diffByte = number & 255; const common = number - diffByte; const bytes = commonPartNumbersMap.get(common) ?? new ResizableUint8Array(); if (!bytes.length) commonPartNumbersMap.set(common, bytes); bytes.push(diffByte); } const result = new ResizableUint8Array(); result.push([16, commonPartNumbersMap.size]); for (const [commonPart, binaryArray] of commonPartNumbersMap) { const { length } = binaryArray; const commonPartWithLength = commonPart + (length & 255); binaryArray.unshift(serializeInt(commonPartWithLength)); result.push(binaryArray.getBuffer()); } return result.getBuffer(); } function deserializeUniqueSimilarIntArray(bytes) { if (bytes.length < 2) throw new Error("Buffer is too short"); const [codeByte, commonPartArraysAmount] = bytes; if (codeByte >> 4 !== 1) throw new Error("Trying to deserialize similar int array with invalid serialized item code"); let offset = 2; const originalIntArr = []; for (let i = 0; i < commonPartArraysAmount; i++) { const { number: commonPartWithLength, byteLength } = deserializeInt(bytes.subarray(offset)); offset += byteLength; const arrayLength = commonPartWithLength & 255; const actualLength = arrayLength === 0 ? 256 : arrayLength; const commonPart = commonPartWithLength - arrayLength; if (offset + actualLength > bytes.length) throw new Error("Malformed similar int array: buffer too short"); for (let j = 0; j < actualLength; j++) { const diffPart = bytes[offset]; originalIntArr.push(commonPart + diffPart); offset++; } } return { numbers: originalIntArr, byteLength: offset }; } function serializeString(string) { const encoded = textEncoder.encode(string); const { length } = encoded; if (length > 4095) throw new Error("String exceeds maximum length of 4095 bytes"); const bytes = new ResizableUint8Array(); bytes.push([32 | length >> 8 & 15, length & 255]); bytes.push(encoded); return bytes.getBuffer(); } function deserializeString(bytes) { if (bytes.length < 2) throw new Error("Buffer is too short"); const [codeByte, lengthByte] = bytes; if (codeByte >> 4 !== 2) throw new Error("Trying to deserialize bytes (sting) with invalid serialized item code."); const length = (codeByte & 15) << 8 | lengthByte; if (bytes.length < length + 2) throw new Error("Malformed string: buffer too short"); const stringBytes = bytes.subarray(2, length + 2); return { string: textDecoder.decode(stringBytes), byteLength: length + 2 }; } var ResizableUint8Array = class { #bytes = []; #length = 0; push(bytes) { this.#addBytes(bytes, "end"); } unshift(bytes) { this.#addBytes(bytes, "start"); } #addBytes(bytes, position) { let bytesToAdd; if (bytes instanceof Uint8Array) bytesToAdd = bytes; else if (Array.isArray(bytes)) bytesToAdd = new Uint8Array(bytes); else bytesToAdd = new Uint8Array([bytes]); this.#length += bytesToAdd.length; this.#bytes[position === "start" ? "unshift" : "push"](bytesToAdd); } getBytesChunks() { return this.#bytes; } getBuffer() { return joinChunks(this.#bytes, this.#length); } get length() { return this.#length; } }; //#endregion //#region src/p2p/commands/binary-command-creator.ts var FRAME_PART_LENGTH = 4; var commandFrameStart = stringToUtf8CodesBuffer("cstr", FRAME_PART_LENGTH); var commandFrameEnd = stringToUtf8CodesBuffer("cend", FRAME_PART_LENGTH); var commandDivFrameStart = stringToUtf8CodesBuffer("dstr", FRAME_PART_LENGTH); var commandDivFrameEnd = stringToUtf8CodesBuffer("dend", FRAME_PART_LENGTH); var startFrames = [commandFrameStart, commandDivFrameStart]; var endFrames = [commandFrameEnd, commandDivFrameEnd]; var commandFramesLength = commandFrameStart.length + commandFrameEnd.length; function isCommandChunk(buffer) { if (buffer.length < commandFramesLength) return false; const { length } = commandFrameStart; const bufferEndingToCompare = buffer.subarray(-length); return startFrames.some((frame) => areBuffersEqual(buffer, frame, FRAME_PART_LENGTH)) && endFrames.some((frame) => areBuffersEqual(bufferEndingToCompare, frame, FRAME_PART_LENGTH)); } function isFirstCommandChunk(buffer) { if (buffer.length < commandFramesLength) return false; return areBuffersEqual(buffer, commandFrameStart, FRAME_PART_LENGTH); } function isLastCommandChunk(buffer) { if (buffer.length < commandFramesLength) return false; return areBuffersEqual(buffer.subarray(-4), commandFrameEnd, FRAME_PART_LENGTH); } var BinaryCommandJoiningError = class extends Error { type; constructor(type) { super(); this.type = type; } }; var BinaryCommandChunksJoiner = class { #chunks = new ResizableUint8Array(); #status = "joining"; #onComplete; constructor(onComplete) { this.#onComplete = onComplete; } addCommandChunk(chunk) { if (this.#status === "completed") return; const isFirstChunk = isFirstCommandChunk(chunk); if (!this.#chunks.length && !isFirstChunk) throw new BinaryCommandJoiningError("no-first-chunk"); if (this.#chunks.length && isFirstChunk) throw new BinaryCommandJoiningError("incomplete-joining"); this.#chunks.push(this.#unframeCommandChunk(chunk)); if (!isLastCommandChunk(chunk)) return; this.#status = "completed"; this.#onComplete(this.#chunks.getBuffer()); } #unframeCommandChunk(chunk) { if (chunk.length < commandFramesLength) throw new Error("Command chunk is too short to unframe"); return chunk.subarray(FRAME_PART_LENGTH, chunk.length - FRAME_PART_LENGTH); } }; var BinaryCommandCreator = class { #bytes = new ResizableUint8Array(); #resultBuffers = []; #status = "creating"; #maxChunkLength; constructor(commandType, maxChunkLength) { this.#maxChunkLength = maxChunkLength; this.#bytes.push(commandType); } addInteger(name, value) { this.#bytes.push(name.charCodeAt(0)); const bytes = serializeInt(value); this.#bytes.push(bytes); } addUniqueSimilarIntArr(name, arr) { this.#bytes.push(name.charCodeAt(0)); const bytes = serializeUniqueSimilarIntArray(arr); this.#bytes.push(bytes); } addString(name, string) { this.#bytes.push(name.charCodeAt(0)); const bytes = serializeString(string); this.#bytes.push(bytes); } complete() { if (!this.#bytes.length) throw new Error("Buffer is empty"); if (this.#status === "completed") return; this.#status = "completed"; const unframedBuffer = this.#bytes.getBuffer(); if (unframedBuffer.length + commandFramesLength <= this.#maxChunkLength) { this.#resultBuffers.push(frameBuffer(unframedBuffer, commandFrameStart, commandFrameEnd)); return; } let chunksCount = Math.ceil(unframedBuffer.length / this.#maxChunkLength); if (Math.ceil(unframedBuffer.length / chunksCount) + commandFramesLength > this.#maxChunkLength) chunksCount++; for (const [i, chunk] of splitBufferToEqualChunks(unframedBuffer, chunksCount)) if (i === 0) this.#resultBuffers.push(frameBuffer(chunk, commandFrameStart, commandDivFrameEnd)); else if (i === chunksCount - 1) this.#resultBuffers.push(frameBuffer(chunk, commandDivFrameStart, commandFrameEnd)); else this.#resultBuffers.push(frameBuffer(chunk, commandDivFrameStart, commandDivFrameEnd)); } getResultBuffers() { if (this.#status === "creating" || !this.#resultBuffers.length) throw new Error("Command is not complete."); return this.#resultBuffers; } }; function deserializeCommand(bytes) { const [commandCode] = bytes; const deserializedCommand = { c: commandCode }; let offset = 1; while (offset < bytes.length) { if (offset + 1 >= bytes.length) throw new Error("Malformed command buffer: truncated name/type header"); const name = String.fromCharCode(bytes[offset]); offset++; switch (getDataTypeFromByte(bytes[offset])) { case SerializedItem.Int: { const { number, byteLength } = deserializeInt(bytes.subarray(offset)); deserializedCommand[name] = number; offset += byteLength; } break; case SerializedItem.SimilarIntArray: { const { numbers, byteLength } = deserializeUniqueSimilarIntArray(bytes.subarray(offset)); deserializedCommand[name] = numbers; offset += byteLength; } break; case SerializedItem.String: { const { string, byteLength } = deserializeString(bytes.subarray(offset)); deserializedCommand[name] = string; offset += byteLength; } break; } } return validateCommand(deserializedCommand); } function getDataTypeFromByte(byte) { const typeCode = byte >> 4; if (typeCode <= SerializedItem.Min || typeCode >= SerializedItem.Max) throw new Error("Not existing type"); return typeCode; } function stringToUtf8CodesBuffer(string, length) { if (length && string.length !== length) throw new Error("Wrong string length"); const buffer = new Uint8Array(length ?? string.length); for (let i = 0; i < string.length; i++) buffer[i] = string.charCodeAt(i); return buffer; } function* splitBufferToEqualChunks(buffer, chunksCount) { const chunkLength = Math.ceil(buffer.length / chunksCount); for (let i = 0; i < chunksCount; i++) yield [i, buffer.subarray(i * chunkLength, (i + 1) * chunkLength)]; } function frameBuffer(buffer, frameStart, frameEnd) { const result = new Uint8Array(buffer.length + frameStart.length + frameEnd.length); result.set(frameStart); result.set(buffer, frameStart.length); result.set(frameEnd, frameStart.length + buffer.length); return result; } function areBuffersEqual(buffer1, buffer2, length) { for (let i = 0; i < length; i++) if (buffer1[i] !== buffer2[i]) return false; return true; } function validateCommand(command) { switch (command.c) { case PeerCommandType$1.SegmentsAnnouncement: return command; case PeerCommandType$1.SegmentRequest: assertNumberFields(command, "i", "r"); return command; case PeerCommandType$1.SegmentData: assertNumberFields(command, "i", "r", "s"); return command; case PeerCommandType$1.SegmentAbsent: case PeerCommandType$1.CancelSegmentRequest: case PeerCommandType$1.SegmentDataSendingCompleted: assertNumberFields(command, "i", "r"); return command; default: throw new Error(`Unknown peer command type: ${String(command.c)}`); } } function assertNumberFields(obj, ...fields) { for (const field of fields) if (typeof obj[field] !== "number") throw new Error(`Expected number field "${field}", got ${typeof obj[field]}`); } //#endregion //#region src/p2p/commands/commands.ts function serializeSegmentAnnouncementCommand(command, maxChunkSize) { const { c: commandCode, p: loadingByHttp, l: loaded } = command; const creator = new BinaryCommandCreator(commandCode, maxChunkSize); if (loaded?.length) creator.addUniqueSimilarIntArr("l", loaded); if (loadingByHttp?.length) creator.addUniqueSimilarIntArr("p", loadingByHttp); creator.complete(); return creator.getResultBuffers(); } function serializePeerSegmentCommand(command, maxChunkSize) { const creator = new BinaryCommandCreator(command.c, maxChunkSize); creator.addInteger("i", command.i); creator.addInteger("r", command.r); creator.complete(); return creator.getResultBuffers(); } function serializePeerSendSegmentCommand(command, maxChunkSize) { const creator = new BinaryCommandCreator(command.c, maxChunkSize); creator.addInteger("i", command.i); creator.addInteger("s", command.s); creator.addInteger("r", command.r); creator.complete(); return creator.getResultBuffers(); } function serializePeerSegmentRequestCommand(command, maxChunkSize) { const creator = new BinaryCommandCreator(command.c, maxChunkSize); creator.addInteger("i", command.i); creator.addInteger("r", command.r); if (command.b) creator.addInteger("b", command.b); creator.complete(); return creator.getResultBuffers(); } function serializePeerCommand(command, maxChunkSize) { switch (command.c) { case PeerCommandType$1.CancelSegmentRequest: case PeerCommandType$1.SegmentAbsent: case PeerCommandType$1.SegmentDataSendingCompleted: return serializePeerSegmentCommand(command, maxChunkSize); case PeerCommandType$1.SegmentRequest: return serializePeerSegmentRequestCommand(command, maxChunkSize); case PeerCommandType$1.SegmentsAnnouncement: return serializeSegmentAnnouncementCommand(command, maxChunkSize); case PeerCommandType$1.SegmentData: return serializePeerSendSegmentCommand(command, maxChunkSize); } } //#endregion //#region src/p2p/commands/index.ts var commands_exports = /* @__PURE__ */ __exportAll({ BinaryCommandChunksJoiner: () => BinaryCommandChunksJoiner, BinaryCommandJoiningError: () => BinaryCommandJoiningError, PeerCommandType: () => PeerCommandType$1, deserializeCommand: () => deserializeCommand, isCommandChunk: () => isCommandChunk, serializePeerCommand: () => serializePeerCommand }); //#endregion //#region src/webtorrent/utils.ts function getRTCError(event, fallbackMessage = "RTC error") { const errorEvent = event; if (errorEvent.error instanceof Error) return errorEvent.error; const msg = errorEvent.error?.message ?? fallbackMessage; return new Error(msg); } function getRTCErrorMessage(event, fallbackMessage = "RTC error") { return getRTCError(event, fallbackMessage).message; } function isTerminalConnectionState(state) { return state === "failed" || state === "closed" || state === "disconnected"; } //#endregion //#region src/webtorrent/data-channel-sender.ts var MAX_BUFFERED_AMOUNT = 64 * 1024; var DataChannelSender = class { channel; maxMessageSize; #currentSendContext; constructor(channel, maxMessageSize) { this.channel = channel; this.maxMessageSize = maxMessageSize; } async sendData(data, onChunkSent) { if (this.#currentSendContext) throw new Error("Already sending data"); if (this.channel.readyState !== "open") throw new Error("Data channel is not open"); this.channel.bufferedAmountLowThreshold = MAX_BUFFERED_AMOUNT; const { promise, resolve, reject } = getPromiseWithResolvers(); let offset = 0; let isSettled = false; const cleanup = () => { if (isSettled) return false; isSettled = true; this.#currentSendContext = void 0; this.channel.removeEventListener("bufferedamountlow", sendChunks); this.channel.removeEventListener("closing", onClose); this.channel.removeEventListener("close", onClose); this.channel.removeEventListener("error", onError); return true; }; this.#currentSendContext = { cancel: () => { if (cleanup()) reject(/* @__PURE__ */ new Error("Send cancelled")); } }; const onClose = () => { if (cleanup()) reject(/* @__PURE__ */ new Error("Data channel closed")); }; const onError = (event) => { if (!cleanup()) return; const message = getRTCErrorMessage(event, "Unknown error"); reject(/* @__PURE__ */ new Error(`Data channel error: ${message}`)); }; const buffer = ArrayBuffer.isView(data) ? data.buffer : data; const byteOffset = ArrayBuffer.isView(data) ? data.byteOffset : 0; const sendChunks = () => { if (isSettled) return; if (this.channel.readyState !== "open") { if (cleanup()) reject(/* @__PURE__ */ new Error(`Data channel not open (state: ${this.channel.readyState})`)); return; } try { while (offset < data.byteLength) { if (this.channel.bufferedAmount > MAX_BUFFERED_AMOUNT) return; const bytesToSend = Math.min(this.maxMessageSize, data.byteLength - offset); const chunk = new Uint8Array(buffer, byteOffset + offset, bytesToSend); this.channel.send(chunk); offset += bytesToSend; onChunkSent?.(bytesToSend); if (!this.#currentSendContext) return; } } catch (error) { if (cleanup()) reject(error instanceof Error ? error : new Error(String(error))); return; } if (cleanup()) resolve(); }; this.channel.addEventListener("bufferedamountlow", sendChunks); this.channel.addEventListener("closing", onClose); this.channel.addEventListener("close", onClose); this.channel.addEventListener("error", onError); sendChunks(); return promise; } cancel() { this.#currentSendContext?.cancel(); } }; //#endregion //#region src/p2p/peer-protocol.ts var logger = (0, import_browser.default)("p2pml-core:peer-protocol"); var PeerProtocol = class { #commandChunks; #dataChannelSender; #uploadingRequestId; #onChunkDownloaded; #onChunkUploaded; #channel; #peerConfig; #eventHandlers; #peerId; constructor(channel, peerConfig, eventHandlers, eventTarget, peerId) { this.#channel = channel; this.#peerConfig = peerConfig; this.#eventHandlers = eventHandlers; this.#peerId = peerId; this.#dataChannelSender = new DataChannelSender(channel, peerConfig.webRtcMaxMessageSize); this.#onChunkDownloaded = eventTarget.getEventDispatcher("onChunkDownloaded"); this.#onChunkUploaded = eventTarget.getEventDispatcher("onChunkUploaded"); if (channel.binaryType !== "arraybuffer") throw new Error(`Expected binaryType "arraybuffer", got "${channel.binaryType}"`); channel.addEventListener("message", this.#onMessageReceived); } #onMessageReceived = (event) => { try { const data = new Uint8Array(event.data); if (isCommandChunk(data)) this.#receivingCommandBytes(data); else { this.#eventHandlers.onSegmentChunkReceived(data); this.#onChunkDownloaded(data.byteLength, "p2p", this.#peerId, this.#peerConfig.streamType, this.#peerConfig.infoHash); } } catch (err) { logger("error handling data channel message: %O", err); this.#eventHandlers.onProtocolError(err); } }; sendCommand(command) { if (this.#channel.readyState !== "open") throw new Error(`cannot send command ${command.c} (channel state: ${this.#channel.readyState})`); const binaryCommandBuffers = serializePeerCommand(command, this.#peerConfig.webRtcMaxMessageSize); for (const buffer of binaryCommandBuffers) this.#channel.send(buffer); } stopUploadingSegmentData() { this.#dataChannelSender.cancel(); this.#uploadingRequestId = void 0; } getUploadingRequestId() { return this.#uploadingRequestId; } async splitSegmentDataToChunksAndUploadAsync(data, requestId) { if (this.#uploadingRequestId !== void 0) throw new Error(`Some segment data is already uploading.`); this.#uploadingRequestId = requestId; try { await this.#dataChannelSender.sendData(data, (chunkSize) => { this.#onChunkUploaded(chunkSize, this.#peerId, this.#peerConfig.streamType, this.#peerConfig.infoHash); }); } finally { if (this.#uploadingRequestId === requestId) this.#uploadingRequestId = void 0; } } #receivingCommandBytes(buffer) { this.#commandChunks ??= new BinaryCommandChunksJoiner((commandBuffer) => { this.#commandChunks = void 0; let command; try { command = deserializeCommand(commandBuffer); } catch (err) { logger("error deserializing command: %O", err); this.#eventHandlers.onProtocolError(err); return; } this.#eventHandlers.onCommandReceived(command); }); try { this.#comm