import { VideoSegment } from '../Storage/VideoSegment'; import Peer from './Peer'; import BalancerOptions from '../Utils/Options'; import SegmentStorage from '../Storage/SegmentStorage'; import Loader from './Loader'; import Cdn from './Cdn'; import DiskSegmentStore from '../Storage/DiskSegmentStore'; import { P2PDerivedTiming } from './P2PDerivedTiming'; import { SwarmAccessDecision } from './SwarmIdentity'; import P2PManifestRegistry from './P2PManifestRegistry'; import { TrackTypeValue } from './P2PSegmentIdResolver'; declare global { interface Window { P2PManager: any | undefined; } } /** * @type {peer} * Peer defined by bittorrent-tracker. */ type peer = { id: string; on: (string: any, callback: any) => null; off: (string: any, callback: any) => null; write: (string: any) => null; destroy: () => null; }; /** * @class * @description P2P manager class, that creates the P2P client and manages the peers and request segments to them. * @exports P2PLoader */ export default class P2PLoader { upload: boolean; private _isEnabled; private _runtimeP2pEnabled; private _packageFound; private _maxConcurrent; private _storage; private _accountCode; private _options; private _maxConnectedPeers; private _renditionStableSegmentCount; private _activeVideoRendition?; private _candidateVideoRendition?; private _candidateStableCount; private _peerTimeoutsForBan; /** V2 disk store (IndexedDB-backed). Populated by Loader at construction. */ private _diskStore?; /** V2 manifest registry (for rendition bandwidth lookups). */ private _manifestRegistry?; /** V2 timing scales received from /decision (all default to 1.0 when absent). */ private _timingScales; /** Backend-configured minimum rendition bandwidth (bps) below which P2P is skipped. 0 = disabled. */ private _minRenditionBandwidthBps; /** Backend-configured minimum CDN throughput (bps) required to announce CDN captures to peers. 0 = disabled. */ private _minUploadBandwidthBps; /** EMA of observed CDN throughput (bps). Feeds the upload gate in `querySwarmAccess`. */ private _cdnEmaBandwidthBps; /** Cached derived timing for the currently active rendition (rebuilt on activateRendition). */ private _activeDerivedTiming?; /** * Fallback bandwidth (bps) for the currently active rendition, captured * from the `Resource` model at activate time. Used when the manifest * registry has no BANDWIDTH entry for the rendition key (e.g. DASH MPDs * that omit the bandwidth attribute). 0 means "no info available". */ private _activeRenditionBandwidthBps; /** * L3: Backend-driven track-type allowlist (from /decision `shareTrackTypes`). * Default `['video']` — mirrors the fallback in Android `PeersManager`. * Use `normalizeShareTrackTypes` to keep the set canonical. */ private _shareTrackTypes; /** * L4 / M5: Cache retention in ms. Default 120 s — matches * Android `PeersManager.DEFAULT_CACHE_RETENTION_SECONDS = 120` and iOS * `P2PLoader.cacheRetentionSeconds = 120`. */ private _cacheRetentionMs; /** * L4 / M5: Cache max bytes. Default 128 MiB — matches Android * `PeersManager.DEFAULT_CACHE_MAX_BYTES` and iOS `P2PLoader.cacheMaxBytes`. */ private _cacheMaxBytes; /** L4: Interval handle for the periodic eviction timer. */ private _cacheEvictionTimer?; /** Signature of the static P2P options used to build the current P2PManager. */ private _staticOptionsSignature?; /** Peers managing */ private _p2pManager?; private _peers; private _bannedPeers; private _candidates; private _activeDownloads; private _segmentsRequested; private _activeDownloadIds; private _monitoringStarted; private _performReset; /** Per-segment local claim role selected by leader election (PRIMARY/BACKUP). */ private _localClaimRoles; /** Statistic params */ private _downloadedPeers; private _uplodadedPeers; private _maxBandwidth; private _minBandwidth; private _timeoutDiscardedBytes; private _failedRequests; private _timeoutRequests; private _segmentAbsentRequests; private _errorRequests; private _downloadMillis; private _byteDownloadCount; private _chunkDownloadCount; private _sumResponseBytes?; private _minResponseBytes?; private _maxResponseBytes?; private _samplesResponseBytes?; private _sumResponseTime?; private _minResponseTime?; private _maxResponseTime?; private _samplesResponseTime?; private _sumNetworkLatency?; private _minNetworkLatency?; private _maxNetworkLatency?; private _samplesNetworkLatency?; private _sumThroughput?; private _minThroughput?; private _maxThroughput?; private _samplesThroughput?; private _sumVideoBytes?; private _sumVideoTime?; private _totalPeerSet; private _activePeerSet; private _availablePeersMap; private _peersAvailable?; private _maxPeersAvailable?; private _minPeersAvailable?; private _peersUsed?; private _peersParallelUsed?; private _maxPeersParallelUsed?; private _minPeersParallelUsed?; private _peerDiscoveryTime?; private _peerConnectionTime?; private _sessionMaxActivePeers; private _updatePeersMetricsRefCounter; private _offerInterval; private _switches; private _switchesDueToQuality; private _switchesDueToConnectivity; private _switchesDueToErrors; private _accumBw; private _downloadMillisVideo; private _downloadedBytesVideo; private _downloadedChunksVideo; private _byteUploadDiscardedCount; private _chunkUploadDiscardedCount; private _chunkUploadCount; private _byteUploadCount; private _uploadRequests; private _uploadRequestsFailed; private _destroyed; private _cdnObject; private _cdnPingTimeBean; /** * Constructs P2PLoader. * @param accountCode * @param {BalancerOptions} options Options object. * @param {SegmentStorage} storage SegmentStore object. */ constructor(accountCode: string, options: BalancerOptions, storage: SegmentStorage); isEnabled(): boolean; enable(): void; disable(): void; getStorage(): SegmentStorage; getUploadedUniqPeers(): number; getPeersAhead(): number; getPeersBehind(): number; getPeersNothing(): number; getId(): any; getCdnObject(): Cdn; resetP2PConnection(): void; /** * Input for `info_hash` (content-level, NO rendition). Byte-for-byte parity * with Android `StringUtil.generateInfoHash(accountCode, mediaUrl, videoId, swarmIdentity)`: * 1. videoId (highest priority) -> `accountCode + videoId` * 2. swarmIdentity canonicalId -> `accountCode + canonicalId` * 3. manifest URL path -> `accountCode + getUrlPath(resource)` * Hashed by P2PManager via `Util.hash` (SHA-256, first 32 hex chars). */ private _buildInfoHashInput; /** * Input for `npaw-peer-group` (swarm-level, WITH rendition). Matches Android * `P2pManager.computeSwarmId`: `accountCode + manifestUrl + rendition` concatenated directly. * Returns undefined when either resource or active rendition is missing, so no * query param is attached to the WebSocket handshake. */ private _buildSwarmIdInput; private _buildTrackerResource; private _buildRenditionKey; private _clearRenditionCandidate; private _observeSegmentRendition; private _clearPeerState; /** * L2: Pushes the canonical SwarmIdentity resolved by the registry into the * P2PManager so the Join payload includes `canonical_swarm_id` + * `canonical_swarm_source` (parity with Android `sendJoin`). No-op when * the registry has not yet resolved one. */ private _applyCanonicalSwarmIdentityToManager; private _switchSwarm; private _shutdownP2PManager; sendJoin(): void; /** * Sets P2P settings from API response. * @param {balancerResponse} e API response. * @public */ setSettings(e: balancerResponse): void; setRuntimeP2pEnabled(enabled: boolean): void; startMonitoring(): void; isMaxPeers(): boolean; getMissingPeers(): number; getMaxPeers(): number; getBannedPeers(): Map; getUploadRequests(): number; getUploadRequestsFailed(): number; /** * @param {string} id Id of the segment. * @returns {VideoSegment|undefined} Segment object corresponding to the id, or undefined. * @public */ getSegment(segment: VideoSegment): VideoSegment | undefined; /** * Return the list of peers that can serve the video segment * * @param segment Video segment object to be requested. */ getPeersWithContent(segment: VideoSegment): Peer[]; /** * Tries to check if the segment is available from any peer, and returns if it is or not. * @param {VideoSegment} seg Video segment object to be requested. * @returns {boolean} If the segment can be downloaded using P2P or not. * @public */ request(segment: VideoSegment, loader?: Loader): boolean; addPeerResponse(response: responseStorageObject): void; private getLastResponses; addPeerLatency(peerLatency: number): void; /** * Callback for error event from P2P Client. * @param {Object} e Error event data. * @private * @static */ private static _errorListener; /** * Callback for warning event from P2P Client. * @param {Object} e Warning event data. * @private * @static */ private static _warningListener; /** * If p2p upload enabled, it stores the downloaded segment to share it with peers. * @param segment Segment to store. * @param data Data to store in the segment in memory. * @public */ storeSegment(segment: VideoSegment): void; /** * Method to be called when we want to notify the peers that we have an updated map (list of video segments). * @private */ private _sendMapToAllPeersV0; /** * Method to be called when we want to notify the peers I have a new segment available. * @private */ private _sendNewSegmentToAllPeersV1; /** * Method to be called when we want to notify the peers that we have an updated map (list of video segments). * @private */ private _sendMapToPeer; /** * Callback for new peer candidate event from P2P Client. * @param {callbackData} e Peer information to listen. * @private */ addPeerCandidate(peer: peer, answer: any): void; /** * If the peer is connected and didnt exist adds it to peers list and removes the candidates. * If it existed before and was connected, it deletes it. * @param {Object} e Event object with event name and the new peer. * @private */ peerConnect(peer: Peer): void; /** * Callback of close peer event. Will remove it from candidates and peers. * @param {Object} e Event object with event name and the peer to remove. * @private */ peerCloseListener(data: Peer): void; /** * Callback of peer segment request event. Given a request if available it returns the chosen segment. * @param {Object} e Callback data from peer segment request event. * @private */ peerSegmentUploadRequest(peer: Peer, segmentId: string, operationId: number): void; /** * Callback from peer segment loaded. It gets the data and adds it to the segment object, * then triggers the success event of it. * @param {Object} e Callback data from peer segment loaded event. * @private */ peerSegmentLoaded(peer: Peer, id: string, time: number, data: ArrayBuffer): void; /** * Callback from peer segment loading progress. It gets the data and adds it to the segment object, * then triggers the success event of it. * @param {Object} e Callback data from peer segment loaded event. * @private */ peerSegmentProgress(peer: Peer, id: string, time: number, data: ArrayBuffer, size: number): void; peerFailedSegmentTimeout(peer: Peer, id: string, size: number): void; peerFailedSegmentAbsent(peer: Peer, id: string): void; private _checkAndBanPeer; /** * Callback from peer segment uploaded. Counts the downloaded bytes and segments. * @param {Object} e callback data from peer segment uploaded event. * @private */ peerSegmentUpload(size: number): void; getUploadedBytes(): number; getUploadedChunks(): number; /** * Callback from peer segment request cancelled. Counts the uploaded bytes that wont be used by the peer. * @param {Object} e callback data from peer segment upload failed event. * @private */ peerCanceledSegmentUpload(size: number): void; /** * Destroys the client and the peers/candidates. * To be called when view is over or content switched. * @public */ destroy(): void; /** * Returns the timestamp of the oldest request stored from all the peers. * @returns {number} Timestamp of the oldest request stored. * @public */ getOldestRequestTS(): number; getPeers(): Map; /** * Returns an object with the P2P data stats of the current content/view. * @returns {P2PLoaderStats} P2P info object. * @public */ getStats(): P2PLoaderStats; /** * Android P2pProvider parity (lastFailureReason + switch tracking). Called * whenever a segment that was originally routed via P2P ends up being * served by a CDN instead — either via the V2 Loader._fallBackP2pToCdn * paths (cache miss, access denied, election timeout, dispatch crash), * via the Loader.onProcessSegmentFail re-route, or via the V1 send-handler * timeout fallback in CdnBalancer. The counter flows into the /cdn ping * P2P entry as `switches` + `switches_due_*`. */ recordSwitchFromP2P(reason: 'quality' | 'connectivity' | 'errors'): void; resetOnPing(): void; onOffer(peerId: string, offerInterval: number): void; updatePeerMetrics(): void; onConnected(peerConnectionTime: number): void; onDiscovery(discoveryTime: number): void; /** Called by Loader right after construction to wire the shared V2 collaborators. */ attachV2Collaborators(store: DiskSegmentStore, registry: P2PManifestRegistry): void; getActiveVideoRendition(): string | undefined; /** * Whether P2P access is allowed for the current swarm AND whether we should * announce CDN captures to peers. Used by the CDN capture path to decide * between "download & share" vs "download & keep to ourselves". * * G1 enforcement: * - `p2pMinRenditionBandwidthBps`: denies peer access entirely when the * active rendition's declared bandwidth is below the threshold. * - `p2pMinUploadBandwidthBps`: flips `announceCapture` off when our CDN * throughput EMA indicates we can't comfortably re-upload segments. */ /** * L1: Per-rendition swarm access. Takes the segment's `v2Rendition` / * `v2TrackType` from the VideoSegment's resolver-populated fields (or an * explicit resolved snapshot) and applies the iOS/Android gate: * - video segment AND rendition == activeVideoRendition -> allowed * - video segment AND rendition != activeVideoRendition -> denied * - non-video track -> allowed * - no active rendition yet -> denied * * The bandwidth gates stay as before. `announceCapture` requires * `upload=true`, adequate CDN EMA and, for video, the matching rendition. */ querySwarmAccess(resolved?: { trackType: TrackTypeValue; rendition?: string; }): SwarmAccessDecision; /** * Side-effectful wrapper around `querySwarmAccess` used on the CDN capture * path. Mirrors iOS `observeResolvedSegment`. Invoked per media segment * completion so we also get a chance to re-evaluate the active rendition. */ observeResolvedSegment(segment: VideoSegment): SwarmAccessDecision; /** * Records a CDN throughput sample (bps) into the EMA used by the upload gate. * Alpha mirrors the EMA used for per-peer transfer speed (0.3). */ recordCdnThroughput(bitsPerSecond: number): void; getCdnEmaBandwidthBps(): number; /** * Builds derived timing for a given segment duration using the current * TimingScaleConfig. Mirrors iOS/Android `P2PDerivedTiming.fromSegmentDuration`. */ getDerivedTimingForSegment(segmentDurationMs?: number): P2PDerivedTiming; /** * H1: Early-announcement path — creates the ACQUIRING entry in the disk store * at CDN-capture start and notifies peers with SEGMENT_STATE(ACQUIRING, PRIMARY) * + a lease derived from the current timing config. Followers can then stream * the in-flight segment via GrowingSegmentReader while we're still downloading * from the CDN. Mirrors iOS `storeAndAnnounceSegment` (commit 5ae40fa). */ announceAcquiringSegment(segment: VideoSegment, access: SwarmAccessDecision): void; /** Per-segment counter of bytes already appended to the disk store during ACQUIRING. */ private _acquiringAppendedBytes; /** * Progressive append for an ACQUIRING segment. The caller passes the full * response buffer so far (XHR gives us cumulative bytes on each progress * tick); we extract the unseen suffix and push it into the disk store so * any open GrowingSegmentReader wakes up and streams the new bytes. */ appendAcquiringBytes(segment: VideoSegment, cumulative: ArrayBuffer | undefined): void; /** * F3: CDN-captured segment goes through the V2 disk store, flipping its * state machine ACQUIRING -> READY and notifying peers via SEGMENT_STATE. * Idempotent (safe to call multiple times for the same segment). */ storeSegmentV2(segment: VideoSegment, access: SwarmAccessDecision): void; /** * H2: V2-style request path. Runs the leader-election poll first; if a peer * is found advertising ACQUIRING or READY for this segment within the * election window, issues the peer request and returns true. Otherwise * returns false so the caller can fall back to CDN. Non-destructive wrt the * legacy synchronous `request()` — callers that want election opt in here. */ requestAsyncWithElection(segment: VideoSegment): Promise; /** * K2: Track-type filter. Defaults to accepting only video tracks to match * the `shareTrackTypes=['video']` default used by iOS/Android — audio and * subtitle segments are small enough that the P2P overhead isn't worth it. * Exposed as public so callers can gate symmetrical decisions (e.g. the * upload path in `storeSegmentV2`). */ canShareTrack(trackType: TrackTypeValue | undefined): boolean; /** * Java `String.hashCode()` implementation used for deterministic peer-id * selection. Signed 32-bit, exactly the algorithm iOS/Android use on the * same inputs so all three platforms compute the same winner. */ private static _javaStringHashCode; /** * Deterministic per-segment delay used to decide claim priority. Lower * derived value => higher priority. Matches iOS `deterministicClaimDelay`: * `hashCode(segmentId + ':' + localPeerId)` normalized into [0, electionWindowMs]. */ determineLocalClaimRole(segmentId: string, electionWindowMs: number): number; /** * Polls the peer set for up to `electionWindowMs` (10 ticks of 50ms by * default) looking for a peer that has the segment available (READY or * ACQUIRING with a live lease). Returns the first eligible peer found, or * undefined if the window elapses with no candidate — in which case the * caller should treat itself as the leader and go to CDN. */ awaitLeaderOrPeer(segmentId: string, electionWindowMs: number): Promise; private _determineLocalClaimRoleForSegment; /** * Recomputes the active rendition side-effects: refreshes the derived-timing * snapshot under the current `TimingScaleConfig`. Called whenever the * rendition stability threshold triggers a swarm switch. */ private _refreshDerivedTiming; /** * L4: Starts/restarts the periodic disk-cache eviction timer. Matches * Android `PeersManager.handleSegmentRetention`: every 30 seconds, evict * READY segments older than `cacheRetentionMs` and, if the total still * exceeds `cacheMaxBytes`, drop the LRU tail. Runs only while P2P is * enabled; `_shutdownP2PManager`/`disable` stop it. */ private _startCacheEvictionTimer; private _stopCacheEvictionTimer; /** * K1: Derived first-byte timeout for a peer request. Prefers the active * rendition's snapshot (cached at activate time) and falls back to a * per-segment computation using the segment's declared duration. Matches * the `timingForResolvedSegment` helper used by Android `P2pProvider`. */ private _resolveFirstByteTimeout; /** * H3 + K4: serve an ACQUIRING segment to a peer by waiting for it to * transition to READY and then forwarding the finalized payload. * * Waiters fire on any mutation (each `append` included), not just state * transitions. We loop — re-subscribing after each intermediate notify — * until either the state becomes terminal or the active rendition's * `fallbackBudgetMs` budget elapses. Parity with the * `GrowingSegmentReader.readNextChunk` wait semantics. */ private _streamAcquiringToPeer; } export {};