diff --git a/p2p-media-loader-demo/package.json b/p2p-media-loader-demo/package.json index c54ef890..94839827 100644 --- a/p2p-media-loader-demo/package.json +++ b/p2p-media-loader-demo/package.json @@ -27,6 +27,7 @@ "@types/react-dom": "^18.2.4", "@vitejs/plugin-react": "^4.0.0", "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.1" + "eslint-plugin-react-refresh": "^0.4.1", + "vite-plugin-node-polyfills": "^0.14.1" } } diff --git a/p2p-media-loader-demo/src/App.tsx b/p2p-media-loader-demo/src/App.tsx index da462e5d..f6b1434c 100644 --- a/p2p-media-loader-demo/src/App.tsx +++ b/p2p-media-loader-demo/src/App.tsx @@ -1,10 +1,11 @@ -import { useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { Engine as HlsJsEngine } from "p2p-media-loader-hlsjs"; import { Engine as ShakaEngine } from "p2p-media-loader-shaka"; import Hls from "hls.js"; import DPlayer from "dplayer"; import shakaLib from "shaka-player"; import muxjs from "mux.js"; +import debug from "debug"; window.muxjs = muxjs; @@ -42,6 +43,9 @@ const streamUrl = { mss: "https://playready.directtaps.net/smoothstreaming/SSWSS720H264/SuperSpeedway_720.ism/Manifest", audioOnly: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/a1/prog_index.m3u8", + dash1: + "http://dash.akamaized.net/dash264/TestCases/1a/qualcomm/1/MultiRate.mpd", + dash2: "http://dash.akamaized.net/dash264/TestCases/5b/nomor/6.mpd", }; function App() { @@ -49,12 +53,46 @@ function App() { localStorage.player ); const [url, setUrl] = useState(localStorage.streamUrl); - const shakaEngine = useRef(new ShakaEngine(shakaLib)); - const hlsEngine = useRef(new HlsJsEngine()); const shakaInstance = useRef(); const hlsInstance = useRef(); const containerRef = useRef(null); const videoRef = useRef(null); + const [httpLoaded, setHttpLoaded] = useState(0); + const [p2pLoaded, setP2PLoaded] = useState(0); + const [httpLoadedGlob, setHttpLoadedGlob] = useLocalStorageItem( + "httpLoaded", + 0, + (v) => v.toString(), + (v) => (v !== null ? +v : 0) + ); + const [p2pLoadedGlob, setP2PLoadedGlob] = useLocalStorageItem( + "p2pLoaded", + 0, + (v) => v.toString(), + (v) => (v !== null ? +v : 0) + ); + + const hlsEngine = useRef(); + const shakaEngine = useRef(); + + const onSegmentLoaded = (byteLength: number, type: "http" | "p2p") => { + const MBytes = getMBFromBytes(byteLength); + if (type === "http") { + setHttpLoaded((prev) => round(prev + MBytes)); + setHttpLoadedGlob((prev) => round(prev + MBytes)); + } else if (type === "p2p") { + setP2PLoaded((prev) => round(prev + MBytes)); + setP2PLoadedGlob((prev) => round(prev + MBytes)); + } + }; + + if (!hlsEngine.current) { + hlsEngine.current = new HlsJsEngine({ onSegmentLoaded }); + } + + if (!shakaEngine.current) { + shakaEngine.current = new ShakaEngine(shakaLib, { onSegmentLoaded }); + } useEffect(() => { if ( @@ -79,8 +117,9 @@ function App() { (window as unknown as ExtendedWindow).videoPlayer = player; }; - const initShakaDplayer = (url: string) => { - const engine = shakaEngine.current; + const initShakaDPlayer = (url: string) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const engine = shakaEngine.current!; const player = new DPlayer({ container: containerRef.current, video: { @@ -88,6 +127,7 @@ function App() { type: "customHlsOrDash", customType: { customHlsOrDash: (video: HTMLVideoElement) => { + video.autoplay = true; const src = video.src; const shakaPlayer = new shakaLib.Player(video); const onError = (error: { code: number }) => { @@ -110,7 +150,8 @@ function App() { const initShakaPlayer = (url: string) => { if (!videoRef.current) return; - const engine = shakaEngine.current; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const engine = shakaEngine.current!; const player = new shakaLib.Player(videoRef.current); const onError = (error: { code: unknown }) => { @@ -128,7 +169,8 @@ function App() { const initHlsJsPlayer = (url: string) => { if (!videoRef.current) return; - const engine = hlsEngine.current; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const engine = hlsEngine.current!; const hls = new Hls({ ...engine.getConfig(), }); @@ -139,8 +181,9 @@ function App() { setPlayerToWindow(hls); }; - const initHlsDplayer = (url: string) => { - const engine = hlsEngine.current; + const initHlsDPlayer = (url: string) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const engine = hlsEngine.current!; const player = new DPlayer({ container: containerRef.current, video: { @@ -150,6 +193,7 @@ function App() { customHls: (video: HTMLVideoElement) => { const hls = new Hls({ ...engine.getConfig(), + liveSyncDurationCount: 7, }); engine.initHlsJsEvents(hls); hls.loadSource(video.src); @@ -159,6 +203,7 @@ function App() { }, }, }); + player.play(); setPlayerToWindow(player); }; @@ -180,19 +225,17 @@ function App() { }; const createNewPlayer = () => { - if (!localStorage.videoUrl) { - localStorage.streamUrl = streamUrl.live2; - setUrl(streamUrl.live2); - } + setHttpLoadedGlob(0); + setP2PLoadedGlob(0); switch (playerType) { case "hls-dplayer": - initHlsDplayer(url); + initHlsDPlayer(url); break; case "hlsjs": initHlsJsPlayer(url); break; case "shaka-dplayer": - initShakaDplayer(url); + initShakaDPlayer(url); break; case "shaka-player": initShakaPlayer(url); @@ -214,54 +257,199 @@ function App() { }; return ( -
-
-

This is Demo

-
- - - - +
+
+
+

This is Demo

+
+ + + + +
+
+
+
+ {!!playerType && ["hlsjs", "shaka-player"].includes(playerType) && ( +
-
-
+
+
+ + +
+
+ +
- {!!playerType && ["hlsjs", "shaka-player"].includes(playerType) && ( -
); } export default App; + +function LoadStat({ + http, + p2p, + title, +}: { + http: number; + p2p: number; + title: string; +}) { + const sum = http + p2p; + return ( +
+

{title}

+
+ Http loaded: {http.toFixed(2)} MB; {getPercent(http, sum)}% +
+
+ P2P loaded: {p2p.toFixed(2)} MB; {getPercent(p2p, sum)}% +
+
+ ); +} + +function LoggersSelect() { + const [activeLoggers, setActiveLoggers] = useLocalStorageItem( + "debug", + [], + (list) => { + setTimeout(() => debug.enable(localStorage.debug), 0); + if (list.length === 0) return null; + return list.join(","); + }, + (storageItem) => { + setTimeout(() => debug.enable(localStorage.debug), 0); + if (!storageItem) return []; + return storageItem.split(","); + } + ); + + const onChange = (event: React.ChangeEvent) => { + setActiveLoggers( + Array.from(event.target.selectedOptions, (option) => option.value) + ); + }; + + return ( +
+

Loggers:

+ +
+ ); +} + +function getPercent(a: number, b: number) { + if (a === 0 && b === 0) return "0"; + if (b === 0) return "100"; + return ((a / b) * 100).toFixed(2); +} + +function round(value: number, digitsAfterComma = 2) { + return Math.round(value * Math.pow(10, digitsAfterComma)) / 100; +} + +function getMBFromBytes(bytes: number) { + return round(bytes / Math.pow(1024, 2)); +} + +function useLocalStorageItem( + prop: string, + initValue: T, + valueToStorageItem: (value: T) => string | null, + storageItemToValue: (storageItem: string | null) => T +): [T, React.Dispatch>] { + const [value, setValue] = useState( + storageItemToValue(localStorage[prop]) ?? initValue + ); + const setValueExternal = useCallback((value: T | ((prev: T) => T)) => { + setValue(value); + if (typeof value === "function") { + const prev = storageItemToValue(localStorage.getItem(prop)); + const next = (value as (prev: T) => T)(prev); + const result = valueToStorageItem(next); + if (result !== null) localStorage.setItem(prop, result); + else localStorage.removeItem(prop); + } else { + const result = valueToStorageItem(value); + if (result !== null) localStorage.setItem(prop, result); + else localStorage.removeItem(prop); + } + }, []); + + useEffect(() => { + const eventHandler = (event: StorageEvent) => { + if (event.key !== prop) return; + const value = event.newValue; + setValue(storageItemToValue(value)); + }; + window.addEventListener("storage", eventHandler); + return () => { + window.removeEventListener("storage", eventHandler); + }; + }, []); + + return [value, setValueExternal]; +} + +const loggers = [ + "core:hybrid-loader-main", + "core:hybrid-loader-main-engine", + "core:hybrid-loader-secondary", + "core:hybrid-loader-secondary-engine", + "core:p2p-loader", + "core:peer", + "core:p2p-loaders-container", + "core:requests-container-main", + "core:requests-container-secondary", + "core:segment-memory-storage", +] as const; diff --git a/p2p-media-loader-demo/src/vite-env.d.ts b/p2p-media-loader-demo/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/p2p-media-loader-demo/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/p2p-media-loader-demo/tsconfig.json b/p2p-media-loader-demo/tsconfig.json index f0bb2433..f1d172fa 100644 --- a/p2p-media-loader-demo/tsconfig.json +++ b/p2p-media-loader-demo/tsconfig.json @@ -1,11 +1,11 @@ { "compilerOptions": { - "target": "ESNext", - "lib": ["DOM", "DOM.Iterable", "ESNext"], + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, - "moduleResolution": "node", - "allowImportingTsExtensions": false, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, @@ -15,6 +15,7 @@ "noUnusedParameters": false, "noFallthroughCasesInSwitch": true, "allowSyntheticDefaultImports": true, + "useDefineForClassFields": true }, "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] diff --git a/p2p-media-loader-demo/tsconfig.node.json b/p2p-media-loader-demo/tsconfig.node.json index cfa1ab5b..3adda81a 100644 --- a/p2p-media-loader-demo/tsconfig.node.json +++ b/p2p-media-loader-demo/tsconfig.node.json @@ -3,7 +3,7 @@ "composite": true, "skipLibCheck": true, "module": "ESNext", - "moduleResolution": "node", + "moduleResolution": "Bundler", "allowSyntheticDefaultImports": true }, "include": ["vite.config.ts"] diff --git a/p2p-media-loader-demo/vite.config.ts b/p2p-media-loader-demo/vite.config.ts index 081c8d9f..d00bebd0 100644 --- a/p2p-media-loader-demo/vite.config.ts +++ b/p2p-media-loader-demo/vite.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; +import { nodePolyfills } from "vite-plugin-node-polyfills"; export default defineConfig({ - plugins: [react()], + plugins: [nodePolyfills(), react()], }); diff --git a/package.json b/package.json index d03156ac..e9d0c288 100644 --- a/package.json +++ b/package.json @@ -26,5 +26,10 @@ }, "dependencies": { "debug": "^4.3.4" + }, + "pnpm": { + "patchedDependencies": { + "bittorrent-tracker@10.0.12": "patches/bittorrent-tracker@10.0.12.patch" + } } } diff --git a/packages/p2p-media-loader-core/package.json b/packages/p2p-media-loader-core/package.json index 4b5d7367..b0619783 100644 --- a/packages/p2p-media-loader-core/package.json +++ b/packages/p2p-media-loader-core/package.json @@ -13,7 +13,6 @@ "types": "lib/index.d.ts" }, "sideEffects": false, - "private": false, "type": "module", "scripts": { "dev": "vite", @@ -26,5 +25,12 @@ "lint": "eslint . --ext .ts", "clean": "rimraf lib dist build", "type-check": "npx tsc --noEmit" + }, + "dependencies": { + "bittorrent-tracker": "10.0.12", + "ripemd160": "^2.0.2" + }, + "devDependencies": { + "@types/ripemd160": "^2.0.2" } } diff --git a/packages/p2p-media-loader-core/src/bandwidth-approximator.ts b/packages/p2p-media-loader-core/src/bandwidth-approximator.ts index 7e7381d6..79216e53 100644 --- a/packages/p2p-media-loader-core/src/bandwidth-approximator.ts +++ b/packages/p2p-media-loader-core/src/bandwidth-approximator.ts @@ -1,58 +1,53 @@ -const SMOOTH_INTERVAL = 15 * 1000; -const MEASURE_INTERVAL = 60 * 1000; - -type NumberWithTime = { - readonly value: number; - readonly timeStamp: number; -}; +import { LoadProgress } from "./request-container"; export class BandwidthApproximator { - private lastBytes: NumberWithTime[] = []; - private currentBytesSum = 0; - private lastBandwidth: NumberWithTime[] = []; - - addBytes(bytes: number): void { - const timeStamp = performance.now(); - this.lastBytes.push({ value: bytes, timeStamp }); - this.currentBytesSum += bytes; - - while (timeStamp - this.lastBytes[0].timeStamp > SMOOTH_INTERVAL) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.currentBytesSum -= this.lastBytes.shift()!.value; - } + private readonly loadings: LoadProgress[] = []; - const interval = Math.min(SMOOTH_INTERVAL, timeStamp); - this.lastBandwidth.push({ - value: (this.currentBytesSum * 8000) / interval, - timeStamp, - }); + addLoading(progress: LoadProgress) { + this.clearStale(); + this.loadings.push(progress); } - // in bits per seconds + // in bits per second getBandwidth(): number { - const timeStamp = performance.now(); - while ( - this.lastBandwidth.length !== 0 && - timeStamp - this.lastBandwidth[0].timeStamp > MEASURE_INTERVAL - ) { - this.lastBandwidth.shift(); - } + this.clearStale(); + return getBandwidthByProgressList(this.loadings); + } - let maxBandwidth = 0; - for (const bandwidth of this.lastBandwidth) { - if (bandwidth.value > maxBandwidth) { - maxBandwidth = bandwidth.value; - } + private clearStale() { + const now = performance.now(); + for (const { startTimestamp } of this.loadings) { + if (now - startTimestamp <= 15000) break; + this.loadings.shift(); } - - return maxBandwidth; } +} - getSmoothInterval(): number { - return SMOOTH_INTERVAL; - } +function getBandwidthByProgressList(loadings: LoadProgress[]) { + if (!loadings.length) return 0; + let margin: number | undefined; + let totalLoadingTime = 0; + let totalBytes = 0; + const now = performance.now(); + + for (const { + startTimestamp: from, + lastLoadedChunkTimestamp: to = now, + loadedBytes, + } of loadings) { + totalBytes += loadedBytes; + + if (margin === undefined || from > margin) { + margin = to; + totalLoadingTime += to - from; + continue; + } - getMeasureInterval(): number { - return MEASURE_INTERVAL; + if (from <= margin && to > margin) { + totalLoadingTime += to - margin; + margin = to; + } } + + return (totalBytes * 8000) / totalLoadingTime; } diff --git a/packages/p2p-media-loader-core/src/core.ts b/packages/p2p-media-loader-core/src/core.ts index d975984a..69700d8b 100644 --- a/packages/p2p-media-loader-core/src/core.ts +++ b/packages/p2p-media-loader-core/src/core.ts @@ -3,38 +3,47 @@ import { Stream, StreamWithSegments, Segment, - SegmentResponse, Settings, + SegmentBase, + CoreEventHandlers, } from "./types"; -import * as Utils from "./utils"; +import * as Utils from "./utils/utils"; import { LinkedMap } from "./linked-map"; import { BandwidthApproximator } from "./bandwidth-approximator"; +import { EngineCallbacks } from "./request-container"; +import { SegmentsMemoryStorage } from "./segments-storage"; export class Core { private manifestResponseUrl?: string; private readonly streams = new Map>(); private readonly settings: Settings = { - simultaneousHttpDownloads: 3, - highDemandBufferLength: 25, - httpBufferLength: 60, - p2pBufferLength: 60, - cachedSegmentExpiration: 120, + simultaneousHttpDownloads: 2, + simultaneousP2PDownloads: 3, + highDemandTimeWindow: 15, + httpDownloadTimeWindow: 45, + p2pDownloadTimeWindow: 45, + cachedSegmentExpiration: 120 * 1000, cachedSegmentsCount: 50, + webRtcMaxMessageSize: 64 * 1024 - 1, + p2pSegmentDownloadTimeout: 5000, + p2pLoaderDestroyTimeout: 30 * 1000, }; private readonly bandwidthApproximator = new BandwidthApproximator(); - private readonly mainStreamLoader = new HybridLoader( - this.settings, - this.bandwidthApproximator - ); + private segmentStorage?: SegmentsMemoryStorage; + private mainStreamLoader?: HybridLoader; private secondaryStreamLoader?: HybridLoader; + constructor(private readonly eventHandlers?: CoreEventHandlers) {} + setManifestResponseUrl(url: string): void { this.manifestResponseUrl = url.split("?")[0]; } hasSegment(segmentLocalId: string): boolean { - const { segment } = - Utils.getSegmentFromStreamsMap(this.streams, segmentLocalId) ?? {}; + const segment = Utils.getSegmentFromStreamsMap( + this.streams, + segmentLocalId + ); return !!segment; } @@ -52,59 +61,94 @@ export class Core { updateStream( streamLocalId: string, - addSegments?: Segment[], + addSegments?: SegmentBase[], removeSegmentIds?: string[] ): void { const stream = this.streams.get(streamLocalId); if (!stream) return; - addSegments?.forEach((s) => stream.segments.addToEnd(s.localId, s)); + addSegments?.forEach((s) => { + const segment = { ...s, stream }; + stream.segments.addToEnd(segment.localId, segment); + }); removeSegmentIds?.forEach((id) => stream.segments.delete(id)); + this.mainStreamLoader?.updateStream(stream); + this.secondaryStreamLoader?.updateStream(stream); } - loadSegment(segmentLocalId: string): Promise { - const { segment, stream } = this.identifySegment(segmentLocalId); - - let loader: HybridLoader; - if (stream.type === "main") { - loader = this.mainStreamLoader; - } else { - this.secondaryStreamLoader = - this.secondaryStreamLoader ?? - new HybridLoader(this.settings, this.bandwidthApproximator); - loader = this.secondaryStreamLoader; + async loadSegment(segmentLocalId: string, callbacks: EngineCallbacks) { + if (!this.manifestResponseUrl) { + throw new Error("Manifest response url is not defined"); } - return loader.loadSegment(segment, stream); + if (!this.segmentStorage) { + this.segmentStorage = new SegmentsMemoryStorage( + this.manifestResponseUrl, + this.settings + ); + await this.segmentStorage.initialize(); + } + const segment = this.identifySegment(segmentLocalId); + const loader = this.getStreamHybridLoader(segment); + void loader.loadSegment(segment, callbacks); } abortSegmentLoading(segmentId: string): void { - this.mainStreamLoader.abortSegment(segmentId); - this.secondaryStreamLoader?.abortSegment(segmentId); + const segment = this.identifySegment(segmentId); + const streamType = segment.stream.type; + if (streamType === "main") this.mainStreamLoader?.abortSegment(segment); + else this.secondaryStreamLoader?.abortSegment(segment); } updatePlayback(position: number, rate: number): void { - this.mainStreamLoader.updatePlayback(position, rate); + this.mainStreamLoader?.updatePlayback(position, rate); this.secondaryStreamLoader?.updatePlayback(position, rate); } destroy(): void { this.streams.clear(); - this.mainStreamLoader.destroy(); + this.mainStreamLoader?.destroy(); this.secondaryStreamLoader?.destroy(); this.manifestResponseUrl = undefined; } - private identifySegment(segmentId: string) { + private identifySegment(segmentId: string): Segment { if (!this.manifestResponseUrl) { throw new Error("Manifest response url is undefined"); } - const { stream, segment } = - Utils.getSegmentFromStreamsMap(this.streams, segmentId) ?? {}; - if (!segment || !stream) { + const segment = Utils.getSegmentFromStreamsMap(this.streams, segmentId); + if (!segment) { throw new Error(`Not found segment with id: ${segmentId}`); } - return { segment, stream }; + return segment; + } + + private getStreamHybridLoader(segment: Segment) { + if (!this.manifestResponseUrl) { + throw new Error("Manifest response url is not defined"); + } + const createNewHybridLoader = (manifestResponseUrl: string) => { + if (!this.segmentStorage?.isInitialized) { + throw new Error("Segment storage is not initialized"); + } + return new HybridLoader( + manifestResponseUrl, + segment, + this.settings, + this.bandwidthApproximator, + this.segmentStorage, + this.eventHandlers + ); + }; + const streamTypeLoaderKeyMap = { + main: "mainStreamLoader", + secondary: "secondaryStreamLoader", + } as const; + const { type } = segment.stream; + const loaderKey = streamTypeLoaderKeyMap[type]; + + return (this[loaderKey] = + this[loaderKey] ?? createNewHybridLoader(this.manifestResponseUrl)); } } diff --git a/packages/p2p-media-loader-core/src/declarations.d.ts b/packages/p2p-media-loader-core/src/declarations.d.ts new file mode 100644 index 00000000..f85dacec --- /dev/null +++ b/packages/p2p-media-loader-core/src/declarations.d.ts @@ -0,0 +1,60 @@ +declare module "bittorrent-tracker" { + export default class Client { + constructor(options: { + infoHash: string; + peerId: string; + announce: string[]; + port: number; + rtcConfig?: RTCConfiguration; + getAnnounceOpts?: () => object; + }); + + on(event: E, handler: TrackerEventHandler): void; + + start(): void; + + complete(): void; + + update(data?: object): void; + + destroy(): void; + } + + export type TrackerEvent = "update" | "peer" | "warning" | "error"; + + export type TrackerEventHandler = E extends "update" + ? (data: object) => void + : E extends "peer" + ? (peer: PeerConnection) => void + : E extends "warning" + ? (warning: unknown) => void + : E extends "error" + ? (error: unknown) => void + : never; + + type PeerEvent = "connect" | "data" | "close" | "error"; + + export type PeerConnectionEventHandler = + E extends "connect" + ? () => void + : E extends "data" + ? (data: ArrayBuffer) => void + : E extends "close" + ? () => void + : E extends "error" + ? (error: { code: string }) => void + : never; + + export type PeerConnection = { + id: string; + initiator: boolean; + _channel: RTCDataChannel; + on( + event: E, + handler: PeerConnectionEventHandler + ): void; + send(data: string | ArrayBuffer): void; + write(data: string | ArrayBuffer): void; + destroy(): void; + }; +} diff --git a/packages/p2p-media-loader-core/src/enums.ts b/packages/p2p-media-loader-core/src/enums.ts new file mode 100644 index 00000000..059691b4 --- /dev/null +++ b/packages/p2p-media-loader-core/src/enums.ts @@ -0,0 +1,12 @@ +export enum PeerCommandType { + SegmentsAnnouncement, + SegmentRequest, + SegmentData, + SegmentAbsent, + CancelSegmentRequest, +} + +export enum PeerSegmentStatus { + Loaded, + LoadingByHttp, +} diff --git a/packages/p2p-media-loader-core/src/errors.ts b/packages/p2p-media-loader-core/src/errors.ts index 25903a60..421932e9 100644 --- a/packages/p2p-media-loader-core/src/errors.ts +++ b/packages/p2p-media-loader-core/src/errors.ts @@ -8,3 +8,9 @@ export class FetchError extends Error { this.details = details; } } + +export class RequestAbortError extends Error { + constructor(message = "AbortError") { + super(message); + } +} diff --git a/packages/p2p-media-loader-core/src/http-loader.ts b/packages/p2p-media-loader-core/src/http-loader.ts index 258f152c..efd00a9c 100644 --- a/packages/p2p-media-loader-core/src/http-loader.ts +++ b/packages/p2p-media-loader-core/src/http-loader.ts @@ -1,76 +1,119 @@ -import { FetchError } from "./errors"; +import { RequestAbortError, FetchError } from "./errors"; import { Segment } from "./types"; +import { HttpRequest, LoadProgress } from "./request-container"; -type Request = { - promise: Promise; - abortController: AbortController; -}; +export function getHttpSegmentRequest(segment: Segment): Readonly { + const { promise, abortController, progress } = fetchSegmentData(segment); + return { + type: "http", + promise, + progress, + abort: () => abortController.abort(), + }; +} -export class HttpLoader { - private readonly requests = new Map(); +function fetchSegmentData(segment: Segment) { + const headers = new Headers(); + const { url, byteRange, localId: segmentId } = segment; - async load(segment: Segment) { - const abortController = new AbortController(); - const promise = this.fetch(segment, abortController); - const requestContext: Request = { - abortController, - promise, - }; - this.requests.set(segment.localId, requestContext); - await promise; - this.requests.delete(segment.localId); - return promise; + if (byteRange) { + const { start, end } = byteRange; + const byteRangeString = `bytes=${start}-${end}`; + headers.set("Range", byteRangeString); } + const abortController = new AbortController(); - private async fetch(segment: Segment, abortController: AbortController) { - const headers = new Headers(); - const { url, byteRange } = segment; + const progress: LoadProgress = { + canBeTracked: false, + totalBytes: 0, + loadedBytes: 0, + percent: 0, + startTimestamp: performance.now(), + }; + const loadSegmentData = async () => { + try { + const response = await window.fetch(url, { + headers, + signal: abortController.signal, + }); - if (byteRange) { - const { start, end } = byteRange; - const byteRangeString = `bytes=${start}-${end}`; - headers.set("Range", byteRangeString); - } - const response = await fetch(url, { - headers, - signal: abortController.signal, - }); - if (!response.ok) { + if (response.ok) { + return await getDataPromiseAndMonitorProgress(response, progress); + } throw new FetchError( - response.statusText ?? "Fetch, bad network response", + response.statusText ?? `Network response was not for ${segmentId}`, response.status, response ); + } catch (error) { + if (error instanceof Error && error.name === "AbortError") { + throw new RequestAbortError(`Segment fetch was aborted ${segmentId}`); + } + throw error; } + }; - return response.arrayBuffer(); - } + return { + promise: loadSegmentData(), + abortController, + progress, + }; +} - isLoading(segmentId: string) { - return this.requests.has(segmentId); +async function getDataPromiseAndMonitorProgress( + response: Response, + progress: LoadProgress +): Promise { + const totalBytesString = response.headers.get("Content-Length"); + if (!response.body) { + return response.arrayBuffer().then((data) => { + progress.loadedBytes = data.byteLength; + progress.totalBytes = data.byteLength; + progress.lastLoadedChunkTimestamp = performance.now(); + progress.percent = 100; + return data; + }); } - abort(segmentId: string) { - this.requests.get(segmentId)?.abortController.abort(); - this.requests.delete(segmentId); + if (totalBytesString) { + progress.totalBytes = +totalBytesString; + progress.canBeTracked = true; } - getLoadingsAmount() { - return this.requests.size; + const reader = response.body.getReader(); + + progress.startTimestamp = performance.now(); + const chunks: Uint8Array[] = []; + for await (const chunk of readStream(reader)) { + chunks.push(chunk); + progress.loadedBytes += chunk.length; + progress.lastLoadedChunkTimestamp = performance.now(); + if (progress.canBeTracked) { + progress.percent = (progress.loadedBytes / progress.totalBytes) * 100; + } } - getLoadingSegmentIds() { - return this.requests.keys(); + if (!progress.canBeTracked) { + progress.totalBytes = progress.loadedBytes; + progress.percent = 100; } + const resultBuffer = new ArrayBuffer(progress.loadedBytes); + const view = new Uint8Array(resultBuffer); - getRequest(segmentId: string) { - return this.requests.get(segmentId)?.promise; + let offset = 0; + for (const chunk of chunks) { + view.set(chunk, offset); + offset += chunk.length; } + return resultBuffer; +} - abortAll() { - for (const request of this.requests.values()) { - request.abortController.abort(); - } - this.requests.clear(); +async function* readStream( + reader: ReadableStreamDefaultReader +): AsyncGenerator { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + yield value; } } diff --git a/packages/p2p-media-loader-core/src/hybrid-loader.ts b/packages/p2p-media-loader-core/src/hybrid-loader.ts index 379b2dc9..41b0c00c 100644 --- a/packages/p2p-media-loader-core/src/hybrid-loader.ts +++ b/packages/p2p-media-loader-core/src/hybrid-loader.ts @@ -1,208 +1,405 @@ -import { Segment, SegmentResponse, StreamWithSegments } from "./index"; -import { HttpLoader } from "./http-loader"; +import { Segment, StreamWithSegments } from "./index"; +import { getHttpSegmentRequest } from "./http-loader"; import { SegmentsMemoryStorage } from "./segments-storage"; -import { Settings } from "./types"; +import { Settings, CoreEventHandlers } from "./types"; import { BandwidthApproximator } from "./bandwidth-approximator"; import { Playback, QueueItem } from "./internal-types"; -import * as Utils from "./utils"; +import { + RequestsContainer, + EngineCallbacks, + HybridLoaderRequest, +} from "./request-container"; +import * as QueueUtils from "./utils/queue"; +import * as LoggerUtils from "./utils/logger"; +import { FetchError } from "./errors"; +import { P2PLoadersContainer } from "./p2p/loaders-container"; +import debug from "debug"; export class HybridLoader { - private readonly httpLoader = new HttpLoader(); - private readonly pluginRequests = new Map(); - private readonly segmentStorage: SegmentsMemoryStorage; + private readonly requests: RequestsContainer; + private readonly p2pLoaders: P2PLoadersContainer; private storageCleanUpIntervalId?: number; - private activeStream?: Readonly; - private lastRequestedSegment?: Readonly; - private playback?: Playback; + private lastRequestedSegment: Readonly; + private readonly playback: Playback; private lastQueueProcessingTimeStamp?: number; + private readonly segmentAvgDuration: number; + private randomHttpDownloadInterval!: number; + private readonly logger: { engine: debug.Debugger; loader: debug.Debugger }; + private readonly levelBandwidth = { value: 0, refreshCount: 0 }; constructor( + private streamManifestUrl: string, + requestedSegment: Segment, private readonly settings: Settings, - private readonly bandwidthApproximator: BandwidthApproximator + private readonly bandwidthApproximator: BandwidthApproximator, + private readonly segmentStorage: SegmentsMemoryStorage, + private readonly eventHandlers?: Pick ) { - this.segmentStorage = new SegmentsMemoryStorage(this.settings); - this.segmentStorage.setIsSegmentLockedPredicate((segment) => { - if (!this.playback || !this.activeStream?.segments.has(segment.localId)) { - return false; - } - const bufferRanges = Utils.getLoadBufferRanges( + this.lastRequestedSegment = requestedSegment; + const activeStream = requestedSegment.stream; + this.playback = { position: requestedSegment.startTime, rate: 1 }; + this.segmentAvgDuration = getSegmentAvgDuration(activeStream); + this.requests = new RequestsContainer(requestedSegment.stream.type); + + if (!this.segmentStorage.isInitialized) { + throw new Error("Segment storage is not initialized."); + } + this.segmentStorage.addIsSegmentLockedPredicate((segment) => { + if (segment.stream !== activeStream) return false; + const bufferRanges = QueueUtils.getLoadBufferRanges( this.playback, this.settings ); - return Utils.isSegmentActual(segment, bufferRanges); + return QueueUtils.isSegmentActual(segment, bufferRanges); }); - - this.storageCleanUpIntervalId = setInterval( - () => this.segmentStorage.clear(), - 1000 + this.p2pLoaders = new P2PLoadersContainer( + this.streamManifestUrl, + requestedSegment.stream, + this.requests, + this.segmentStorage, + this.settings ); + + const loader = debug(`core:hybrid-loader-${activeStream.type}`); + const engine = debug(`core:hybrid-loader-${activeStream.type}-engine`); + loader.color = "coral"; + engine.color = "orange"; + this.logger = { loader, engine }; + + this.setIntervalLoading(); + } + + private setIntervalLoading() { + const randomTimeout = (Math.random() * 2 + 1) * 1000; + this.randomHttpDownloadInterval = window.setTimeout(() => { + this.loadRandomThroughHttp(); + this.setIntervalLoading(); + }, randomTimeout); } - async loadSegment( - segment: Readonly, - stream: Readonly - ): Promise { - if (!this.playback) { - this.playback = { position: segment.startTime, rate: 1 }; + // api method for engines + async loadSegment(segment: Readonly, callbacks: EngineCallbacks) { + this.logger.engine(`requests: ${LoggerUtils.getSegmentString(segment)}`); + const { stream } = segment; + if (stream !== this.lastRequestedSegment.stream) { + this.logger.engine( + `stream changed to ${LoggerUtils.getStreamString(stream)}` + ); + this.p2pLoaders.changeCurrentLoader(stream); + this.refreshLevelBandwidth(true); } - if (stream !== this.activeStream) this.activeStream = stream; this.lastRequestedSegment = segment; - this.processQueue(); - const storageData = await this.segmentStorage.getSegment(segment.localId); - if (storageData) { - return { - data: storageData, - bandwidth: this.bandwidthApproximator.getBandwidth(), - }; + if (this.segmentStorage.hasSegment(segment)) { + // TODO: error handling + const data = await this.segmentStorage.getSegmentData(segment); + if (data) { + callbacks.onSuccess({ + data, + bandwidth: this.levelBandwidth.value, + }); + } + } else { + this.requests.addEngineCallbacks(segment, callbacks); } - const request = this.createPluginSegmentRequest(segment); - return request.responsePromise; + + this.processQueue(); } private processQueue(force = true) { - if (!this.activeStream || !this.lastRequestedSegment || !this.playback) { - return; - } const now = performance.now(); if ( !force && this.lastQueueProcessingTimeStamp !== undefined && - now - this.lastQueueProcessingTimeStamp >= 950 + now - this.lastQueueProcessingTimeStamp <= 1000 ) { return; } this.lastQueueProcessingTimeStamp = now; - const { queue, queueSegmentIds } = Utils.generateQueue({ - segment: this.lastRequestedSegment, - stream: this.activeStream, + const { queue, queueSegmentIds } = QueueUtils.generateQueue({ + lastRequestedSegment: this.lastRequestedSegment, playback: this.playback, settings: this.settings, - isSegmentLoaded: (segmentId) => this.segmentStorage.has(segmentId), + skipSegment: (segment) => this.segmentStorage.hasSegment(segment), }); - const bufferRanges = Utils.getLoadBufferRanges( - this.playback, - this.settings + this.requests.abortAllNotRequestedByEngine((segment) => + queueSegmentIds.has(segment.localId) ); - for (const segmentId of this.getLoadingSegmentIds()) { - const segment = this.activeStream.segments.get(segmentId); - if ( - !queueSegmentIds.has(segmentId) && - !this.pluginRequests.has(segmentId) && - !(segment && Utils.isSegmentActual(segment, bufferRanges)) - ) { - this.abortSegment(segmentId); - } - } - const { simultaneousHttpDownloads } = this.settings; - for (const { segment, statuses } of queue) { - if (this.httpLoader.isLoading(segment.localId)) continue; - if (statuses.has("high-demand")) { - if (this.httpLoader.getLoadingsAmount() < simultaneousHttpDownloads) { - void this.loadSegmentThroughHttp(segment); + const { simultaneousHttpDownloads, simultaneousP2PDownloads } = + this.settings; + + for (const item of queue) { + const { statuses, segment } = item; + const request = this.requests.get(segment); + + if (statuses.isHighDemand) { + if (request?.type === "http") continue; + + if (request?.type === "p2p") { + const timeToPlayback = getTimeToSegmentPlayback( + segment, + this.playback + ); + const remainingDownloadTime = + getPredictedRemainingDownloadTime(request); + if ( + remainingDownloadTime === undefined || + remainingDownloadTime > timeToPlayback + ) { + request.abort(); + } else { + continue; + } + } + if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { + void this.loadThroughHttp(item); continue; } - this.abortLastHttpLoadingAfter(queue, segment.localId); - if (this.httpLoader.getLoadingsAmount() < simultaneousHttpDownloads) { - void this.loadSegmentThroughHttp(segment); + + this.abortLastHttpLoadingAfter(queue, segment); + if (this.requests.httpRequestsCount < simultaneousHttpDownloads) { + void this.loadThroughHttp(item); + continue; + } + + if (this.requests.isP2PRequested(segment)) continue; + + if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + void this.loadThroughP2P(item); + continue; + } + + this.abortLastP2PLoadingAfter(queue, segment); + if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + void this.loadThroughP2P(item); + } + break; + } + if (statuses.isP2PDownloadable) { + if (request) continue; + if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + void this.loadThroughP2P(item); + continue; + } + + this.abortLastP2PLoadingAfter(queue, segment); + if (this.requests.p2pRequestsCount < simultaneousP2PDownloads) { + void this.loadThroughP2P(item); } } break; } } - getLoadingSegmentIds() { - return this.httpLoader.getLoadingSegmentIds(); - } - - abortSegment(segmentId: string) { - this.httpLoader.abort(segmentId); - const request = this.pluginRequests.get(segmentId); - if (!request) return; - request.onError("Abort"); - this.pluginRequests.delete(segmentId); + // api method for engines + abortSegment(segment: Segment) { + this.logger.engine("abort: ", LoggerUtils.getSegmentString(segment)); + this.requests.abortEngineRequest(segment); } - private async loadSegmentThroughHttp(segment: Segment) { + private async loadThroughHttp(item: QueueItem, isRandom = false) { + const { segment } = item; let data: ArrayBuffer | undefined; try { - data = await this.httpLoader.load(segment); + const httpRequest = getHttpSegmentRequest(segment); + + if (!isRandom) { + this.logger.loader( + `http request: ${LoggerUtils.getQueueItemString(item)}` + ); + } + + this.requests.addLoaderRequest(segment, httpRequest); + this.bandwidthApproximator.addLoading(httpRequest.progress); + data = await httpRequest.promise; + if (!data) return; + this.logger.loader(`http responses: ${segment.externalId}`); + this.onSegmentLoaded(item, "http", data); } catch (err) { - // TODO: handle abort + if (err instanceof FetchError) { + this.processQueue(); + } + } + } + + private async loadThroughP2P(item: QueueItem) { + const p2pLoader = this.p2pLoaders.currentLoader; + try { + const downloadPromise = p2pLoader.downloadSegment(item); + if (downloadPromise === undefined) return; + const data = await downloadPromise; + this.onSegmentLoaded(item, "p2p", data); + } catch (error) { + this.processQueue(); + } + } + + private loadRandomThroughHttp() { + const { simultaneousHttpDownloads } = this.settings; + const p2pLoader = this.p2pLoaders.currentLoader; + const connectedPeersAmount = p2pLoader.connectedPeersAmount; + if ( + this.requests.httpRequestsCount >= simultaneousHttpDownloads || + !connectedPeersAmount + ) { + return; + } + const { queue } = QueueUtils.generateQueue({ + lastRequestedSegment: this.lastRequestedSegment, + playback: this.playback, + settings: this.settings, + skipSegment: (segment, statuses) => + !statuses.isHttpDownloadable || + this.segmentStorage.hasSegment(segment) || + this.requests.isHybridLoaderRequested(segment) || + p2pLoader.isLoadingOrLoadedBySomeone(segment), + }); + if (!queue.length) return; + const peersAmount = connectedPeersAmount + 1; + const probability = Math.min(queue.length / peersAmount, 1); + const shouldLoad = Math.random() < probability; + + if (!shouldLoad) return; + const item = queue[Math.floor(Math.random() * queue.length)]; + void this.loadThroughHttp(item, true); + + this.logger.loader( + `http random request: ${LoggerUtils.getQueueItemString(item)}` + ); + } + + private onSegmentLoaded( + queueItem: QueueItem, + type: HybridLoaderRequest["type"], + data: ArrayBuffer + ) { + const { segment, statuses } = queueItem; + const byteLength = data.byteLength; + if (type === "http" && statuses.isHighDemand) { + this.refreshLevelBandwidth(true); } - if (!data) return; - this.bandwidthApproximator.addBytes(data.byteLength); void this.segmentStorage.storeSegment(segment, data); - const request = this.pluginRequests.get(segment.localId); - if (request) { - request.onSuccess({ - bandwidth: this.bandwidthApproximator.getBandwidth(), - data, - }); + + const bandwidth = statuses.isHighDemand + ? this.bandwidthApproximator.getBandwidth() + : this.levelBandwidth.value; + + this.requests.resolveEngineRequest(segment, { data, bandwidth }); + this.eventHandlers?.onSegmentLoaded?.(byteLength, type); + this.processQueue(); + } + + private abortLastHttpLoadingAfter(queue: QueueItem[], segment: Segment) { + for (const { segment: itemSegment } of arrayBackwards(queue)) { + if (itemSegment.localId === segment.localId) break; + if (this.requests.isHttpRequested(segment)) { + this.requests.abortLoaderRequest(segment); + this.logger.loader( + "http aborted: ", + LoggerUtils.getSegmentString(segment) + ); + break; + } } - this.pluginRequests.delete(segment.localId); } - private abortLastHttpLoadingAfter(queue: QueueItem[], segmentId: string) { - for (let i = queue.length - 1; i >= 0; i--) { - const { segment } = queue[i]; - if (segment.localId === segmentId) break; - if (this.httpLoader.isLoading(segment.localId)) { - this.abortSegment(segment.localId); + private abortLastP2PLoadingAfter(queue: QueueItem[], segment: Segment) { + for (const { segment: itemSegment } of arrayBackwards(queue)) { + if (itemSegment.localId === segment.localId) break; + if (this.requests.isP2PRequested(segment)) { + this.requests.abortLoaderRequest(segment); + this.logger.loader( + "p2p aborted: ", + LoggerUtils.getSegmentString(segment) + ); break; } } } + private refreshLevelBandwidth(levelChanged = false) { + if (levelChanged) this.levelBandwidth.refreshCount = 0; + if (this.levelBandwidth.refreshCount < 3) { + const currentBandwidth = this.bandwidthApproximator.getBandwidth(); + this.levelBandwidth.value = currentBandwidth ?? 0; + this.levelBandwidth.refreshCount++; + } + } + updatePlayback(position: number, rate: number) { - if (!this.playback) return; 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) this.playback.rate = rate; - this.processQueue(false); + if (isRateChanged && rate !== 0) this.playback.rate = rate; + if (isPositionSignificantlyChanged) { + this.logger.engine("position significantly changed"); + } + void this.processQueue(isPositionSignificantlyChanged); } - private createPluginSegmentRequest(segment: Segment) { - let onSuccess: Request["onSuccess"]; - let onError: Request["onError"]; - const responsePromise = new Promise((resolve, reject) => { - onSuccess = resolve; - onError = reject; - }); - const request: Request = { - responsePromise, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onSuccess: onSuccess!, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onError: onError!, - }; - - this.pluginRequests.set(segment.localId, request); - return request; + updateStream(stream: StreamWithSegments) { + if (stream !== this.lastRequestedSegment.stream) return; + this.logger.engine(`update stream: ${LoggerUtils.getStreamString(stream)}`); + this.processQueue(); } destroy() { clearInterval(this.storageCleanUpIntervalId); + clearInterval(this.randomHttpDownloadInterval); this.storageCleanUpIntervalId = undefined; void this.segmentStorage.destroy(); - this.httpLoader.abortAll(); - for (const request of this.pluginRequests.values()) { - request.onError("Aborted"); - } - this.pluginRequests.clear(); - this.playback = undefined; + this.requests.destroy(); + this.p2pLoaders.destroy(); + this.logger.loader.destroy(); + this.logger.engine.destroy(); } } -type Request = { - responsePromise: Promise; - onSuccess: (response: SegmentResponse) => void; - onError: (reason?: unknown) => void; -}; +function* arrayBackwards(arr: T[]) { + for (let i = arr.length - 1; i >= 0; i--) { + yield arr[i]; + } +} + +function getTimeToSegmentPlayback(segment: Segment, playback: Playback) { + return Math.max(segment.startTime - playback.position, 0) / playback.rate; +} + +function getPredictedRemainingDownloadTime(request: HybridLoaderRequest) { + const { progress } = request; + if (!progress || progress.lastLoadedChunkTimestamp === undefined) { + return undefined; + } + + const now = performance.now(); + const bandwidth = + progress.percent / + (progress.lastLoadedChunkTimestamp - progress.startTimestamp); + const remainingDownloadPercent = 100 - progress.percent; + const predictedRemainingTimeFromLastDownload = + remainingDownloadPercent / bandwidth; + const timeFromLastDownload = now - progress.lastLoadedChunkTimestamp; + return (predictedRemainingTimeFromLastDownload - timeFromLastDownload) / 1000; +} + +function getSegmentAvgDuration(stream: StreamWithSegments) { + const { segments } = stream; + let sumDuration = 0; + const size = segments.size; + for (const segment of segments.values()) { + const duration = segment.endTime - segment.startTime; + sumDuration += duration; + } + + return sumDuration / size; +} diff --git a/packages/p2p-media-loader-core/src/index.ts b/packages/p2p-media-loader-core/src/index.ts index 780df27e..036287ad 100644 --- a/packages/p2p-media-loader-core/src/index.ts +++ b/packages/p2p-media-loader-core/src/index.ts @@ -1,3 +1,5 @@ +/// + export { Core } from "./core"; export * from "./errors"; export type * from "./types"; diff --git a/packages/p2p-media-loader-core/src/internal-types.d.ts b/packages/p2p-media-loader-core/src/internal-types.d.ts new file mode 100644 index 00000000..f11f12dd --- /dev/null +++ b/packages/p2p-media-loader-core/src/internal-types.d.ts @@ -0,0 +1,60 @@ +import { Segment } from "./types"; +import { PeerCommandType } from "./enums"; + +export type Playback = { + position: number; + rate: number; +}; + +export type NumberRange = { + from: number; + to: number; +}; + +export type LoadBufferRanges = { + highDemand: NumberRange; + http: NumberRange; + p2p: NumberRange; +}; + +export type QueueItemStatuses = { + isHighDemand: boolean; + isHttpDownloadable: boolean; + isP2PDownloadable: boolean; +}; + +export type QueueItem = { segment: Segment; statuses: QueueItemStatuses }; + +export type BasePeerCommand = { + c: T; +}; + +// {l: loadedSegmentsExternalIds; p: loadingInProcessSegmentExternalIds} +export type JsonSegmentAnnouncement = { + l: string; + p: string; +}; + +export type PeerSegmentCommand = BasePeerCommand< + | PeerCommandType.SegmentRequest + | PeerCommandType.SegmentAbsent + | PeerCommandType.CancelSegmentRequest +> & { + i: string; +}; + +export type PeerSegmentAnnouncementCommand = + BasePeerCommand & { + a: JsonSegmentAnnouncement; + }; + +export type PeerSendSegmentCommand = + BasePeerCommand & { + i: string; + s: number; + }; + +export type PeerCommand = + | PeerSegmentCommand + | PeerSegmentAnnouncementCommand + | PeerSendSegmentCommand; diff --git a/packages/p2p-media-loader-core/src/internal-types.ts b/packages/p2p-media-loader-core/src/internal-types.ts deleted file mode 100644 index 813c57a7..00000000 --- a/packages/p2p-media-loader-core/src/internal-types.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Segment } from "./types"; - -export type Playback = { - position: number; - rate: number; -}; - -export type SegmentLoadStatus = - | "high-demand" - | "http-downloadable" - | "p2p-downloadable"; - -export type NumberRange = { - from: number; - to: number; -}; - -export type LoadBufferRanges = { - highDemand: NumberRange; - http: NumberRange; - p2p: NumberRange; -}; - -export type QueueItem = { segment: Segment; statuses: Set }; diff --git a/packages/p2p-media-loader-core/src/p2p/loader.ts b/packages/p2p-media-loader-core/src/p2p/loader.ts new file mode 100644 index 00000000..da25f186 --- /dev/null +++ b/packages/p2p-media-loader-core/src/p2p/loader.ts @@ -0,0 +1,251 @@ +import TrackerClient, { PeerConnection } from "bittorrent-tracker"; +import { Peer } from "./peer"; +import * as PeerUtil from "../utils/peer"; +import { Segment, Settings, StreamWithSegments } from "../types"; +import { QueueItem } from "../internal-types"; +import { SegmentsMemoryStorage } from "../segments-storage"; +import * as Utils from "../utils/utils"; +import * as LoggerUtils from "../utils/logger"; +import { PeerSegmentStatus } from "../enums"; +import { RequestsContainer } from "../request-container"; +import debug from "debug"; + +export class P2PLoader { + private readonly streamHash: string; + private readonly peerId: string; + private readonly trackerClient: TrackerClient; + private readonly peers = new Map(); + private readonly logger = debug("core:p2p-loader"); + private isAnnounceMicrotaskCreated = false; + + constructor( + private streamManifestUrl: string, + private readonly stream: StreamWithSegments, + private readonly requests: RequestsContainer, + private readonly segmentStorage: SegmentsMemoryStorage, + private readonly settings: Settings + ) { + this.peerId = PeerUtil.generatePeerId(); + const streamExternalId = Utils.getStreamExternalId( + this.streamManifestUrl, + this.stream + ); + this.streamHash = PeerUtil.getStreamHash(streamExternalId); + + this.trackerClient = createTrackerClient({ + streamHash: utf8ToHex(this.streamHash), + peerHash: utf8ToHex(this.peerId), + }); + this.logger( + `create tracker client: ${LoggerUtils.getStreamString(stream)}; ${ + this.peerId + }` + ); + this.subscribeOnTrackerEvents(this.trackerClient); + this.segmentStorage.subscribeOnUpdate( + this.stream, + this.broadcastAnnouncement + ); + this.requests.subscribeOnHttpRequestsUpdate(this.broadcastAnnouncement); + this.trackerClient.start(); + } + + private subscribeOnTrackerEvents(trackerClient: TrackerClient) { + // eslint-disable-next-line @typescript-eslint/no-empty-function + trackerClient.on("update", () => {}); + trackerClient.on("peer", (peerConnection) => { + const peer = this.peers.get(peerConnection.id); + if (peer) peer.addConnection(peerConnection); + else this.createPeer(peerConnection); + }); + trackerClient.on("warning", (warning) => { + this.logger( + `tracker warning (${LoggerUtils.getStreamString( + this.stream + )}: ${warning})` + ); + }); + trackerClient.on("error", (error) => { + this.logger( + `tracker error (${LoggerUtils.getStreamString(this.stream)}: ${error})` + ); + }); + } + + private createPeer(connection: PeerConnection) { + const peer = new Peer( + connection, + { + onPeerConnected: this.onPeerConnected.bind(this), + onPeerClosed: this.onPeerClosed.bind(this), + onSegmentRequested: this.onSegmentRequested.bind(this), + }, + this.settings + ); + this.logger(`create new peer: ${peer.id}`); + this.peers.set(connection.id, peer); + } + + downloadSegment(item: QueueItem): Promise | undefined { + const { segment, statuses } = item; + const untestedPeers: Peer[] = []; + let fastestPeer: Peer | undefined; + let fastestPeerBandwidth = 0; + + for (const peer of this.peers.values()) { + if ( + !peer.downloadingSegment && + peer.getSegmentStatus(segment) === PeerSegmentStatus.Loaded + ) { + const { bandwidth } = peer; + if (bandwidth === undefined) { + untestedPeers.push(peer); + } else if (bandwidth > fastestPeerBandwidth) { + fastestPeerBandwidth = bandwidth; + fastestPeer = peer; + } + } + } + + const peer = untestedPeers.length + ? getRandomItem(untestedPeers) + : fastestPeer; + + if (!peer) return; + + const request = peer.requestSegment(segment); + this.requests.addLoaderRequest(segment, request); + this.logger( + `p2p request ${segment.externalId} | ${LoggerUtils.getStatusesString( + statuses + )}` + ); + request.promise.then(() => { + this.logger(`p2p loaded: ${segment.externalId}`); + }); + + return request.promise; + } + + isLoadingOrLoadedBySomeone(segment: Segment): boolean { + for (const peer of this.peers.values()) { + if (peer.getSegmentStatus(segment)) return true; + } + return false; + } + + get connectedPeersAmount() { + let count = 0; + for (const peer of this.peers.values()) { + if (peer.isConnected) count++; + } + return count; + } + + private getSegmentsAnnouncement() { + const loaded: string[] = + this.segmentStorage.getStoredSegmentExternalIdsOfStream(this.stream); + const httpLoading: string[] = []; + + for (const request of this.requests.httpRequests()) { + const segment = this.stream.segments.get(request.segment.localId); + if (!segment) continue; + + httpLoading.push(segment.externalId); + } + return PeerUtil.getJsonSegmentsAnnouncement(loaded, httpLoading); + } + + private onPeerConnected(peer: Peer) { + this.logger(`connected with peer: ${peer.id}`); + const announcement = this.getSegmentsAnnouncement(); + peer.sendSegmentsAnnouncement(announcement); + } + + private onPeerClosed(peer: Peer) { + this.logger(`peer closed: ${peer.id}`); + this.peers.delete(peer.id); + } + + private broadcastAnnouncement = () => { + if (this.isAnnounceMicrotaskCreated) return; + + this.isAnnounceMicrotaskCreated = true; + queueMicrotask(() => { + const announcement = this.getSegmentsAnnouncement(); + for (const peer of this.peers.values()) { + if (!peer.isConnected) continue; + peer.sendSegmentsAnnouncement(announcement); + } + this.isAnnounceMicrotaskCreated = false; + }); + }; + + private async onSegmentRequested(peer: Peer, segmentExternalId: string) { + const segment = Utils.getSegmentFromStreamByExternalId( + this.stream, + segmentExternalId + ); + const segmentData = + segment && (await this.segmentStorage.getSegmentData(segment)); + if (segmentData) void peer.sendSegmentData(segmentExternalId, segmentData); + else peer.sendSegmentAbsent(segmentExternalId); + } + + destroy() { + this.logger( + `destroy tracker client: ${LoggerUtils.getStreamString(this.stream)}` + ); + this.segmentStorage.unsubscribeFromUpdate( + this.stream, + this.broadcastAnnouncement + ); + this.requests.unsubscribeFromHttpRequestsUpdate(this.broadcastAnnouncement); + for (const peer of this.peers.values()) { + peer.destroy(); + } + this.peers.clear(); + this.trackerClient.destroy(); + } +} + +function createTrackerClient({ + streamHash, + peerHash, +}: { + streamHash: string; + peerHash: string; +}) { + return new TrackerClient({ + infoHash: streamHash, + peerId: peerHash, + port: 6881, + announce: [ + // "wss://tracker.novage.com.ua", + "wss://tracker.openwebtorrent.com", + ], + rtcConfig: { + iceServers: [ + { + urls: [ + "stun:stun.l.google.com:19302", + "stun:global.stun.twilio.com:3478", + ], + }, + ], + }, + }); +} + +function utf8ToHex(utf8String: string) { + let result = ""; + for (let i = 0; i < utf8String.length; i++) { + result += utf8String.charCodeAt(i).toString(16); + } + + return result; +} + +function getRandomItem(items: T[]): T { + return items[Math.floor(Math.random() * items.length)]; +} diff --git a/packages/p2p-media-loader-core/src/p2p/loaders-container.ts b/packages/p2p-media-loader-core/src/p2p/loaders-container.ts new file mode 100644 index 00000000..cbc8ced9 --- /dev/null +++ b/packages/p2p-media-loader-core/src/p2p/loaders-container.ts @@ -0,0 +1,99 @@ +import { P2PLoader } from "./loader"; +import debug from "debug"; +import { Settings, Stream, StreamWithSegments } from "../index"; +import { RequestsContainer } from "../request-container"; +import { SegmentsMemoryStorage } from "../segments-storage"; +import * as LoggerUtils from "../utils/logger"; + +type P2PLoaderContainerItem = { + stream: Stream; + loader: P2PLoader; + destroyTimeoutId?: number; + loggerInfo: string; +}; + +export class P2PLoadersContainer { + private readonly loaders = new Map(); + private _currentLoaderItem!: P2PLoaderContainerItem; + private readonly logger = debug("core:p2p-loaders-container"); + + constructor( + private readonly streamManifestUrl: string, + stream: StreamWithSegments, + private readonly requests: RequestsContainer, + private readonly segmentStorage: SegmentsMemoryStorage, + private readonly settings: Settings + ) { + this.changeCurrentLoader(stream); + } + + private createLoader(stream: StreamWithSegments): P2PLoaderContainerItem { + if (this.loaders.has(stream.localId)) { + throw new Error("Loader for this stream already exists"); + } + const loader = new P2PLoader( + this.streamManifestUrl, + stream, + this.requests, + this.segmentStorage, + this.settings + ); + const loggerInfo = LoggerUtils.getStreamString(stream); + this.logger(`created new loader: ${loggerInfo}`); + return { + loader, + stream, + loggerInfo: LoggerUtils.getStreamString(stream), + }; + } + + changeCurrentLoader(stream: StreamWithSegments) { + const loaderItem = this.loaders.get(stream.localId); + const prev = this._currentLoaderItem; + if (loaderItem) { + this._currentLoaderItem = loaderItem; + clearTimeout(loaderItem.destroyTimeoutId); + loaderItem.destroyTimeoutId = undefined; + } else { + const loader = this.createLoader(stream); + this.loaders.set(stream.localId, loader); + this._currentLoaderItem = loader; + } + this.logger( + `change current p2p loader: ${LoggerUtils.getStreamString(stream)}` + ); + + if (!prev) return; + + const ids = this.segmentStorage.getStoredSegmentExternalIdsOfStream( + prev.stream + ); + if (!ids.length) this.destroyAndRemoveLoader(prev); + else this.setLoaderDestroyTimeout(prev); + } + + private setLoaderDestroyTimeout(item: P2PLoaderContainerItem) { + item.destroyTimeoutId = window.setTimeout( + () => this.destroyAndRemoveLoader(item), + this.settings.p2pLoaderDestroyTimeout + ); + } + + private destroyAndRemoveLoader(item: P2PLoaderContainerItem) { + item.loader.destroy(); + this.loaders.delete(item.stream.localId); + this.logger(`destroy p2p loader: `, item.loggerInfo); + } + + get currentLoader() { + return this._currentLoaderItem.loader; + } + + destroy() { + for (const { loader, destroyTimeoutId } of this.loaders.values()) { + loader.destroy(); + clearTimeout(destroyTimeoutId); + } + this.loaders.clear(); + } +} diff --git a/packages/p2p-media-loader-core/src/p2p/peer.ts b/packages/p2p-media-loader-core/src/p2p/peer.ts new file mode 100644 index 00000000..9fb4d43f --- /dev/null +++ b/packages/p2p-media-loader-core/src/p2p/peer.ts @@ -0,0 +1,391 @@ +import { PeerConnection } from "bittorrent-tracker"; +import { + JsonSegmentAnnouncement, + PeerCommand, + PeerSegmentAnnouncementCommand, + PeerSegmentCommand, + PeerSendSegmentCommand, +} from "../internal-types"; +import { PeerCommandType, PeerSegmentStatus } from "../enums"; +import * as PeerUtil from "../utils/peer"; +import { P2PRequest } from "../request-container"; +import { Segment, Settings } from "../types"; +import * as Utils from "../utils/utils"; +import debug from "debug"; + +export class PeerRequestError extends Error { + constructor( + readonly type: + | "abort" + | "request-timeout" + | "response-bytes-mismatch" + | "segment-absent" + | "peer-closed" + | "destroy" + ) { + super(); + } +} + +type PeerEventHandlers = { + onPeerConnected: (peer: Peer) => void; + onPeerClosed: (peer: Peer) => void; + onSegmentRequested: (peer: Peer, segmentId: string) => void; +}; + +type PeerRequest = { + segment: Segment; + p2pRequest: P2PRequest; + resolve: (data: ArrayBuffer) => void; + reject: (error: PeerRequestError) => void; + chunks: ArrayBuffer[]; + responseTimeoutId: number; +}; + +type PeerSettings = Pick< + Settings, + "p2pSegmentDownloadTimeout" | "webRtcMaxMessageSize" +>; + +export class Peer { + readonly id: string; + private connection?: PeerConnection; + private connections = new Set(); + private segments = new Map(); + private request?: PeerRequest; + private readonly logger = debug("core:peer"); + private readonly bandwidthMeasurer = new BandwidthMeasurer(); + private isUploadingSegment = false; + + constructor( + connection: PeerConnection, + private readonly eventHandlers: PeerEventHandlers, + private readonly settings: PeerSettings + ) { + this.id = hexToUtf8(connection.id); + this.eventHandlers = eventHandlers; + this.addConnection(connection); + } + + addConnection(connection: PeerConnection) { + if (this.connection && connection !== this.connection) { + connection.destroy(); + return; + } + this.connections.add(connection); + + connection.on("connect", () => { + if (this.connection) return; + + this.connection = connection; + for (const item of this.connections) { + if (item !== connection) { + this.connections.delete(item); + item.destroy(); + } + } + this.eventHandlers.onPeerConnected(this); + this.logger(`connected with peer: ${this.id}`); + + connection.on("data", this.onReceiveData.bind(this)); + connection.on("close", () => { + this.connection = undefined; + this.cancelSegmentRequest("peer-closed"); + this.logger(`connection with peer closed: ${this.id}`); + this.destroy(); + this.eventHandlers.onPeerClosed(this); + }); + connection.on("error", (error) => { + if (error.code === "ERR_DATA_CHANNEL") { + this.logger(`peer error: ${this.id} ${error.code}`); + this.destroy(); + this.eventHandlers.onPeerClosed(this); + } + }); + }); + } + + get isConnected() { + return !!this.connection; + } + + get downloadingSegment(): Segment | undefined { + return this.request?.segment; + } + + get bandwidth(): number | undefined { + return this.bandwidthMeasurer.getBandwidth(); + } + + getSegmentStatus(segment: Segment): PeerSegmentStatus | undefined { + const { externalId } = segment; + return this.segments.get(externalId); + } + + private onReceiveData(data: ArrayBuffer) { + const command = PeerUtil.getPeerCommandFromArrayBuffer(data); + if (!command) { + this.receiveSegmentChunk(data); + return; + } + + switch (command.c) { + case PeerCommandType.SegmentsAnnouncement: + this.segments = PeerUtil.getSegmentsFromPeerAnnouncement(command.a); + break; + + case PeerCommandType.SegmentRequest: + this.eventHandlers.onSegmentRequested(this, command.i); + break; + + case PeerCommandType.SegmentData: + if (this.request?.segment.externalId === command.i) { + const { progress } = this.request.p2pRequest; + progress.totalBytes = command.s; + progress.canBeTracked = true; + } + break; + + case PeerCommandType.SegmentAbsent: + if (this.request?.segment.externalId === command.i) { + this.cancelSegmentRequest("segment-absent"); + this.segments.delete(command.i); + } + break; + + case PeerCommandType.CancelSegmentRequest: + this.isUploadingSegment = false; + break; + } + } + + private sendCommand(command: PeerCommand) { + if (!this.connection) return; + this.connection.send(JSON.stringify(command)); + } + + requestSegment(segment: Segment) { + if (this.request) { + throw new Error("Segment already is downloading"); + } + const { externalId } = segment; + const command: PeerSegmentCommand = { + c: PeerCommandType.SegmentRequest, + i: externalId, + }; + this.sendCommand(command); + this.request = this.createPeerRequest(segment); + return this.request.p2pRequest; + } + + sendSegmentsAnnouncement(announcement: JsonSegmentAnnouncement) { + const command: PeerSegmentAnnouncementCommand = { + c: PeerCommandType.SegmentsAnnouncement, + a: announcement, + }; + this.sendCommand(command); + } + + async sendSegmentData(segmentExternalId: string, data: ArrayBuffer) { + if (!this.connection) return; + this.logger(`send segment ${segmentExternalId} to ${this.id}`); + const command: PeerSendSegmentCommand = { + c: PeerCommandType.SegmentData, + i: segmentExternalId, + s: data.byteLength, + }; + this.sendCommand(command); + + const chunks = getBufferChunks(data, this.settings.webRtcMaxMessageSize); + const connection = this.connection; + const channel = connection._channel; + const { promise, resolve, reject } = Utils.getControlledPromise(); + + const sendChunk = () => { + while (channel.bufferedAmount <= channel.bufferedAmountLowThreshold) { + const chunk = chunks.next().value; + if (!chunk) { + resolve(); + break; + } + if (chunk && !this.isUploadingSegment) { + reject(); + break; + } + connection.send(chunk); + } + }; + try { + channel.addEventListener("bufferedamountlow", sendChunk); + this.isUploadingSegment = true; + sendChunk(); + await promise; + this.logger(`segment ${segmentExternalId} has been sent to ${this.id}`); + } catch (err) { + this.logger(`cancel segment uploading ${segmentExternalId}`); + } finally { + channel.removeEventListener("bufferedamountlow", sendChunk); + this.isUploadingSegment = false; + } + } + + sendSegmentAbsent(segmentExternalId: string) { + const command: PeerSegmentCommand = { + c: PeerCommandType.SegmentAbsent, + i: segmentExternalId, + }; + this.sendCommand(command); + } + + private createPeerRequest(segment: Segment): PeerRequest { + const { promise, resolve, reject } = + Utils.getControlledPromise(); + return { + segment, + resolve, + reject, + responseTimeoutId: this.setRequestTimeout(), + chunks: [], + p2pRequest: { + type: "p2p", + progress: { + canBeTracked: false, + totalBytes: 0, + loadedBytes: 0, + percent: 0, + startTimestamp: performance.now(), + }, + promise, + abort: () => this.cancelSegmentRequest("abort"), + }, + }; + } + + private receiveSegmentChunk(chunk: ArrayBuffer): void { + const { request } = this; + const progress = request?.p2pRequest?.progress; + if (!request || !progress) return; + + progress.loadedBytes += chunk.byteLength; + progress.percent = (progress.loadedBytes / progress.loadedBytes) * 100; + progress.lastLoadedChunkTimestamp = performance.now(); + request.chunks.push(chunk); + + if (progress.loadedBytes === progress.totalBytes) { + const segmentData = joinChunks(request.chunks); + const { lastLoadedChunkTimestamp, startTimestamp, loadedBytes } = + progress; + const loadingDuration = lastLoadedChunkTimestamp - startTimestamp; + this.bandwidthMeasurer.addMeasurement(loadedBytes, loadingDuration); + this.approveRequest(segmentData); + } else if (progress.loadedBytes > progress.totalBytes) { + this.cancelSegmentRequest("response-bytes-mismatch"); + } + } + + private approveRequest(data: ArrayBuffer) { + this.request?.resolve(data); + this.clearRequest(); + } + + private cancelSegmentRequest(type: PeerRequestError["type"]) { + if (!this.request) return; + this.logger( + `cancel segment request ${this.request?.segment.externalId} (${type})` + ); + const error = new PeerRequestError(type); + const sendCancelCommandTypes: PeerRequestError["type"][] = [ + "destroy", + "abort", + "request-timeout", + "response-bytes-mismatch", + ]; + if (sendCancelCommandTypes.includes(type)) { + this.sendCommand({ + c: PeerCommandType.CancelSegmentRequest, + i: this.request.segment.externalId, + }); + } + this.request.reject(error); + this.clearRequest(); + } + + private setRequestTimeout(): number { + return window.setTimeout( + () => this.cancelSegmentRequest("request-timeout"), + this.settings.p2pSegmentDownloadTimeout + ); + } + + private clearRequest() { + clearTimeout(this.request?.responseTimeoutId); + this.request = undefined; + } + + destroy() { + this.cancelSegmentRequest("destroy"); + this.connection?.destroy(); + this.connection = undefined; + for (const connection of this.connections) { + connection.destroy(); + } + this.connections.clear(); + } +} + +const SMOOTHING_COEF = 0.5; + +class BandwidthMeasurer { + private bandwidth?: number; + + addMeasurement(bytes: number, loadingDurationMs: number) { + const bits = bytes * 8; + const currentBandwidth = (bits * 1000) / loadingDurationMs; + + this.bandwidth = + this.bandwidth !== undefined + ? currentBandwidth * SMOOTHING_COEF + + (1 - SMOOTHING_COEF) * this.bandwidth + : currentBandwidth; + } + + getBandwidth() { + return this.bandwidth; + } +} + +function* getBufferChunks( + data: ArrayBuffer, + maxChunkSize: number +): Generator { + let bytesLeft = data.byteLength; + while (bytesLeft > 0) { + const bytesToSend = bytesLeft >= maxChunkSize ? maxChunkSize : bytesLeft; + const from = data.byteLength - bytesLeft; + const buffer = data.slice(from, from + bytesToSend); + bytesLeft -= bytesToSend; + yield buffer; + } +} + +function joinChunks(chunks: ArrayBuffer[]): ArrayBuffer { + const bytesSum = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0); + const buffer = new Uint8Array(bytesSum); + let offset = 0; + for (const chunk of chunks) { + buffer.set(new Uint8Array(chunk), offset); + offset += chunk.byteLength; + } + + return buffer; +} + +function hexToUtf8(hexString: string) { + const bytes = new Uint8Array(hexString.length / 2); + + for (let i = 0; i < hexString.length; i += 2) { + bytes[i / 2] = parseInt(hexString.slice(i, i + 2), 16); + } + const decoder = new TextDecoder(); + return decoder.decode(bytes); +} diff --git a/packages/p2p-media-loader-core/src/request-container.ts b/packages/p2p-media-loader-core/src/request-container.ts new file mode 100644 index 00000000..78140899 --- /dev/null +++ b/packages/p2p-media-loader-core/src/request-container.ts @@ -0,0 +1,233 @@ +import { Segment, SegmentResponse, StreamType } from "./types"; +import { RequestAbortError } from "./errors"; +import { Subscriptions } from "./segments-storage"; +import Debug from "debug"; + +export type EngineCallbacks = { + onSuccess: (response: SegmentResponse) => void; + onError: (reason?: unknown) => void; +}; + +export type LoadProgress = { + startTimestamp: number; + lastLoadedChunkTimestamp?: number; + percent: number; + loadedBytes: number; + totalBytes: number; + canBeTracked: boolean; +}; + +type RequestBase = { + promise: Promise; + abort: () => void; + progress: LoadProgress; +}; + +export type HttpRequest = RequestBase & { + type: "http"; +}; + +export type P2PRequest = RequestBase & { + type: "p2p"; +}; + +export type HybridLoaderRequest = HttpRequest | P2PRequest; + +type Request = { + segment: Readonly; + loaderRequest?: Readonly; + engineCallbacks?: Readonly; +}; + +function getRequestItemId(segment: Segment) { + return segment.localId; +} + +export class RequestsContainer { + private readonly requests = new Map(); + private readonly onHttpRequestsHandlers = new Subscriptions(); + private readonly logger: Debug.Debugger; + + constructor(streamType: StreamType) { + this.logger = Debug(`core:requests-container-${streamType}`); + this.logger.color = "LightSeaGreen"; + } + + get httpRequestsCount() { + let count = 0; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const request of this.httpRequests()) count++; + return count; + } + + get p2pRequestsCount() { + let count = 0; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const request of this.p2pRequests()) count++; + return count; + } + + get(segment: Segment) { + const id = getRequestItemId(segment); + return this.requests.get(id)?.loaderRequest; + } + + addLoaderRequest(segment: Segment, loaderRequest: HybridLoaderRequest) { + const segmentId = getRequestItemId(segment); + const existingRequest = this.requests.get(segmentId); + if (existingRequest) { + existingRequest.loaderRequest = loaderRequest; + } else { + this.requests.set(segmentId, { + segment, + loaderRequest, + }); + } + this.logger( + `add loader request: ${loaderRequest.type} ${segment.externalId}` + ); + + const clearRequestItem = () => this.clearRequestItem(segmentId, "loader"); + loaderRequest.promise + .then(() => clearRequestItem()) + .catch((err) => { + if (err instanceof RequestAbortError) clearRequestItem(); + }); + if (loaderRequest.type === "http") this.onHttpRequestsHandlers.fire(); + } + + addEngineCallbacks(segment: Segment, engineCallbacks: EngineCallbacks) { + const segmentId = getRequestItemId(segment); + const requestItem = this.requests.get(segmentId); + + const { onSuccess, onError } = engineCallbacks; + engineCallbacks.onSuccess = (response) => { + this.clearRequestItem(segmentId, "engine"); + return onSuccess(response); + }; + + engineCallbacks.onError = (error) => { + if (error instanceof RequestAbortError) { + this.clearRequestItem(segmentId, "engine"); + } + return onError(error); + }; + + if (requestItem) { + requestItem.engineCallbacks = engineCallbacks; + } else { + this.requests.set(segmentId, { + segment, + engineCallbacks, + }); + } + this.logger(`add engine request ${segment.externalId}`); + } + + values() { + return this.requests.values(); + } + + *httpRequests(): Generator { + for (const request of this.requests.values()) { + if (request.loaderRequest?.type === "http") yield request; + } + } + + *p2pRequests(): Generator { + for (const request of this.requests.values()) { + if (request.loaderRequest?.type === "p2p") yield request; + } + } + + resolveEngineRequest(segment: Segment, response: SegmentResponse) { + const id = getRequestItemId(segment); + this.requests.get(id)?.engineCallbacks?.onSuccess(response); + } + + isHttpRequested(segment: Segment): boolean { + const id = getRequestItemId(segment); + return this.requests.get(id)?.loaderRequest?.type === "http"; + } + + isP2PRequested(segment: Segment): boolean { + const id = getRequestItemId(segment); + return this.requests.get(id)?.loaderRequest?.type === "p2p"; + } + + isHybridLoaderRequested(segment: Segment): boolean { + const id = getRequestItemId(segment); + return !!this.requests.get(id)?.loaderRequest; + } + + abortEngineRequest(segment: Segment) { + const id = getRequestItemId(segment); + const request = this.requests.get(id); + if (!request) return; + + request.engineCallbacks?.onError(new RequestAbortError()); + request.loaderRequest?.abort(); + } + + abortLoaderRequest(segment: Segment) { + const id = getRequestItemId(segment); + this.requests.get(id)?.loaderRequest?.abort(); + } + + private clearRequestItem( + requestItemId: string, + type: "loader" | "engine" + ): void { + const requestItem = this.requests.get(requestItemId); + if (!requestItem) return; + const { segment, loaderRequest } = requestItem; + const segmentExternalId = segment.externalId; + + if (type === "engine") { + this.logger(`remove engine callbacks: ${segmentExternalId}`); + delete requestItem.engineCallbacks; + } + if (type === "loader" && loaderRequest) { + this.logger( + `remove loader request: ${loaderRequest.type} ${segmentExternalId}` + ); + if (loaderRequest.type === "http") { + this.onHttpRequestsHandlers.fire(); + } + delete requestItem.loaderRequest; + } + if (!requestItem.engineCallbacks && !requestItem.loaderRequest) { + this.logger(`remove request item ${segmentExternalId}`); + const segmentId = getRequestItemId(segment); + this.requests.delete(segmentId); + } + } + + abortAllNotRequestedByEngine(isLocked?: (segment: Segment) => boolean) { + const isSegmentLocked = isLocked ? isLocked : () => false; + for (const { + loaderRequest, + engineCallbacks, + segment, + } of this.requests.values()) { + if (engineCallbacks || !loaderRequest) continue; + if (!isSegmentLocked(segment)) loaderRequest.abort(); + } + } + + subscribeOnHttpRequestsUpdate(handler: () => void) { + this.onHttpRequestsHandlers.add(handler); + } + + unsubscribeFromHttpRequestsUpdate(handler: () => void) { + this.onHttpRequestsHandlers.remove(handler); + } + + destroy() { + for (const request of this.requests.values()) { + request.loaderRequest?.abort(); + request.engineCallbacks?.onError(); + } + this.requests.clear(); + } +} diff --git a/packages/p2p-media-loader-core/src/segments-storage.ts b/packages/p2p-media-loader-core/src/segments-storage.ts index c743d51c..e8b1a1db 100644 --- a/packages/p2p-media-loader-core/src/segments-storage.ts +++ b/packages/p2p-media-loader-core/src/segments-storage.ts @@ -1,83 +1,206 @@ -import { Segment } from "./types"; +import { Segment, Settings, Stream } from "./types"; +import Debug from "debug"; + +type StorageSettings = Pick< + Settings, + "cachedSegmentExpiration" | "cachedSegmentsCount" +>; + +function getStreamShortExternalId(stream: Readonly) { + const { type, index } = stream; + return `${type}-${index}`; +} + +function getStorageItemId(segment: Segment) { + const streamExternalId = getStreamShortExternalId(segment.stream); + return `${streamExternalId}|${segment.externalId}`; +} + +export class Subscriptions< + T extends (...args: unknown[]) => void = () => void +> { + private readonly list: Set; + + constructor(handlers?: T | T[]) { + if (handlers) { + this.list = new Set(Array.isArray(handlers) ? handlers : [handlers]); + } else { + this.list = new Set(); + } + } + + add(handler: T) { + this.list.add(handler); + } + + remove(handler: T) { + this.list.delete(handler); + } + + fire(...args: Parameters) { + for (const handler of this.list) { + handler(...args); + } + } + + get isEmpty() { + return this.list.size === 0; + } +} + +type StorageItem = { + segment: Segment; + data: ArrayBuffer; + lastAccessed: number; +}; export class SegmentsMemoryStorage { - private cache = new Map< - string, - { segment: Segment; data: ArrayBuffer; lastAccessed: number } - >(); - private isSegmentLockedPredicate?: (segment: Segment) => boolean; + private cache = new Map(); + private _isInitialized = false; + private readonly isSegmentLockedPredicates: (( + segment: Segment + ) => boolean)[] = []; + private onUpdateHandlers = new Map(); + private readonly logger: Debug.Debugger; constructor( - private settings: { - cachedSegmentExpiration: number; - cachedSegmentsCount: number; - } - ) {} + private readonly masterManifestUrl: string, + private readonly settings: StorageSettings + ) { + this.logger = Debug("core:segment-memory-storage"); + this.logger.color = "RebeccaPurple"; + } + + async initialize() { + this._isInitialized = true; + this.logger("initialized"); + } - setIsSegmentLockedPredicate(predicate: (segment: Segment) => boolean) { - this.isSegmentLockedPredicate = predicate; + get isInitialized(): boolean { + return this._isInitialized; + } + + addIsSegmentLockedPredicate(predicate: (segment: Segment) => boolean) { + this.isSegmentLockedPredicates.push(predicate); + } + + private isSegmentLocked(segment: Segment): boolean { + return this.isSegmentLockedPredicates.some((p) => p(segment)); } async storeSegment(segment: Segment, data: ArrayBuffer) { - this.cache.set(segment.localId, { + const id = getStorageItemId(segment); + const streamId = getStreamShortExternalId(segment.stream); + this.cache.set(id, { segment, data, lastAccessed: performance.now(), }); + this.logger(`add segment: ${id}`); + this.fireOnUpdateSubscriptions(streamId); + void this.clear(); } - async getSegment(segmentId: string): Promise { - const cacheItem = this.cache.get(segmentId); + async getSegmentData(segment: Segment): Promise { + const itemId = getStorageItemId(segment); + const cacheItem = this.cache.get(itemId); if (cacheItem === undefined) return undefined; cacheItem.lastAccessed = performance.now(); return cacheItem.data; } - has(segmentId: string) { - return this.cache.has(segmentId); + hasSegment(segment: Segment): boolean { + const id = getStorageItemId(segment); + return this.cache.has(id); } - async clear(): Promise { - const segmentsToDelete: string[] = []; - const remainingSegments: { - lastAccessed: number; - segment: Segment; - }[] = []; + getStoredSegmentExternalIdsOfStream(stream: Stream) { + const streamId = getStreamShortExternalId(stream); + const externalIds: string[] = []; + for (const { segment } of this.cache.values()) { + const itemStreamId = getStreamShortExternalId(segment.stream); + if (itemStreamId === streamId) externalIds.push(segment.externalId); + } + return externalIds; + } + + private async clear(): Promise { + const itemsToDelete: string[] = []; + const remainingItems: [string, StorageItem][] = []; + const streamIdsOfChangedItems = new Set(); // Delete old segments const now = performance.now(); - for (const [segmentId, { lastAccessed, segment }] of this.cache.entries()) { + for (const entry of this.cache.entries()) { + const [itemId, item] = entry; + const { lastAccessed, segment } = item; if (now - lastAccessed > this.settings.cachedSegmentExpiration) { - if (!this.isSegmentLockedPredicate?.(segment)) { - segmentsToDelete.push(segmentId); + if (!this.isSegmentLocked(segment)) { + const streamId = getStreamShortExternalId(segment.stream); + itemsToDelete.push(itemId); + streamIdsOfChangedItems.add(streamId); } } else { - remainingSegments.push({ segment, lastAccessed }); + remainingItems.push(entry); } } // Delete segments over cached count let countOverhead = - remainingSegments.length - this.settings.cachedSegmentsCount; + remainingItems.length - this.settings.cachedSegmentsCount; if (countOverhead > 0) { - remainingSegments.sort((a, b) => a.lastAccessed - b.lastAccessed); + remainingItems.sort(([, a], [, b]) => a.lastAccessed - b.lastAccessed); - for (const cachedSegment of remainingSegments) { - if (!this.isSegmentLockedPredicate?.(cachedSegment.segment)) { - segmentsToDelete.push(cachedSegment.segment.localId); + for (const [itemId, { segment }] of remainingItems) { + if (!this.isSegmentLocked(segment)) { + const streamId = getStreamShortExternalId(segment.stream); + itemsToDelete.push(itemId); + streamIdsOfChangedItems.add(streamId); countOverhead--; if (countOverhead === 0) break; } } } - segmentsToDelete.forEach((id) => this.cache.delete(id)); - return segmentsToDelete.length > 0; + if (itemsToDelete.length) { + this.logger(`cleared ${itemsToDelete.length} segments`); + itemsToDelete.forEach((id) => this.cache.delete(id)); + for (const streamId of streamIdsOfChangedItems) { + this.fireOnUpdateSubscriptions(streamId); + } + } + + return itemsToDelete.length > 0; + } + + subscribeOnUpdate(stream: Stream, handler: () => void) { + const streamId = getStreamShortExternalId(stream); + const handlers = this.onUpdateHandlers.get(streamId); + if (!handlers) { + this.onUpdateHandlers.set(streamId, new Subscriptions(handler)); + } else { + handlers.add(handler); + } + } + + unsubscribeFromUpdate(stream: Stream, handler: () => void) { + const streamId = getStreamShortExternalId(stream); + const handlers = this.onUpdateHandlers.get(streamId); + if (handlers) { + handlers.remove(handler); + if (handlers.isEmpty) this.onUpdateHandlers.delete(streamId); + } + } + + private fireOnUpdateSubscriptions(streamId: string) { + this.onUpdateHandlers.get(streamId)?.fire(); } public async destroy() { this.cache.clear(); + this.onUpdateHandlers.clear(); + this._isInitialized = false; } } diff --git a/packages/p2p-media-loader-core/src/types.ts b/packages/p2p-media-loader-core/src/types.d.ts similarity index 53% rename from packages/p2p-media-loader-core/src/types.ts rename to packages/p2p-media-loader-core/src/types.d.ts index 38aefe61..a894961a 100644 --- a/packages/p2p-media-loader-core/src/types.ts +++ b/packages/p2p-media-loader-core/src/types.d.ts @@ -1,18 +1,25 @@ import { LinkedMap } from "./linked-map"; +import { HybridLoaderRequest } from "./request-container"; + +export type { EngineCallbacks } from "./request-container"; export type StreamType = "main" | "secondary"; export type ByteRange = { start: number; end: number }; -export type Segment = { +export type SegmentBase = { readonly localId: string; - readonly externalId: number; + readonly externalId: string; readonly url: string; readonly byteRange?: ByteRange; readonly startTime: number; readonly endTime: number; }; +export type Segment = SegmentBase & { + readonly stream: StreamWithSegments; +}; + export type Stream = { readonly localId: string; readonly type: StreamType; @@ -26,13 +33,16 @@ export type ReadonlyLinkedMap = Pick< export type StreamWithSegments< TStream extends Stream = Stream, - TMap extends ReadonlyLinkedMap = LinkedMap + TMap extends ReadonlyLinkedMap = LinkedMap< + string, + Segment + > > = TStream & { readonly segments: TMap; }; export type StreamWithReadonlySegments = - StreamWithSegments>; + StreamWithSegments>; export type SegmentResponse = { data: ArrayBuffer; @@ -40,10 +50,21 @@ export type SegmentResponse = { }; export type Settings = { - highDemandBufferLength: number; - httpBufferLength: number; - p2pBufferLength: number; + highDemandTimeWindow: number; + httpDownloadTimeWindow: number; + p2pDownloadTimeWindow: number; simultaneousHttpDownloads: number; + simultaneousP2PDownloads: number; cachedSegmentExpiration: number; cachedSegmentsCount: number; + webRtcMaxMessageSize: number; + p2pSegmentDownloadTimeout: number; + p2pLoaderDestroyTimeout: number; +}; + +export type CoreEventHandlers = { + onSegmentLoaded?: ( + byteLength: number, + type: HybridLoaderRequest["type"] + ) => void; }; diff --git a/packages/p2p-media-loader-core/src/utils.ts b/packages/p2p-media-loader-core/src/utils.ts deleted file mode 100644 index 66a56eff..00000000 --- a/packages/p2p-media-loader-core/src/utils.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Segment, Settings, Stream, StreamWithSegments } from "./index"; -import { - SegmentLoadStatus, - Playback, - LoadBufferRanges, - QueueItem, - NumberRange, -} from "./internal-types"; - -export function getStreamExternalId( - stream: Stream, - manifestResponseUrl: string -): string { - const { type, index } = stream; - return `${manifestResponseUrl}-${type}-${index}`; -} - -export function getSegmentFromStreamsMap( - streams: Map, - segmentId: string -): { segment: Segment; stream: StreamWithSegments } | undefined { - for (const stream of streams.values()) { - const segment = stream.segments.get(segmentId); - if (segment) return { segment, stream }; - } -} - -export function generateQueue({ - segment, - stream, - playback, - settings, - isSegmentLoaded, -}: { - stream: Readonly; - segment: Readonly; - playback: Readonly; - isSegmentLoaded: (segmentId: string) => boolean; - settings: Pick< - Settings, - "highDemandBufferLength" | "httpBufferLength" | "p2pBufferLength" - >; -}) { - const bufferRanges = getLoadBufferRanges(playback, settings); - const { localId: requestedSegmentId } = segment; - - const queue: QueueItem[] = []; - const queueSegmentIds = new Set(); - - const nextSegment = stream.segments.getNextTo(segment.localId)?.[1]; - const isNextSegmentHighDemand = !!( - nextSegment && - getSegmentLoadStatuses(nextSegment, bufferRanges)?.has("high-demand") - ); - - let i = 0; - for (const segment of stream.segments.values(requestedSegmentId)) { - const statuses = getSegmentLoadStatuses(segment, bufferRanges); - if (!statuses && !(i === 0 && isNextSegmentHighDemand)) break; - if (isSegmentLoaded(segment.localId)) continue; - - queueSegmentIds.add(segment.localId); - queue.push({ segment, statuses: statuses ?? new Set(["high-demand"]) }); - i++; - } - - return { queue, queueSegmentIds }; -} - -export function getLoadBufferRanges( - playback: Readonly, - settings: Pick< - Settings, - "highDemandBufferLength" | "httpBufferLength" | "p2pBufferLength" - > -): LoadBufferRanges { - const { position, rate } = playback; - const { highDemandBufferLength, httpBufferLength, p2pBufferLength } = - settings; - - const getRange = (position: number, rate: number, bufferLength: number) => { - return { - from: position, - to: position + rate * bufferLength, - }; - }; - return { - highDemand: getRange(position, rate, highDemandBufferLength), - http: getRange(position, rate, httpBufferLength), - p2p: getRange(position, rate, p2pBufferLength), - }; -} - -export function getSegmentLoadStatuses( - segment: Readonly, - loadBufferRanges: LoadBufferRanges -): Set | undefined { - const { highDemand, http, p2p } = loadBufferRanges; - const { startTime, endTime } = segment; - - const statuses = new Set(); - const isValueInRange = (value: number, range: NumberRange) => - value >= range.from && value < range.to; - - if ( - isValueInRange(startTime, highDemand) || - isValueInRange(endTime, highDemand) - ) { - statuses.add("high-demand"); - } - if (isValueInRange(startTime, http) || isValueInRange(endTime, http)) { - statuses.add("http-downloadable"); - } - if (isValueInRange(startTime, p2p) || isValueInRange(endTime, p2p)) { - statuses.add("p2p-downloadable"); - } - if (statuses.size) return statuses; -} - -export function isSegmentActual( - segment: Readonly, - bufferRanges: LoadBufferRanges -) { - const { startTime, endTime } = segment; - const { highDemand, p2p, http } = bufferRanges; - - const isInRange = (value: number) => { - return ( - value > highDemand.from && - (value < highDemand.to || value < http.to || value < p2p.to) - ); - }; - - return isInRange(startTime) || isInRange(endTime); -} diff --git a/packages/p2p-media-loader-core/src/utils/logger.ts b/packages/p2p-media-loader-core/src/utils/logger.ts new file mode 100644 index 00000000..c479f828 --- /dev/null +++ b/packages/p2p-media-loader-core/src/utils/logger.ts @@ -0,0 +1,26 @@ +import { Segment, Stream } from "../types"; +import { QueueItem, QueueItemStatuses } from "../internal-types"; + +export function getStreamString(stream: Stream) { + return `${stream.type}-${stream.index}`; +} + +export function getSegmentString(segment: Segment) { + const { externalId } = segment; + return `(${getStreamString(segment.stream)} | ${externalId})`; +} + +export function getStatusesString(statuses: QueueItemStatuses): string { + const { isHighDemand, isHttpDownloadable, isP2PDownloadable } = statuses; + if (isHighDemand) return "high-demand"; + if (isHttpDownloadable && isP2PDownloadable) return "http-p2p-window"; + if (isHttpDownloadable) return "http-window"; + if (isP2PDownloadable) return "p2p-window"; + return "-"; +} + +export function getQueueItemString(item: QueueItem) { + const { segment, statuses } = item; + const statusString = getStatusesString(statuses); + return `${segment.externalId} ${statusString}`; +} diff --git a/packages/p2p-media-loader-core/src/utils/peer.ts b/packages/p2p-media-loader-core/src/utils/peer.ts new file mode 100644 index 00000000..05c8cdd1 --- /dev/null +++ b/packages/p2p-media-loader-core/src/utils/peer.ts @@ -0,0 +1,84 @@ +import { JsonSegmentAnnouncement, PeerCommand } from "../internal-types"; +import { PeerCommandType, PeerSegmentStatus } from "../enums"; +import RIPEMD160 from "ripemd160"; + +const HASH_SYMBOLS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +const PEER_ID_LENGTH = 20; + +export function getStreamHash(streamId: string): string { + const symbolsCount = HASH_SYMBOLS.length; + const bytes = new RIPEMD160().update(streamId).digest(); + let hash = ""; + + for (const byte of bytes) { + hash += HASH_SYMBOLS[byte % symbolsCount]; + } + + return hash; +} + +export function generatePeerId(): string { + let peerId = "PEER:"; + const randomCharsAmount = PEER_ID_LENGTH - peerId.length; + + for (let i = 0; i < randomCharsAmount; i++) { + peerId += HASH_SYMBOLS.charAt( + Math.floor(Math.random() * HASH_SYMBOLS.length) + ); + } + + return peerId; +} + +export function getPeerCommandFromArrayBuffer( + data: ArrayBuffer +): PeerCommand | undefined { + const bytes = new Uint8Array(data); + + // Serialized JSON string check by first, second and last characters: '{" .... }' + if ( + bytes[0] === 123 && + bytes[1] === 34 && + bytes[data.byteLength - 1] === 125 + ) { + try { + const decoded = new TextDecoder().decode(data); + const parsed = JSON.parse(decoded) as object; + if (isPeerCommand(parsed)) return parsed; + } catch { + return undefined; + } + } +} + +export function getSegmentsFromPeerAnnouncement( + announcement: JsonSegmentAnnouncement +): Map { + const segmentStatusMap = new Map(); + announcement.l + .split("|") + .forEach((id) => segmentStatusMap.set(id, PeerSegmentStatus.Loaded)); + + announcement.p + .split("|") + .forEach((id) => segmentStatusMap.set(id, PeerSegmentStatus.LoadingByHttp)); + return segmentStatusMap; +} + +export function getJsonSegmentsAnnouncement( + loadedSegmentExternalIds: string[], + loadingByHttpSegmentExternalIds: string[] +): JsonSegmentAnnouncement { + return { + l: loadedSegmentExternalIds.join("|"), + p: loadingByHttpSegmentExternalIds.join("|"), + }; +} + +function isPeerCommand(command: object): command is PeerCommand { + return ( + (command as PeerCommand).c !== undefined && + Object.values(PeerCommandType).includes((command as PeerCommand).c) + ); +} diff --git a/packages/p2p-media-loader-core/src/utils/queue.ts b/packages/p2p-media-loader-core/src/utils/queue.ts new file mode 100644 index 00000000..9b629596 --- /dev/null +++ b/packages/p2p-media-loader-core/src/utils/queue.ts @@ -0,0 +1,122 @@ +import { Segment, Settings } from "../types"; +import { + LoadBufferRanges, + NumberRange, + Playback, + QueueItem, + QueueItemStatuses, +} from "../internal-types"; + +export function generateQueue({ + lastRequestedSegment, + playback, + settings, + skipSegment, +}: { + lastRequestedSegment: Readonly; + playback: Readonly; + skipSegment: (segment: Segment, statuses: QueueItemStatuses) => boolean; + settings: Pick< + Settings, + "highDemandTimeWindow" | "httpDownloadTimeWindow" | "p2pDownloadTimeWindow" + >; +}): { queue: QueueItem[]; queueSegmentIds: Set } { + const bufferRanges = getLoadBufferRanges(playback, settings); + const { localId: requestedSegmentId, stream } = lastRequestedSegment; + + const queue: QueueItem[] = []; + const queueSegmentIds = new Set(); + + const { segments } = stream; + const isNextNotActual = (segmentId: string) => { + const next = segments.getNextTo(segmentId)?.[1]; + if (!next) return true; + const statuses = getSegmentLoadStatuses(next, bufferRanges); + return isNotActualStatuses(statuses); + }; + + let i = 0; + for (const segment of segments.values(requestedSegmentId)) { + const statuses = getSegmentLoadStatuses(segment, bufferRanges); + const isNotActual = isNotActualStatuses(statuses); + if (isNotActual && (i !== 0 || isNextNotActual(requestedSegmentId))) break; + i++; + if (skipSegment(segment, statuses)) continue; + + if (isNotActual) statuses.isHighDemand = true; + queue.push({ segment, statuses }); + queueSegmentIds.add(segment.localId); + } + + return { queue, queueSegmentIds }; +} + +export function getLoadBufferRanges( + playback: Readonly, + settings: Pick< + Settings, + "highDemandTimeWindow" | "httpDownloadTimeWindow" | "p2pDownloadTimeWindow" + > +): LoadBufferRanges { + const { position, rate } = playback; + const { + highDemandTimeWindow, + httpDownloadTimeWindow, + p2pDownloadTimeWindow, + } = settings; + + const getRange = (position: number, rate: number, bufferLength: number) => { + return { + from: position, + to: position + rate * bufferLength, + }; + }; + return { + highDemand: getRange(position, rate, highDemandTimeWindow), + http: getRange(position, rate, httpDownloadTimeWindow), + p2p: getRange(position, rate, p2pDownloadTimeWindow), + }; +} + +export function getSegmentLoadStatuses( + segment: Readonly, + loadBufferRanges: LoadBufferRanges +): QueueItemStatuses { + const { highDemand, http, p2p } = loadBufferRanges; + const { startTime, endTime } = segment; + + const isValueInRange = (value: number, range: NumberRange) => + value >= range.from && value < range.to; + + return { + isHighDemand: + isValueInRange(startTime, highDemand) || + isValueInRange(endTime, highDemand), + isHttpDownloadable: + isValueInRange(startTime, http) || isValueInRange(endTime, http), + isP2PDownloadable: + isValueInRange(startTime, p2p) || isValueInRange(endTime, p2p), + }; +} + +function isNotActualStatuses(statuses: QueueItemStatuses) { + const { isHighDemand, isHttpDownloadable, isP2PDownloadable } = statuses; + return !isHighDemand && !isHttpDownloadable && !isP2PDownloadable; +} + +export function isSegmentActual( + segment: Readonly, + bufferRanges: LoadBufferRanges +) { + const { startTime, endTime } = segment; + const { highDemand, p2p, http } = bufferRanges; + + const isInRange = (value: number) => { + return ( + value > highDemand.from && + (value < highDemand.to || value < http.to || value < p2p.to) + ); + }; + + return isInRange(startTime) || isInRange(endTime); +} diff --git a/packages/p2p-media-loader-core/src/utils/utils.ts b/packages/p2p-media-loader-core/src/utils/utils.ts new file mode 100644 index 00000000..d9fb72fd --- /dev/null +++ b/packages/p2p-media-loader-core/src/utils/utils.ts @@ -0,0 +1,47 @@ +import { Segment, Stream, StreamWithSegments } from "../index"; + +const PEER_PROTOCOL_VERSION = "V1"; + +export function getStreamExternalId( + manifestResponseUrl: string, + stream: Readonly +): string { + const { type, index } = stream; + return `${PEER_PROTOCOL_VERSION}:${manifestResponseUrl}-${type}-${index}`; +} + +export function getSegmentFromStreamsMap( + streams: Map, + segmentId: string +): Segment | undefined { + for (const stream of streams.values()) { + const segment = stream.segments.get(segmentId); + if (segment) return segment; + } +} + +export function getSegmentFromStreamByExternalId( + stream: StreamWithSegments, + segmentExternalId: string +): Segment | undefined { + for (const segment of stream.segments.values()) { + if (segment.externalId === segmentExternalId) return segment; + } +} + +export function getControlledPromise() { + let resolve: (value: T) => void; + let reject: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + return { + promise, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + resolve: resolve!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + reject: reject!, + }; +} diff --git a/packages/p2p-media-loader-core/tsconfig.json b/packages/p2p-media-loader-core/tsconfig.json index c5e79e55..df1def43 100644 --- a/packages/p2p-media-loader-core/tsconfig.json +++ b/packages/p2p-media-loader-core/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "outDir": "lib", "rootDir": "src", - "tsBuildInfoFile": "./build/.tsbuildinfo" + "tsBuildInfoFile": "./build/.tsbuildinfo", }, "include": ["src/**/*"] } diff --git a/packages/p2p-media-loader-hlsjs/src/engine.ts b/packages/p2p-media-loader-hlsjs/src/engine.ts index 4f620135..c918ae21 100644 --- a/packages/p2p-media-loader-hlsjs/src/engine.ts +++ b/packages/p2p-media-loader-hlsjs/src/engine.ts @@ -2,7 +2,7 @@ import type Hls from "hls.js"; import type { HlsConfig, Events } from "hls.js"; import { FragmentLoaderBase } from "./fragment-loader"; import { SegmentManager } from "./segment-mananger"; -import { Core } from "p2p-media-loader-core"; +import { Core, CoreEventHandlers } from "p2p-media-loader-core"; import Debug from "debug"; export class Engine { @@ -10,8 +10,8 @@ export class Engine { private readonly segmentManager: SegmentManager; private debugDestroying = Debug("hls:destroying"); - constructor() { - this.core = new Core(); + constructor(eventHandlers?: CoreEventHandlers) { + this.core = new Core(eventHandlers); this.segmentManager = new SegmentManager(this.core); } @@ -61,17 +61,14 @@ export class Engine { hls.on("hlsMediaAttached" as Events.MEDIA_ATTACHED, (event, data) => { const { media } = data; media.addEventListener("timeupdate", () => { - // console.log("playhead time: ", media.currentTime); this.core.updatePlayback(media.currentTime, media.playbackRate); }); media.addEventListener("seeking", () => { - // console.log("playhead time: ", media.currentTime); this.core.updatePlayback(media.currentTime, media.playbackRate); }); media.addEventListener("ratechange", () => { - // console.log("playback rate: ", media.playbackRate); this.core.updatePlayback(media.currentTime, media.playbackRate); }); }); diff --git a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts index af8d2f38..8ed41e65 100644 --- a/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts +++ b/packages/p2p-media-loader-hlsjs/src/fragment-loader.ts @@ -8,7 +8,12 @@ import type { LoaderStats, } from "hls.js"; import * as Utils from "./utils"; -import { Core, FetchError, SegmentResponse } from "p2p-media-loader-core"; +import { + RequestAbortError, + Core, + FetchError, + SegmentResponse, +} from "p2p-media-loader-core"; const DEFAULT_DOWNLOAD_LATENCY = 10; @@ -65,28 +70,30 @@ export class FragmentLoaderBase implements Loader { return; } - try { - this.response = await this.core.loadSegment(this.segmentId); - } catch (error) { - if (this.stats.aborted) return; - return this.handleError(error); - } - if (!this.response) return; - const loadedBytes = this.response.data.byteLength; - - stats.loading = getLoadingStat({ - targetBitrate: this.response.bandwidth, - loadingEndTime: performance.now(), - loadedBytes, - }); - stats.total = stats.loaded = loadedBytes; - - callbacks.onSuccess( - { data: this.response.data, url: context.url }, - this.stats, - context, - this.response - ); + const onSuccess = (response: SegmentResponse) => { + this.response = response; + const loadedBytes = this.response.data.byteLength; + stats.loading = getLoadingStat({ + targetBitrate: this.response.bandwidth, + loadingEndTime: performance.now(), + loadedBytes, + }); + stats.total = stats.loaded = loadedBytes; + + callbacks.onSuccess( + { data: this.response.data, url: context.url }, + this.stats, + context, + this.response + ); + }; + + const onError = (error: unknown) => { + if (error instanceof RequestAbortError && this.stats.aborted) return; + this.handleError(error); + }; + + void this.core.loadSegment(this.segmentId, { onSuccess, onError }); } private handleError(thrownError: unknown) { @@ -104,8 +111,8 @@ export class FragmentLoaderBase implements Loader { private abortInternal() { if (!this.response && this.segmentId) { - this.core.abortSegmentLoading(this.segmentId); this.stats.aborted = true; + this.core.abortSegmentLoading(this.segmentId); } } @@ -122,7 +129,7 @@ export class FragmentLoaderBase implements Loader { if (this.defaultLoader) { this.defaultLoader.destroy(); } else { - this.abortInternal(); + if (!this.stats.aborted) this.abortInternal(); this.callbacks = null; this.config = null; } diff --git a/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts b/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts index 944ca1f9..f2732a17 100644 --- a/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts +++ b/packages/p2p-media-loader-hlsjs/src/segment-mananger.ts @@ -4,7 +4,7 @@ import type { LevelUpdatedData, AudioTrackLoadedData, } from "hls.js"; -import { Core, Segment } from "p2p-media-loader-core"; +import { Core, SegmentBase } from "p2p-media-loader-core"; export class SegmentManager { core: Core; @@ -43,7 +43,7 @@ export class SegmentManager { if (!playlist) return; const segmentToRemoveIds = new Set(playlist.segments.keys()); - const newSegments: Segment[] = []; + const newSegments: SegmentBase[] = []; fragments.forEach((fragment, index) => { const { url: responseUrl, @@ -66,13 +66,14 @@ export class SegmentManager { newSegments.push({ localId: segmentLocalId, url: responseUrl, - externalId: live ? sn : index, + externalId: live ? sn.toString() : index.toString(), byteRange, startTime, endTime, }); }); + if (!newSegments.length && !segmentToRemoveIds.size) return; this.core.updateStream(url, newSegments, [...segmentToRemoveIds]); } } diff --git a/packages/p2p-media-loader-shaka/src/engine.ts b/packages/p2p-media-loader-shaka/src/engine.ts index 364c642d..8ccb40d4 100644 --- a/packages/p2p-media-loader-shaka/src/engine.ts +++ b/packages/p2p-media-loader-shaka/src/engine.ts @@ -15,20 +15,19 @@ import { } from "./types"; import { LoadingHandler } from "./loading-handler"; import { decorateMethod } from "./utils"; -import { Core } from "p2p-media-loader-core"; +import { Core, CoreEventHandlers } from "p2p-media-loader-core"; export class Engine { private readonly shaka: Shaka; private readonly streamInfo: StreamInfo = {}; - private readonly core = new Core(); - private readonly segmentManager = new SegmentManager( - this.streamInfo, - this.core - ); + private readonly core: Core; + private readonly segmentManager: SegmentManager; private debugDestroying = Debug("shaka:destroying"); - constructor(shaka?: unknown) { + constructor(shaka?: unknown, eventHandlers?: CoreEventHandlers) { this.shaka = (shaka as Shaka | undefined) ?? window.shaka; + this.core = new Core(eventHandlers); + this.segmentManager = new SegmentManager(this.streamInfo, this.core); } initShakaPlayer(player: shaka.Player) { diff --git a/packages/p2p-media-loader-shaka/src/loading-handler.ts b/packages/p2p-media-loader-shaka/src/loading-handler.ts index 605992ef..99773cfd 100644 --- a/packages/p2p-media-loader-shaka/src/loading-handler.ts +++ b/packages/p2p-media-loader-shaka/src/loading-handler.ts @@ -2,7 +2,7 @@ import * as Utils from "./stream-utils"; import { SegmentManager } from "./segment-manager"; import { StreamInfo } from "./types"; import { Shaka, Stream } from "./types"; -import { Core } from "p2p-media-loader-core"; +import { Core, EngineCallbacks, SegmentResponse } from "p2p-media-loader-core"; interface LoadingHandlerInterface { handleLoading: shaka.extern.SchemePlugin; @@ -69,9 +69,9 @@ export class LoadingHandler implements LoadingHandlerInterface { if (!this.core.hasSegment(segmentId)) return this.defaultLoad(); const loadSegment = async (): Promise => { - const response = await this.core.loadSegment(segmentId); - - const { data, bandwidth } = response; + const { request, callbacks } = getSegmentRequest(); + await this.core.loadSegment(segmentId, callbacks); + const { data, bandwidth } = await request; return { data, headers: {}, @@ -99,3 +99,25 @@ function getLoadingDurationBasedOnBandwidth( const bits = bytesLoaded * 8; return Math.round(bits / bandwidth) * 1000; } + +function getSegmentRequest(): { + callbacks: EngineCallbacks; + request: Promise; +} { + let onSuccess: (value: SegmentResponse) => void; + let onError: (reason?: unknown) => void; + const request = new Promise((resolve, reject) => { + onSuccess = resolve; + onError = reject; + }); + + return { + request, + callbacks: { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + onSuccess: onSuccess!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + onError: onError!, + }, + }; +} diff --git a/packages/p2p-media-loader-shaka/src/manifest-parser-decorator.ts b/packages/p2p-media-loader-shaka/src/manifest-parser-decorator.ts index ed054583..636b6ea8 100644 --- a/packages/p2p-media-loader-shaka/src/manifest-parser-decorator.ts +++ b/packages/p2p-media-loader-shaka/src/manifest-parser-decorator.ts @@ -97,7 +97,7 @@ export class ManifestParserDecorator implements shaka.extern.ManifestParser { processStream(video, "main", videoCount++); } if (audio && !processedStreams.has(audio.id)) { - processStream(audio, !video ? "secondary" : "main", audioCount++); + processStream(audio, !video ? "main" : "secondary", audioCount++); } } } diff --git a/packages/p2p-media-loader-shaka/src/segment-manager.ts b/packages/p2p-media-loader-shaka/src/segment-manager.ts index b99258d8..856610eb 100644 --- a/packages/p2p-media-loader-shaka/src/segment-manager.ts +++ b/packages/p2p-media-loader-shaka/src/segment-manager.ts @@ -3,7 +3,7 @@ import { HookedStream, StreamInfo, Stream } from "./types"; import { Core, StreamWithReadonlySegments, - Segment, + SegmentBase, StreamType, } from "p2p-media-loader-core"; @@ -61,9 +61,9 @@ export class SegmentManager { segmentReferences: shaka.media.SegmentReference[] ) { const staleSegmentsIds = new Set(managerStream.segments.keys()); - const newSegments: Segment[] = []; + const newSegments: SegmentBase[] = []; for (const reference of segmentReferences) { - const externalId = reference.getStartTime(); + const externalId = (+reference.getStartTime().toFixed(3)).toString(); const segmentLocalId = Utils.getSegmentLocalIdFromReference(reference); if (!managerStream.segments.has(segmentLocalId)) { @@ -89,7 +89,7 @@ export class SegmentManager { const segments = [...managerStream.segments.values()]; const lastMediaSequence = Utils.getStreamLastMediaSequence(managerStream); - const newSegments: Segment[] = []; + const newSegments: SegmentBase[] = []; if (segments.length === 0) { const firstReferenceMediaSequence = lastMediaSequence === undefined @@ -98,7 +98,7 @@ export class SegmentManager { segmentReferences.forEach((reference, index) => { const segment = Utils.createSegment({ segmentReference: reference, - externalId: firstReferenceMediaSequence + index, + externalId: (firstReferenceMediaSequence + index).toString(), }); newSegments.push(segment); }); @@ -116,7 +116,7 @@ export class SegmentManager { const segment = Utils.createSegment({ localId, segmentReference: reference, - externalId: index, + externalId: index.toString(), }); newSegments.push(segment); index--; diff --git a/packages/p2p-media-loader-shaka/src/stream-utils.ts b/packages/p2p-media-loader-shaka/src/stream-utils.ts index cc5a2897..efd69db3 100644 --- a/packages/p2p-media-loader-shaka/src/stream-utils.ts +++ b/packages/p2p-media-loader-shaka/src/stream-utils.ts @@ -1,6 +1,6 @@ import { HookedStream, Stream } from "./types"; import { - Segment, + SegmentBase, StreamWithReadonlySegments, ByteRange, } from "p2p-media-loader-core"; @@ -11,9 +11,9 @@ export function createSegment({ localId, }: { segmentReference: shaka.media.SegmentReference; - externalId: number; + externalId: string; localId?: string; -}): Segment { +}): SegmentBase { const { byteRange, url, startTime, endTime } = getSegmentInfoFromReference(segmentReference); return { diff --git a/packages/tsconfig.base.json b/packages/tsconfig.base.json index 54a1580d..cc86eadc 100644 --- a/packages/tsconfig.base.json +++ b/packages/tsconfig.base.json @@ -5,7 +5,7 @@ "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, - "moduleResolution": "Node", + "moduleResolution": "Bundler", "allowImportingTsExtensions": false, "resolveJsonModule": false, "isolatedModules": true, @@ -20,6 +20,7 @@ "noUnusedLocals": false, "noUnusedParameters": false, "noFallthroughCasesInSwitch": true, - "composite": true - } + "composite": true, + "allowSyntheticDefaultImports": true + }, } diff --git a/patches/bittorrent-tracker@10.0.12.patch b/patches/bittorrent-tracker@10.0.12.patch new file mode 100644 index 00000000..c8507428 --- /dev/null +++ b/patches/bittorrent-tracker@10.0.12.patch @@ -0,0 +1,138 @@ +diff --git a/.idea/.gitignore b/.idea/.gitignore +new file mode 100644 +index 0000000000000000000000000000000000000000..b58b603fea78041071d125a30db58d79b3d49217 +--- /dev/null ++++ b/.idea/.gitignore +@@ -0,0 +1,5 @@ ++# Default ignored files ++/shelf/ ++/workspace.xml ++# Editor-based HTTP Client requests ++/httpRequests/ +diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml +new file mode 100644 +index 0000000000000000000000000000000000000000..54ecbb2b9134d6a566fcd38f6f3c390218f5498b +--- /dev/null ++++ b/.idea/codeStyles/Project.xml +@@ -0,0 +1,44 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml +new file mode 100644 +index 0000000000000000000000000000000000000000..79ee123c2b23e069e35ed634d687e17f731cc702 +--- /dev/null ++++ b/.idea/codeStyles/codeStyleConfig.xml +@@ -0,0 +1,5 @@ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/.idea/e50e68725add4f8d6518bfa3a5479dee.iml b/.idea/e50e68725add4f8d6518bfa3a5479dee.iml +new file mode 100644 +index 0000000000000000000000000000000000000000..ebbda94b7d0f898ddad4c9aa59459fc21676874c +--- /dev/null ++++ b/.idea/e50e68725add4f8d6518bfa3a5479dee.iml +@@ -0,0 +1,15 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml +new file mode 100644 +index 0000000000000000000000000000000000000000..0be03f464545c7fdfc45458031b80a2b1b9bff17 +--- /dev/null ++++ b/.idea/inspectionProfiles/Project_Default.xml +@@ -0,0 +1,6 @@ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/.idea/modules.xml b/.idea/modules.xml +new file mode 100644 +index 0000000000000000000000000000000000000000..1481ef72f8fc054af042eea2e463400b7aabea70 +--- /dev/null ++++ b/.idea/modules.xml +@@ -0,0 +1,8 @@ ++ ++ ++ ++ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/package.json b/package.json +index 9d537a8a1dd93687f4535bf2882b7b80139ec594..fe85f2f94b6d98f98ac926223ad077368ab80561 100644 +--- a/package.json ++++ b/package.json +@@ -12,9 +12,6 @@ + }, + "browser": { + "./lib/common-node.js": false, +- "./lib/client/http-tracker.js": false, +- "./lib/client/udp-tracker.js": false, +- "./server.js": false, + "socks": false + }, + "chromeapp": { \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f0a4b610..48fd3510 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,14 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +patchedDependencies: + bittorrent-tracker@10.0.12: + hash: 3bacck7ok4ioq2ztv47aeh7t7e + path: patches/bittorrent-tracker@10.0.12.patch + importers: .: @@ -74,15 +83,29 @@ importers: version: 18.2.4 '@vitejs/plugin-react': specifier: ^4.0.0 - version: 4.0.0(vite@4.3.2) + version: 4.0.0(vite@4.5.0) eslint-plugin-react-hooks: specifier: ^4.6.0 - version: 4.6.0(eslint@8.39.0) + version: 4.6.0(eslint@8.52.0) eslint-plugin-react-refresh: specifier: ^0.4.1 - version: 0.4.1(eslint@8.39.0) - - packages/p2p-media-loader-core: {} + version: 0.4.1(eslint@8.52.0) + vite-plugin-node-polyfills: + specifier: ^0.14.1 + version: 0.14.1(vite@4.5.0) + + packages/p2p-media-loader-core: + dependencies: + bittorrent-tracker: + specifier: 10.0.12 + version: 10.0.12(patch_hash=3bacck7ok4ioq2ztv47aeh7t7e) + ripemd160: + specifier: ^2.0.2 + version: 2.0.2 + devDependencies: + '@types/ripemd160': + specifier: ^2.0.2 + version: 2.0.2 packages/p2p-media-loader-hlsjs: dependencies: @@ -104,6 +127,11 @@ importers: packages: + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + /@ampproject/remapping@2.2.1: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} engines: {node: '>=6.0.0'} @@ -348,6 +376,15 @@ packages: dev: true optional: true + /@esbuild/android-arm64@0.18.20: + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-arm@0.17.19: resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} engines: {node: '>=12'} @@ -357,6 +394,15 @@ packages: dev: true optional: true + /@esbuild/android-arm@0.18.20: + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/android-x64@0.17.19: resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} engines: {node: '>=12'} @@ -366,6 +412,15 @@ packages: dev: true optional: true + /@esbuild/android-x64@0.18.20: + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-arm64@0.17.19: resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} engines: {node: '>=12'} @@ -375,6 +430,15 @@ packages: dev: true optional: true + /@esbuild/darwin-arm64@0.18.20: + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/darwin-x64@0.17.19: resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} engines: {node: '>=12'} @@ -384,6 +448,15 @@ packages: dev: true optional: true + /@esbuild/darwin-x64@0.18.20: + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-arm64@0.17.19: resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} engines: {node: '>=12'} @@ -393,6 +466,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-arm64@0.18.20: + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/freebsd-x64@0.17.19: resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} engines: {node: '>=12'} @@ -402,6 +484,15 @@ packages: dev: true optional: true + /@esbuild/freebsd-x64@0.18.20: + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm64@0.17.19: resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} engines: {node: '>=12'} @@ -411,6 +502,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm64@0.18.20: + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-arm@0.17.19: resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} engines: {node: '>=12'} @@ -420,6 +520,15 @@ packages: dev: true optional: true + /@esbuild/linux-arm@0.18.20: + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ia32@0.17.19: resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} engines: {node: '>=12'} @@ -429,6 +538,15 @@ packages: dev: true optional: true + /@esbuild/linux-ia32@0.18.20: + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-loong64@0.17.19: resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} engines: {node: '>=12'} @@ -438,6 +556,15 @@ packages: dev: true optional: true + /@esbuild/linux-loong64@0.18.20: + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-mips64el@0.17.19: resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} engines: {node: '>=12'} @@ -447,6 +574,15 @@ packages: dev: true optional: true + /@esbuild/linux-mips64el@0.18.20: + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-ppc64@0.17.19: resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} engines: {node: '>=12'} @@ -456,6 +592,15 @@ packages: dev: true optional: true + /@esbuild/linux-ppc64@0.18.20: + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-riscv64@0.17.19: resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} engines: {node: '>=12'} @@ -465,6 +610,15 @@ packages: dev: true optional: true + /@esbuild/linux-riscv64@0.18.20: + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-s390x@0.17.19: resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} engines: {node: '>=12'} @@ -474,6 +628,15 @@ packages: dev: true optional: true + /@esbuild/linux-s390x@0.18.20: + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/linux-x64@0.17.19: resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} engines: {node: '>=12'} @@ -483,6 +646,15 @@ packages: dev: true optional: true + /@esbuild/linux-x64@0.18.20: + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + /@esbuild/netbsd-x64@0.17.19: resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} engines: {node: '>=12'} @@ -492,6 +664,15 @@ packages: dev: true optional: true + /@esbuild/netbsd-x64@0.18.20: + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/openbsd-x64@0.17.19: resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} engines: {node: '>=12'} @@ -501,6 +682,15 @@ packages: dev: true optional: true + /@esbuild/openbsd-x64@0.18.20: + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + /@esbuild/sunos-x64@0.17.19: resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} engines: {node: '>=12'} @@ -510,6 +700,15 @@ packages: dev: true optional: true + /@esbuild/sunos-x64@0.18.20: + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-arm64@0.17.19: resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} engines: {node: '>=12'} @@ -519,6 +718,15 @@ packages: dev: true optional: true + /@esbuild/win32-arm64@0.18.20: + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-ia32@0.17.19: resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} engines: {node: '>=12'} @@ -528,6 +736,15 @@ packages: dev: true optional: true + /@esbuild/win32-ia32@0.18.20: + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@esbuild/win32-x64@0.17.19: resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} engines: {node: '>=12'} @@ -537,6 +754,15 @@ packages: dev: true optional: true + /@esbuild/win32-x64@0.18.20: + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.39.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -547,11 +773,26 @@ packages: eslint-visitor-keys: 3.4.1 dev: true + /@eslint-community/eslint-utils@4.4.0(eslint@8.52.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.52.0 + eslint-visitor-keys: 3.4.1 + dev: true + /@eslint-community/regexpp@4.5.1: resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true + /@eslint-community/regexpp@4.9.1: + resolution: {integrity: sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + /@eslint/eslintrc@2.0.3: resolution: {integrity: sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -569,11 +810,33 @@ packages: - supports-color dev: true + /@eslint/eslintrc@2.1.2: + resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.23.0 + ignore: 5.2.4 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + /@eslint/js@8.39.0: resolution: {integrity: sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /@eslint/js@8.52.0: + resolution: {integrity: sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@humanwhocodes/config-array@0.11.10: resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==} engines: {node: '>=10.10.0'} @@ -585,6 +848,17 @@ packages: - supports-color dev: true + /@humanwhocodes/config-array@0.11.13: + resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + /@humanwhocodes/module-importer@1.0.1: resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} @@ -594,6 +868,10 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@humanwhocodes/object-schema@2.0.1: + resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + dev: true + /@isaacs/cliui@8.0.2: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -668,6 +946,61 @@ packages: dev: true optional: true + /@rollup/plugin-inject@5.0.3: + resolution: {integrity: sha512-411QlbL+z2yXpRWFXSmw/teQRMkXcAAC8aYTemc15gwJRpvEVDQwoe+N/HTFD8RFG8+88Bme9DK2V9CVm7hJdA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.0.4 + estree-walker: 2.0.2 + magic-string: 0.27.0 + dev: true + + /@rollup/pluginutils@5.0.4: + resolution: {integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.2 + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + + /@thaunknown/simple-peer@9.12.1: + resolution: {integrity: sha512-IS5BXvXx7cvBAzaxqotJf4s4rJCPk5JABLK6Gbnn7oAmWVcH4hYABabBBrvvJtv/xyUqR4v/H3LalnGRJJfEog==} + dependencies: + debug: 4.3.4 + err-code: 3.0.1 + get-browser-rtc: 1.1.0 + queue-microtask: 1.2.3 + streamx: 2.15.1 + uint8-util: 2.2.4 + transitivePeerDependencies: + - supports-color + dev: false + + /@thaunknown/simple-websocket@9.1.1(bufferutil@4.0.8)(utf-8-validate@5.0.10): + resolution: {integrity: sha512-vzQloFWRodRZqZhpxMpBljFtISesY8TihA8T5uKwCYdj2I1ImMhE/gAeTCPsCGOtxJfGKu3hw/is6MXauWLjOg==} + dependencies: + debug: 4.3.4 + queue-microtask: 1.2.3 + streamx: 2.15.1 + uint8-util: 2.2.4 + ws: 8.14.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /@types/debug@4.1.8: resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==} dependencies: @@ -678,6 +1011,10 @@ packages: resolution: {integrity: sha512-bkTVZkK3Vi7N7eX2FUBnqKhCjTaeRLkhvY8H6zolatbSTtjPPdxyUzhE3C29sIBYRRq1kQHSduFgCHKg5VF3Jw==} dev: true + /@types/estree@1.0.2: + resolution: {integrity: sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==} + dev: true + /@types/json-schema@7.0.12: resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} dev: true @@ -686,6 +1023,12 @@ packages: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: true + /@types/node@20.8.8: + resolution: {integrity: sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ==} + dependencies: + undici-types: 5.25.3 + dev: true + /@types/prop-types@15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} dev: true @@ -704,6 +1047,12 @@ packages: csstype: 3.1.2 dev: true + /@types/ripemd160@2.0.2: + resolution: {integrity: sha512-hv3Oh/+ldCqp1xBRGi/1G6y2fxV6wUiiwjPt2Q7fe4UgmbD52ChdmxJyjDsGCRb9yuTBS9281UhH4D9gO85k7A==} + dependencies: + '@types/node': 20.8.8 + dev: true + /@types/scheduler@0.16.3: resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} dev: true @@ -842,7 +1191,11 @@ packages: eslint-visitor-keys: 3.4.1 dev: true - /@vitejs/plugin-react@4.0.0(vite@4.3.2): + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + + /@vitejs/plugin-react@4.0.0(vite@4.5.0): resolution: {integrity: sha512-HX0XzMjL3hhOYm+0s95pb0Z7F8O81G7joUHgfDd/9J/ZZf5k4xX6QAMFkKsHFxaHlf6X7GD7+XuaZ66ULiJuhQ==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -852,11 +1205,19 @@ packages: '@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.22.5) '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.22.5) react-refresh: 0.14.0 - vite: 4.3.2 + vite: 4.5.0 transitivePeerDependencies: - supports-color dev: true + /acorn-jsx@5.3.2(acorn@8.10.0): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.10.0 + dev: true + /acorn-jsx@5.3.2(acorn@8.8.2): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -865,12 +1226,23 @@ packages: acorn: 8.8.2 dev: true + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + /acorn@8.8.2: resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} engines: {node: '>=0.4.0'} hasBin: true dev: true + /addr-to-ip-port@2.0.0: + resolution: {integrity: sha512-9bYbtjamtdLHZSqVIUXhilOryNPiL+x+Q5J/Unpg4VY3ZIkK3fT52UoErj1NdUeVm3J1t2iBEAur4Ywbl/bahw==} + engines: {node: '>=12.20.0'} + dev: false + /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: @@ -918,10 +1290,34 @@ packages: engines: {node: '>=8'} dev: true + /asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + dependencies: + bn.js: 4.12.0 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + dev: true + + /assert@2.1.0: + resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} + dependencies: + call-bind: 1.0.2 + is-nan: 1.3.2 + object-is: 1.1.5 + object.assign: 4.1.4 + util: 0.12.5 + dev: true + /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} dev: false + /available-typed-arrays@1.0.5: + resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} + engines: {node: '>= 0.4'} + dev: true + /axios@1.2.3(debug@4.3.4): resolution: {integrity: sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw==} dependencies: @@ -940,6 +1336,69 @@ packages: resolution: {integrity: sha512-urXwkHgwp6GsXVF+it01485Z2Cj4pnW02ICnM0TemOlkKmCNnDLmyy+ZZiRXBpwldUXO+aRNr7Hdia4CBvXJ5A==} dev: false + /base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + dev: false + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /bencode@4.0.0: + resolution: {integrity: sha512-AERXw18df0pF3ziGOCyUjqKZBVNH8HV3lBxnx5w0qtgMIk4a1wb9BkcCQbkp9Zstfrn/dzRwl7MmUHHocX3sRQ==} + engines: {node: '>=12.20.0'} + dependencies: + uint8-util: 2.2.4 + dev: false + + /bittorrent-peerid@1.3.6: + resolution: {integrity: sha512-VyLcUjVMEOdSpHaCG/7odvCdLbAB1y3l9A2V6WIje24uV7FkJPrQrH/RrlFmKxP89pFVDEnE+YlHaFujlFIZsg==} + dev: false + + /bittorrent-tracker@10.0.12(patch_hash=3bacck7ok4ioq2ztv47aeh7t7e): + resolution: {integrity: sha512-EYQEwhOYkrRiiwkCFcM9pbzJInsAe7UVmUgevW133duwlZzjwf5ABwDE7pkkmNRS6iwN0b8LbI/94q16dYqiow==} + engines: {node: '>=12.20.0'} + hasBin: true + dependencies: + '@thaunknown/simple-peer': 9.12.1 + '@thaunknown/simple-websocket': 9.1.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + bencode: 4.0.0 + bittorrent-peerid: 1.3.6 + chrome-dgram: 3.0.6 + clone: 2.1.2 + compact2string: 1.4.1 + debug: 4.3.4 + ip: 1.1.8 + lru: 3.1.0 + minimist: 1.2.8 + once: 1.4.0 + queue-microtask: 1.2.3 + random-iterate: 1.0.1 + run-parallel: 1.2.0 + run-series: 1.1.9 + simple-get: 4.0.1 + socks: 2.7.1 + string2compact: 2.0.1 + uint8-util: 2.2.4 + unordered-array-remove: 1.0.2 + ws: 8.14.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 + transitivePeerDependencies: + - supports-color + dev: false + patched: true + + /bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + dev: true + + /bn.js@5.2.1: + resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} + dev: true + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -960,6 +1419,71 @@ packages: fill-range: 7.0.1 dev: true + /brorand@1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + dev: true + + /browser-resolve@2.0.0: + resolution: {integrity: sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==} + dependencies: + resolve: 1.22.6 + dev: true + + /browserify-aes@1.2.0: + resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} + dependencies: + buffer-xor: 1.0.3 + cipher-base: 1.0.4 + create-hash: 1.2.0 + evp_bytestokey: 1.0.3 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /browserify-cipher@1.0.1: + resolution: {integrity: sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==} + dependencies: + browserify-aes: 1.2.0 + browserify-des: 1.0.2 + evp_bytestokey: 1.0.3 + dev: true + + /browserify-des@1.0.2: + resolution: {integrity: sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==} + dependencies: + cipher-base: 1.0.4 + des.js: 1.1.0 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /browserify-rsa@4.1.0: + resolution: {integrity: sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==} + dependencies: + bn.js: 5.2.1 + randombytes: 2.1.0 + dev: true + + /browserify-sign@4.2.1: + resolution: {integrity: sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==} + dependencies: + bn.js: 5.2.1 + browserify-rsa: 4.1.0 + create-hash: 1.2.0 + create-hmac: 1.1.7 + elliptic: 6.5.4 + inherits: 2.0.4 + parse-asn1: 5.1.6 + readable-stream: 3.6.2 + safe-buffer: 5.2.1 + dev: true + + /browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + dependencies: + pako: 1.0.11 + dev: true + /browserslist@4.21.8: resolution: {integrity: sha512-j+7xYe+v+q2Id9qbBeCI8WX5NmZSRe8es1+0xntD/+gaWXznP8tFEkv5IgSaHf5dS1YwVMbX/4W6m937mj+wQw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -971,6 +1495,43 @@ packages: update-browserslist-db: 1.0.11(browserslist@4.21.8) dev: true + /buffer-xor@1.0.3: + resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} + dev: true + + /buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /bufferutil@4.0.8: + resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} + engines: {node: '>=6.14.2'} + requiresBuild: true + dependencies: + node-gyp-build: 4.6.1 + dev: false + + /builtin-status-codes@3.0.0: + resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} + dev: true + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: true + /callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -997,6 +1558,25 @@ packages: supports-color: 7.2.0 dev: true + /chrome-dgram@3.0.6: + resolution: {integrity: sha512-bqBsUuaOiXiqxXt/zA/jukNJJ4oaOtc7ciwqJpZVEaaXwwxqgI2/ZdG02vXYWUhHGziDlvGMQWk0qObgJwVYKA==} + dependencies: + inherits: 2.0.4 + run-series: 1.1.9 + dev: false + + /cipher-base@1.0.4: + resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + dev: false + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -1025,14 +1605,60 @@ packages: delayed-stream: 1.0.0 dev: false + /compact2string@1.4.1: + resolution: {integrity: sha512-3D+EY5nsRhqnOwDxveBv5T8wGo4DEvYxjDtPGmdOX+gfr5gE92c2RC0w2wa+xEefm07QuVqqcF3nZJUZ92l/og==} + dependencies: + ipaddr.js: 2.1.0 + dev: false + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true + /console-browserify@1.2.0: + resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} + dev: true + + /constants-browserify@1.0.0: + resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} + dev: true + /convert-source-map@1.9.0: resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} dev: true + /create-ecdh@4.0.4: + resolution: {integrity: sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==} + dependencies: + bn.js: 4.12.0 + elliptic: 6.5.4 + dev: true + + /create-hash@1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} + dependencies: + cipher-base: 1.0.4 + inherits: 2.0.4 + md5.js: 1.3.5 + ripemd160: 2.0.2 + sha.js: 2.4.11 + dev: true + + /create-hmac@1.1.7: + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + dependencies: + cipher-base: 1.0.4 + create-hash: 1.2.0 + inherits: 2.0.4 + ripemd160: 2.0.2 + safe-buffer: 5.2.1 + sha.js: 2.4.11 + dev: true + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -1042,6 +1668,22 @@ packages: which: 2.0.2 dev: true + /crypto-browserify@3.12.0: + resolution: {integrity: sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==} + dependencies: + browserify-cipher: 1.0.1 + browserify-sign: 4.2.1 + create-ecdh: 4.0.4 + create-hash: 1.2.0 + create-hmac: 1.1.7 + diffie-hellman: 5.0.3 + inherits: 2.0.4 + pbkdf2: 3.1.2 + public-encrypt: 4.0.3 + randombytes: 2.1.0 + randomfill: 1.0.4 + dev: true + /csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} dev: true @@ -1057,15 +1699,55 @@ packages: dependencies: ms: 2.1.2 + /decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: false + /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} dev: true + /define-data-property@1.1.0: + resolution: {integrity: sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.1 + gopd: 1.0.1 + has-property-descriptors: 1.0.0 + dev: true + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.0 + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} dev: false + /des.js@1.1.0: + resolution: {integrity: sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: true + + /diffie-hellman@5.0.3: + resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} + dependencies: + bn.js: 4.12.0 + miller-rabin: 4.0.1 + randombytes: 2.1.0 + dev: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1084,6 +1766,11 @@ packages: resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} dev: false + /domain-browser@4.22.0: + resolution: {integrity: sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==} + engines: {node: '>=10'} + dev: true + /dplayer@1.27.1(debug@4.3.4): resolution: {integrity: sha512-2laBMXs5V1B9zPwJ7eAIw/OBo+Xjvy03i4GHTk3Cg+IWbrq8rKMFO0fFr6ClAYotYOCcFGOvaJDkOZcgKllsCA==} dependencies: @@ -1102,6 +1789,18 @@ packages: resolution: {integrity: sha512-FytjTbGwz///F+ToZ5XSeXbbSaXalsVRXsz2mHityI5gfxft7ieW3HqFLkU5V1aIrY42aflICqbmFoDxW10etg==} dev: true + /elliptic@6.5.4: + resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: true + /eme-encryption-scheme-polyfill@2.1.1: resolution: {integrity: sha512-njD17wcUrbqCj0ArpLu5zWXtaiupHb/2fIUQGdInf83GlI+Q6mmqaPGLdrke4savKAu15J/z1Tg/ivDgl14g0g==} dev: false @@ -1114,6 +1813,10 @@ packages: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: true + /err-code@3.0.1: + resolution: {integrity: sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==} + dev: false + /esbuild@0.17.19: resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} engines: {node: '>=12'} @@ -1144,6 +1847,36 @@ packages: '@esbuild/win32-x64': 0.17.19 dev: true + /esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + dev: true + /escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -1175,21 +1908,21 @@ packages: prettier-linter-helpers: 1.0.0 dev: true - /eslint-plugin-react-hooks@4.6.0(eslint@8.39.0): + /eslint-plugin-react-hooks@4.6.0(eslint@8.52.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 dependencies: - eslint: 8.39.0 + eslint: 8.52.0 dev: true - /eslint-plugin-react-refresh@0.4.1(eslint@8.39.0): + /eslint-plugin-react-refresh@0.4.1(eslint@8.52.0): resolution: {integrity: sha512-QgrvtRJkmV+m4w953LS146+6RwEe5waouubFVNLBfOjXJf6MLczjymO8fOcKj9jMS8aKkTCMJqiPu2WEeFI99A==} peerDependencies: eslint: '>=7' dependencies: - eslint: 8.39.0 + eslint: 8.52.0 dev: true /eslint-scope@5.1.1: @@ -1208,11 +1941,24 @@ packages: estraverse: 5.3.0 dev: true + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + /eslint-visitor-keys@3.4.1: resolution: {integrity: sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /eslint@8.39.0: resolution: {integrity: sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1262,6 +2008,53 @@ packages: - supports-color dev: true + /eslint@8.52.0: + resolution: {integrity: sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) + '@eslint-community/regexpp': 4.9.1 + '@eslint/eslintrc': 2.1.2 + '@eslint/js': 8.52.0 + '@humanwhocodes/config-array': 0.11.13 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.23.0 + graphemer: 1.4.0 + ignore: 5.2.4 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + /espree@9.5.2: resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1271,6 +2064,15 @@ packages: eslint-visitor-keys: 3.4.1 dev: true + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) + eslint-visitor-keys: 3.4.3 + dev: true + /esquery@1.5.0: resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} engines: {node: '>=0.10'} @@ -1295,11 +2097,27 @@ packages: engines: {node: '>=4.0'} dev: true + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} dev: true + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: true + + /evp_bytestokey@1.0.3: + resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} + dependencies: + md5.js: 1.3.5 + safe-buffer: 5.2.1 + dev: true + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true @@ -1308,6 +2126,10 @@ packages: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} dev: true + /fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + dev: false + /fast-glob@3.2.12: resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} engines: {node: '>=8.6.0'} @@ -1379,6 +2201,12 @@ packages: debug: 4.3.4 dev: false + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + /foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} @@ -1400,19 +2228,36 @@ packages: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true dev: true optional: true + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + /gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} dev: true + /get-browser-rtc@1.1.0: + resolution: {integrity: sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==} + dev: false + + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: true + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1469,6 +2314,13 @@ packages: type-fest: 0.20.2 dev: true + /globals@13.23.0: + resolution: {integrity: sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + /globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -1481,10 +2333,20 @@ packages: slash: 3.0.0 dev: true + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + /grapheme-splitter@1.0.4: resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} dev: true + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -1495,10 +2357,71 @@ packages: engines: {node: '>=8'} dev: true + /has-property-descriptors@1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.2.1 + dev: true + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: true + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag@1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /hash-base@3.1.0: + resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} + engines: {node: '>=4'} + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + safe-buffer: 5.2.1 + + /hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: true + /hls.js@1.4.5: resolution: {integrity: sha512-xb7IiSM9apU3tJWb5rdSStobXPNJJykHTwSy7JnLF5y/kLJXWjoR/fEpNBlwYxkKcDiiSfO9SQI8yFravZJxIg==} dev: false + /hmac-drbg@1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: true + + /https-browserify@1.0.0: + resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} + dev: true + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + /ignore@5.2.4: resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} engines: {node: '>= 4'} @@ -1526,6 +2449,37 @@ packages: /inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /ip@1.1.8: + resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==} + dev: false + + /ip@2.0.0: + resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} + dev: false + + /ipaddr.js@2.1.0: + resolution: {integrity: sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==} + engines: {node: '>= 10'} + dev: false + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.13.0: + resolution: {integrity: sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==} + dependencies: + has: 1.0.3 dev: true /is-extglob@2.1.1: @@ -1538,6 +2492,13 @@ packages: engines: {node: '>=8'} dev: true + /is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1545,6 +2506,14 @@ packages: is-extglob: 2.1.1 dev: true + /is-nan@1.3.2: + resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + dev: true + /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1555,10 +2524,22 @@ packages: engines: {node: '>=8'} dev: true + /is-typed-array@1.1.12: + resolution: {integrity: sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.11 + dev: true + /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /isomorphic-timers-promises@1.0.1: + resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==} + engines: {node: '>=10'} + dev: true + /jackspeak@2.2.1: resolution: {integrity: sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==} engines: {node: '>=14'} @@ -1646,6 +2627,28 @@ packages: engines: {node: 14 || >=16.14} dev: true + /lru@3.1.0: + resolution: {integrity: sha512-5OUtoiVIGU4VXBOshidmtOsvBIvcQR6FD/RzWSvaeHyxCGB+PCUCu+52lqMfdc0h/2CLvHhZS4TwUmMQrrMbBQ==} + engines: {node: '>= 0.4.0'} + dependencies: + inherits: 2.0.4 + dev: false + + /magic-string@0.27.0: + resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /md5.js@1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1659,6 +2662,14 @@ packages: picomatch: 2.3.1 dev: true + /miller-rabin@4.0.1: + resolution: {integrity: sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==} + hasBin: true + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + dev: true + /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -1671,12 +2682,25 @@ packages: mime-db: 1.52.0 dev: false + /mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: false + /min-document@2.19.0: resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} dependencies: dom-walk: 0.1.2 dev: false + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: true + + /minimalistic-crypto-utils@1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + dev: true + /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: @@ -1690,6 +2714,10 @@ packages: brace-expansion: 2.0.1 dev: true + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: false + /minipass@6.0.2: resolution: {integrity: sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==} engines: {node: '>=16 || 14 >=14.17'} @@ -1721,15 +2749,80 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /node-gyp-build@4.6.1: + resolution: {integrity: sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==} + hasBin: true + requiresBuild: true + dev: false + /node-releases@2.0.12: resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==} dev: true + /node-stdlib-browser@1.2.0: + resolution: {integrity: sha512-VSjFxUhRhkyed8AtLwSCkMrJRfQ3e2lGtG3sP6FEgaLKBBbxM/dLfjRe1+iLhjvyLFW3tBQ8+c0pcOtXGbAZJg==} + engines: {node: '>=10'} + dependencies: + assert: 2.1.0 + browser-resolve: 2.0.0 + browserify-zlib: 0.2.0 + buffer: 5.7.1 + console-browserify: 1.2.0 + constants-browserify: 1.0.0 + create-require: 1.1.1 + crypto-browserify: 3.12.0 + domain-browser: 4.22.0 + events: 3.3.0 + https-browserify: 1.0.0 + isomorphic-timers-promises: 1.0.1 + os-browserify: 0.3.0 + path-browserify: 1.0.1 + pkg-dir: 5.0.0 + process: 0.11.10 + punycode: 1.4.1 + querystring-es3: 0.2.1 + readable-stream: 3.6.2 + stream-browserify: 3.0.0 + stream-http: 3.2.0 + string_decoder: 1.3.0 + timers-browserify: 2.0.12 + tty-browserify: 0.0.1 + url: 0.11.3 + util: 0.12.5 + vm-browserify: 1.1.2 + dev: true + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: true + + /object-is@1.1.5: + resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + dev: true + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign@4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 - dev: true /optionator@0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} @@ -1743,6 +2836,22 @@ packages: word-wrap: 1.2.3 dev: true + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /os-browserify@0.3.0: + resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} + dev: true + /p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1757,6 +2866,10 @@ packages: p-limit: 3.1.0 dev: true + /pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + dev: true + /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1764,6 +2877,20 @@ packages: callsites: 3.1.0 dev: true + /parse-asn1@5.1.6: + resolution: {integrity: sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==} + dependencies: + asn1.js: 5.4.1 + browserify-aes: 1.2.0 + evp_bytestokey: 1.0.3 + pbkdf2: 3.1.2 + safe-buffer: 5.2.1 + dev: true + + /path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + dev: true + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1779,6 +2906,10 @@ packages: engines: {node: '>=8'} dev: true + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + /path-scurry@1.9.2: resolution: {integrity: sha512-qSDLy2aGFPm8i4rsbHd4MNyTcrzHFsLQykrtbuGRknZZCBBVXSv2tSCDN2Cg6Rt/GFRw8GoW9y9Ecw5rIPG1sg==} engines: {node: '>=16 || 14 >=14.17'} @@ -1792,6 +2923,17 @@ packages: engines: {node: '>=8'} dev: true + /pbkdf2@3.1.2: + resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} + engines: {node: '>=0.12'} + dependencies: + create-hash: 1.2.0 + create-hmac: 1.1.7 + ripemd160: 2.0.2 + safe-buffer: 5.2.1 + sha.js: 2.4.11 + dev: true + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} dev: true @@ -1801,6 +2943,13 @@ packages: engines: {node: '>=8.6'} dev: true + /pkg-dir@5.0.0: + resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} + engines: {node: '>=10'} + dependencies: + find-up: 5.0.0 + dev: true + /postcss@8.4.24: resolution: {integrity: sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==} engines: {node: ^10 || ^12 || >=14} @@ -1810,6 +2959,15 @@ packages: source-map-js: 1.0.2 dev: true + /postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.6 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -1831,7 +2989,6 @@ packages: /process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - dev: false /promise-polyfill@8.3.0: resolution: {integrity: sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==} @@ -1841,13 +2998,60 @@ packages: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false + /public-encrypt@4.0.3: + resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} + dependencies: + bn.js: 4.12.0 + browserify-rsa: 4.1.0 + create-hash: 1.2.0 + parse-asn1: 5.1.6 + randombytes: 2.1.0 + safe-buffer: 5.2.1 + dev: true + + /punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + dev: true + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} dev: true + /qs@6.11.2: + resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: true + + /querystring-es3@0.2.1: + resolution: {integrity: sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==} + engines: {node: '>=0.4.x'} + dev: true + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + /queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + dev: false + + /random-iterate@1.0.1: + resolution: {integrity: sha512-Jdsdnezu913Ot8qgKgSgs63XkAjEsnMcS1z+cC6D6TNXsUXsMxy0RpclF2pzGZTEiTXL9BiArdGTEexcv4nqcA==} + dev: false + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /randomfill@1.0.4: + resolution: {integrity: sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==} + dependencies: + randombytes: 2.1.0 + safe-buffer: 5.2.1 dev: true /react-dom@18.2.0(react@18.2.0): @@ -1872,6 +3076,14 @@ packages: loose-envify: 1.4.0 dev: false + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + /regenerator-runtime@0.13.11: resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} dev: false @@ -1881,6 +3093,15 @@ packages: engines: {node: '>=4'} dev: true + /resolve@1.22.6: + resolution: {integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==} + hasBin: true + dependencies: + is-core-module: 2.13.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1901,18 +3122,42 @@ packages: glob: 10.2.7 dev: true + /ripemd160@2.0.2: + resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + /rollup@3.25.1: resolution: {integrity: sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 + dev: true + + /rollup@3.29.4: + resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 dev: true /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: queue-microtask: 1.2.3 + + /run-series@1.1.9: + resolution: {integrity: sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==} + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: true /scheduler@0.23.0: @@ -1934,6 +3179,18 @@ packages: lru-cache: 6.0.0 dev: true + /setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + dev: true + + /sha.js@2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + /shaka-player@4.3.7: resolution: {integrity: sha512-3aeRb/AdVnGJKI1i23pD+b2e4eXljjJflxW+RLeWrSGHNRt/givvV3DyGIzpNQ9icUDatUyOtSkx8AdTXZWYXQ==} engines: {node: '>=14'} @@ -1953,21 +3210,77 @@ packages: engines: {node: '>=8'} dev: true + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + dev: true + /signal-exit@4.0.2: resolution: {integrity: sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==} engines: {node: '>=14'} dev: true + /simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + dev: false + + /simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + dev: false + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} dev: true + /smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + dev: false + + /socks@2.7.1: + resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} + engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} + dependencies: + ip: 2.0.0 + smart-buffer: 4.2.0 + dev: false + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} dev: true + /stream-browserify@3.0.0: + resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.2 + dev: true + + /stream-http@3.2.0: + resolution: {integrity: sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==} + dependencies: + builtin-status-codes: 3.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + xtend: 4.0.2 + dev: true + + /streamx@2.15.1: + resolution: {integrity: sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==} + dependencies: + fast-fifo: 1.3.2 + queue-tick: 1.0.1 + dev: false + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1986,6 +3299,19 @@ packages: strip-ansi: 7.1.0 dev: true + /string2compact@2.0.1: + resolution: {integrity: sha512-Bm/T8lHMTRXw+u83LE+OW7fXmC/wM+Mbccfdo533ajSBNxddDHlRrvxE49NdciGHgXkUQM5WYskJ7uTkbBUI0A==} + engines: {node: '>=12.20.0'} + dependencies: + addr-to-ip-port: 2.0.0 + ipaddr.js: 2.1.0 + dev: false + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2019,10 +3345,22 @@ packages: has-flag: 4.0.0 dev: true + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + /text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true + /timers-browserify@2.0.12: + resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} + engines: {node: '>=0.6.0'} + dependencies: + setimmediate: 1.0.5 + dev: true + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -2049,6 +3387,10 @@ packages: typescript: 5.0.2 dev: true + /tty-browserify@0.0.1: + resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==} + dev: true + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2067,6 +3409,20 @@ packages: hasBin: true dev: true + /uint8-util@2.2.4: + resolution: {integrity: sha512-uEI5lLozmKQPYEevfEhP9LY3Je5ZmrQhaWXrzTVqrLNQl36xsRh8NiAxYwB9J+2BAt99TRbmCkROQB2ZKhx4UA==} + dependencies: + base64-arraybuffer: 1.0.2 + dev: false + + /undici-types@5.25.3: + resolution: {integrity: sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==} + dev: true + + /unordered-array-remove@1.0.2: + resolution: {integrity: sha512-45YsfD6svkgaCBNyvD+dFHm4qFX9g3wRSIVgWVPtm2OCnphvPxzJoe20ATsiNpNJrmzHifnxm+BN5F7gFT/4gw==} + dev: false + /update-browserslist-db@1.0.11(browserslist@4.21.8): resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} hasBin: true @@ -2084,6 +3440,48 @@ packages: punycode: 2.3.0 dev: true + /url@0.11.3: + resolution: {integrity: sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==} + dependencies: + punycode: 1.4.1 + qs: 6.11.2 + dev: true + + /utf-8-validate@5.0.10: + resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} + engines: {node: '>=6.14.2'} + requiresBuild: true + dependencies: + node-gyp-build: 4.6.1 + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + /util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.12 + which-typed-array: 1.1.11 + dev: true + + /vite-plugin-node-polyfills@0.14.1(vite@4.5.0): + resolution: {integrity: sha512-S5ofYUkXea/d94AHzDwiTA7Pv/yEwzagnjgVEuBZdy7E72GBfK17qpljAlyK3CD+CRcDzAwwl/4bEjKdvZmTGQ==} + peerDependencies: + vite: ^2.0.0 || ^3.0.0 || ^4.0.0 + dependencies: + '@rollup/plugin-inject': 5.0.3 + buffer-polyfill: /buffer@6.0.3 + node-stdlib-browser: 1.2.0 + process: 0.11.10 + vite: 4.5.0 + transitivePeerDependencies: + - rollup + dev: true + /vite@4.3.2: resolution: {integrity: sha512-9R53Mf+TBoXCYejcL+qFbZde+eZveQLDYd9XgULILLC1a5ZwPaqgmdVpL8/uvw2BM/1TzetWjglwm+3RO+xTyw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2113,7 +3511,57 @@ packages: postcss: 8.4.24 rollup: 3.25.1 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 + dev: true + + /vite@4.5.0: + resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.18.20 + postcss: 8.4.31 + rollup: 3.29.4 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vm-browserify@1.1.2: + resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + dev: true + + /which-typed-array@1.1.11: + resolution: {integrity: sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.5 + call-bind: 1.0.2 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.0 dev: true /which@2.0.2: @@ -2149,6 +3597,26 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /ws@8.14.2(bufferutil@4.0.8)(utf-8-validate@5.0.10): + resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 + dev: false + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} dev: true /yallist@3.1.1: