npaw-plugin-nwf
Version:
NPAW's Plugin
518 lines (517 loc) • 22 kB
TypeScript
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<string, boolean>;
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<string, Peer>;
/**
* 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<boolean>;
/**
* 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<Peer | undefined>;
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 {};