{"version":3,"file":"link-stats.cjs","names":[],"sources":["../../src/webrtc/link-stats.ts"],"sourcesContent":["/**\n * One link's outbound picture, in the shape a call UI actually renders.\n *\n * Everything here is derived, not reported: WebRTC hands out cumulative\n * counters and a graph of candidate pairs, and turning that into\n * `\"1,2 Mbps · 42 ms · 1080p60\"` is the work {@link createLinkStatsSampler}\n * does.\n */\nexport interface LinkStats {\n    /** Throughput since the previous sample. `0` on the first one — there is no delta yet. */\n    kbps: number;\n    /** Width of the largest stream being sent, or `0` before one is reported. */\n    width: number;\n    /** Height of the largest stream being sent, or `0` before one is reported. */\n    height: number;\n    /** Frame rate of the largest stream being sent, or `0` when the browser omits it. */\n    fps: number;\n    /** Round trip to the peer in milliseconds, or `null` before the first reading. */\n    rttMs: number | null;\n    /**\n     * Uplink the transport estimates for this path, in kbps, or `null`.\n     *\n     * `null` and not `0`, because the two mean opposite things: no estimate yet\n     * is the normal state for the first seconds of every call and the permanent\n     * state on an engine that publishes none, while `0` is indistinguishable\n     * from a path that died. A consumer that reads absence as zero drops the\n     * quality at the start of every call.\n     *\n     * This is the field that separates \"healthy at 2.5 Mbps\" from \"capped at\n     * 2.5 Mbps and drowning\" — `kbps` reports the cap being honoured either\n     * way, while the queue behind it grows.\n     */\n    availableKbps: number | null;\n    /**\n     * What the encoder says is holding the picture back, or `null` for nothing.\n     *\n     * `\"bandwidth\"` wins over the other values when senders disagree, because\n     * it is the only one a lower cap answers. Reacting to bandwidth on a\n     * machine that is actually CPU-bound buys a worse picture and no relief.\n     *\n     * The spec's `\"none\"` is reported as `null`: a consumer should not have to\n     * know that one of the truthy strings means \"nothing\".\n     */\n    limitedBy: RTCQualityLimitationReason | null;\n    /**\n     * Whether the link is travelling through a TURN relay.\n     *\n     * On a self-hosted mesh this is the hosting bill: a relayed stream goes up\n     * and down through the machine somebody is paying for, and the person who\n     * picked 4K is not that somebody.\n     *\n     * Resolved only from the pair the transport **names**, never from a merely\n     * `succeeded` one — guessing the route from a pair that carries nothing\n     * would report a cost nobody is paying.\n     */\n    relayed: boolean;\n}\n\n/** Which media a sampler counts. */\nexport type LinkStatsKind = \"video\" | \"audio\" | \"all\";\n\n/** Options for {@link createLinkStatsSampler}. */\nexport interface LinkStatsSamplerOptions {\n    /**\n     * Which media the throughput counts. Default `\"video\"`.\n     *\n     * Video is the default because it is what saturates an uplink — audio is an\n     * order of magnitude cheaper, and mixing it in moves the number by less than\n     * the noise between two samples. Use `\"all\"` when the figure is meant to be\n     * the connection's real cost rather than the picture's.\n     */\n    kind?: LinkStatsKind;\n}\n\n/**\n * A sampler bound to one connection.\n *\n * Holds the previous byte counter and timestamp, which is the whole reason this\n * is an object rather than a function: the rate is a delta, so somebody has to\n * remember the last reading. One sampler per link — sharing one across peers\n * subtracts one connection's counter from another's and reports nonsense.\n */\nexport interface LinkStatsSampler {\n    /**\n     * Reduce a report you already have.\n     *\n     * @param report - A report from `RTCPeerConnection.getStats()`.\n     * @returns The stats for this sample.\n     */\n    read: (report: RTCStatsReport) => LinkStats;\n    /**\n     * Fetch a report and reduce it.\n     *\n     * @param connection - The connection to sample.\n     * @returns The stats for this sample.\n     */\n    sample: (connection: RTCPeerConnection) => Promise<LinkStats>;\n    /**\n     * Drop the baseline the rate is derived from.\n     *\n     * Call it after an ICE restart, a reconnect, or a pause — otherwise the next\n     * sample divides the bytes of the whole gap by the whole gap and reports the\n     * average of a period nobody is asking about. The next reading comes back at\n     * `0` kbps and starts a fresh baseline; the resolution and round trip already\n     * on screen are kept, so the badge does not blank out.\n     */\n    reset: () => void;\n}\n\n/**\n * The three field readers below take an entry the collector has already\n * established is an object.\n *\n * A report is a `Map` whose values the browser fills, and nothing says they\n * have to be objects — a polyfill or a mock can put anything in there. That\n * check belongs at the door of the one loop that walks the report, not repeated\n * in each reader: three copies of the same guard means three branches no test\n * can reach past the first, and a reader that silently returns `null` for a\n * primitive hides the case instead of skipping it.\n */\ntype StatsEntry = Record<string, unknown>;\n\nfunction numberField(entry: StatsEntry, key: string): number | null {\n    const value: unknown = entry[key];\n    return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\nfunction stringField(entry: StatsEntry, key: string): string | null {\n    const value: unknown = entry[key];\n    return typeof value === \"string\" ? value : null;\n}\n\nfunction booleanField(entry: StatsEntry, key: string): boolean | null {\n    const value: unknown = entry[key];\n    return typeof value === \"boolean\" ? value : null;\n}\n\n/**\n * Whether a string is one of the reasons the spec defines.\n *\n * `\"none\"` is deliberately not one of them here: the field it feeds reports\n * \"nothing is limiting\" as `null`, so a consumer never has to know that one of\n * the truthy strings means no.\n */\nfunction isLimitationReason(value: string | null): value is RTCQualityLimitationReason {\n    return value === \"bandwidth\" || value === \"cpu\" || value === \"other\";\n}\n\n/**\n * Resolve the media an RTP entry carries.\n *\n * `kind` is the standard field and `mediaType` is what older Chrome reported;\n * both are still in the wild, and a sampler that reads only one of them\n * silently counts nothing on the browser that uses the other.\n */\nfunction entryKind(entry: StatsEntry): string | null {\n    return stringField(entry, \"kind\") ?? stringField(entry, \"mediaType\");\n}\n\n/** What one candidate pair says about the path it describes. */\ninterface PairFacts {\n    id: string;\n    /** `true` when the browser marks this pair as the chosen one non-standardly. */\n    selected: boolean;\n    state: string | null;\n    rttMs: number | null;\n    availableKbps: number | null;\n    localCandidateId: string | null;\n}\n\n/** What one sender says about what it is sending. */\ninterface SenderFacts {\n    kind: string | null;\n    bytes: number;\n    width: number;\n    height: number;\n    fps: number;\n    limitedBy: RTCQualityLimitationReason | null;\n}\n\n/** Everything a single walk over a report yields. */\ninterface CollectedReport {\n    namedPairId: string | null;\n    pairs: PairFacts[];\n    relayCandidateIds: Set<string>;\n    senders: SenderFacts[];\n}\n\n/**\n * Reduce a report in **one** pass.\n *\n * One pass is the point rather than tidiness. Every field below lives in the\n * same report, and the pair the transport selected has to be resolved before\n * any of the path fields can be read — so a consumer that asks for round trip,\n * then throughput headroom, then whether the route is relayed, walks the same\n * report three times and resolves the same pair three times, per link, on every\n * tick. On a mesh of eight at one sample every two seconds that is the most\n * expensive recurring work in the call, on the device least able to pay it.\n *\n * @param report - A report from `RTCPeerConnection.getStats()`.\n * @returns The entries that matter, grouped.\n */\nfunction collect(report: RTCStatsReport): CollectedReport {\n    let namedPairId: string | null = null;\n    const pairs: PairFacts[] = [];\n    const relayCandidateIds = new Set<string>();\n    const senders: SenderFacts[] = [];\n\n    report.forEach((raw: unknown) => {\n        if (typeof raw !== \"object\" || raw === null) return;\n        const entry = raw as StatsEntry;\n        const type = stringField(entry, \"type\");\n        if (type === \"transport\") {\n            namedPairId = stringField(entry, \"selectedCandidatePairId\") ?? namedPairId;\n            return;\n        }\n        if (type === \"candidate-pair\") {\n            const seconds = numberField(entry, \"currentRoundTripTime\");\n            const bps = numberField(entry, \"availableOutgoingBitrate\");\n            pairs.push({\n                id: stringField(entry, \"id\") ?? \"\",\n                selected: booleanField(entry, \"selected\") === true,\n                state: stringField(entry, \"state\"),\n                rttMs: seconds === null ? null : seconds * 1000,\n                availableKbps: bps === null ? null : Math.round(bps / 1000),\n                localCandidateId: stringField(entry, \"localCandidateId\"),\n            });\n            return;\n        }\n        if (type === \"local-candidate\") {\n            const id = stringField(entry, \"id\");\n            if (id !== null && stringField(entry, \"candidateType\") === \"relay\") {\n                relayCandidateIds.add(id);\n            }\n            return;\n        }\n        if (type !== \"outbound-rtp\") return;\n        const reason = stringField(entry, \"qualityLimitationReason\");\n        senders.push({\n            kind: entryKind(entry),\n            bytes: numberField(entry, \"bytesSent\") ?? 0,\n            width: numberField(entry, \"frameWidth\") ?? 0,\n            height: numberField(entry, \"frameHeight\") ?? 0,\n            fps: Math.round(numberField(entry, \"framesPerSecond\") ?? 0),\n            limitedBy: isLimitationReason(reason) ? reason : null,\n        });\n    });\n\n    return { namedPairId, pairs, relayCandidateIds, senders };\n}\n\n/**\n * The candidate pair carrying the link, and how sure we are that it is.\n *\n * A connection routinely keeps several viable pairs alive at once — host,\n * server-reflexive, relayed — and only one of them carries traffic. Reading the\n * first `succeeded` pair makes a reading jump between paths that are not being\n * travelled: 8 ms on an idle host pair alternating with 180 ms on the TURN pair\n * doing the work.\n *\n * The chain is `transport.selectedCandidatePairId` → a pair flagged\n * `selected: true` → the first `succeeded` one. The middle step is not in the\n * spec and is there because an engine that fills neither the transport field\n * nor it does not appear to exist, while one that fills only the flag does: a\n * reader that skips straight to `succeeded` silently answers about the wrong\n * path there. The last step is a guess, and `named` says so — the fields where\n * guessing would report something false refuse it.\n *\n * @param collected - A collected report.\n * @returns The pair and whether the browser actually named it.\n */\nfunction carryingPair(collected: CollectedReport): { pair: PairFacts | null; named: boolean } {\n    const byId =\n        collected.namedPairId === null\n            ? undefined\n            : collected.pairs.find((pair) => pair.id === collected.namedPairId);\n    if (byId !== undefined) return { pair: byId, named: true };\n\n    const flagged = collected.pairs.find((pair) => pair.selected);\n    if (flagged !== undefined) return { pair: flagged, named: true };\n\n    const succeeded = collected.pairs.find((pair) => pair.state === \"succeeded\");\n    return { pair: succeeded ?? null, named: false };\n}\n\n/**\n * Read the round trip of the candidate pair actually carrying the link.\n *\n * A `succeeded` pair is kept as a last resort because not every browser names\n * the selected one — losing the reading entirely is worse than an occasionally\n * optimistic one. See {@link carryingPair} for the chain and why the middle\n * step exists.\n *\n * @param report - A report from `RTCPeerConnection.getStats()`.\n * @returns Round trip in milliseconds, rounded, or `null` when nothing reported\n *   one — which is the normal state before the connection settles.\n *\n * @example\n * const rttMs = readRoundTripMs(await pc.getStats());\n */\nexport function readRoundTripMs(report: RTCStatsReport): number | null {\n    return roundTripOf(collect(report));\n}\n\n/**\n * Read the uplink the transport estimates for this path, in kbps.\n *\n * This is the field that tells a cap being honoured apart from a cap that is\n * drowning: `bytesSent` reports the same 2500 kbps whether the path has room\n * for it or the queue behind it is growing. No fixed budget can stand in for it\n * — a domestic uplink of 1 Mbps and a fibre link differ by an order of\n * magnitude, and in Brazil the upload routinely is a tenth of the download\n * beside it.\n *\n * @param report - A report from `RTCPeerConnection.getStats()`.\n * @returns The estimate in kbps, or `null` while there is none. Every reader\n *   needs a fallback for that, not a default of zero.\n *\n * @example\n * const headroom = readAvailableOutgoingKbps(await pc.getStats());\n * if (headroom !== null && headroom < asked) lowerTheCap(headroom);\n */\nexport function readAvailableOutgoingKbps(report: RTCStatsReport): number | null {\n    return availableOf(collect(report));\n}\n\n/**\n * Read what the encoder says is holding the picture back.\n *\n * `\"bandwidth\"` wins when senders disagree, because it is the only reason a\n * lower cap answers. The spec's `\"none\"` comes back as `null`.\n *\n * @param report - A report from `RTCPeerConnection.getStats()`.\n * @returns The reason, or `null` when nothing is limiting the picture.\n *\n * @example\n * if (readQualityLimitation(await pc.getStats()) === \"cpu\") stopBlurringTheBackground();\n */\nexport function readQualityLimitation(report: RTCStatsReport): RTCQualityLimitationReason | null {\n    return limitationOf(collect(report));\n}\n\n/**\n * Read whether the link is travelling through a TURN relay.\n *\n * Resolved only from the pair the browser names, never from a merely\n * `succeeded` one: a relayed route is somebody's hosting bill, and reporting\n * one from a pair that carries nothing bills a cost nobody is paying.\n *\n * @param report - A report from `RTCPeerConnection.getStats()`.\n * @returns `true` when the carrying pair's local candidate is a relay.\n *\n * @example\n * if (readRelayed(await pc.getStats())) capTheStreamThatCostsMoney();\n */\nexport function readRelayed(report: RTCStatsReport): boolean {\n    return relayedOf(collect(report));\n}\n\n/** Round trip of the carrying pair, in whole milliseconds. */\nfunction roundTripOf(collected: CollectedReport): number | null {\n    const withTiming: CollectedReport = {\n        ...collected,\n        pairs: collected.pairs.filter((pair) => pair.rttMs !== null),\n    };\n    const { pair } = carryingPair(withTiming);\n    return pair?.rttMs === undefined || pair.rttMs === null ? null : Math.round(pair.rttMs);\n}\n\n/** Estimated uplink of the carrying pair, in kbps. */\nfunction availableOf(collected: CollectedReport): number | null {\n    const { pair } = carryingPair(collected);\n    return pair?.availableKbps ?? null;\n}\n\n/** Whether the pair the browser named travels through a relay. */\nfunction relayedOf(collected: CollectedReport): boolean {\n    const { pair, named } = carryingPair(collected);\n    if (!named || pair === null || pair.localCandidateId === null) return false;\n    return collected.relayCandidateIds.has(pair.localCandidateId);\n}\n\n/** The strongest limitation any sender reports, with bandwidth winning. */\nfunction limitationOf(collected: CollectedReport): RTCQualityLimitationReason | null {\n    let found: RTCQualityLimitationReason | null = null;\n    for (const sender of collected.senders) {\n        if (sender.limitedBy === null) continue;\n        if (sender.limitedBy === \"bandwidth\") return \"bandwidth\";\n        found = found ?? sender.limitedBy;\n    }\n    return found;\n}\n\n/**\n * Track one link's throughput, resolution and round trip across samples.\n *\n * Every rate here is a **delta**. `bytesSent` is cumulative since the connection\n * opened, so dividing it by the session length gives the historical average —\n * a number that only ever falls and never shows what is happening now. The\n * previous reading is kept on the sampler and subtracted, which is the part\n * every hand-rolled copy of this ends up rewriting.\n *\n * Bytes are summed across every matching sender, because a peer publishing a\n * camera and a screen at once occupies one uplink with both — and the uplink is\n * what runs out. Resolution and frame rate come from the **largest** stream by\n * area, which is the one that dominates that bandwidth and the one somebody\n * watching the call is looking at.\n *\n * @param options - See {@link LinkStatsSamplerOptions}.\n * @returns A sampler. Use one per `RTCPeerConnection`.\n *\n * @example\n * const sampler = createLinkStatsSampler();\n *\n * setInterval(async () => {\n *   const stats = await sampler.sample(pc);\n *   badge.textContent = `${stats.kbps} kbps · ${stats.rttMs ?? \"—\"} ms`;\n * }, 2000);\n */\nexport function createLinkStatsSampler(options: LinkStatsSamplerOptions = {}): LinkStatsSampler {\n    const kind: LinkStatsKind = options.kind ?? \"video\";\n    let lastBytes: number | null = null;\n    let lastSampleAt = 0;\n    let last: LinkStats = {\n        kbps: 0,\n        width: 0,\n        height: 0,\n        fps: 0,\n        rttMs: null,\n        availableKbps: null,\n        limitedBy: null,\n        relayed: false,\n    };\n\n    function read(report: RTCStatsReport): LinkStats {\n        const now = performance.now();\n        const collected = collect(report);\n        const path = {\n            rttMs: roundTripOf(collected),\n            availableKbps: availableOf(collected),\n            limitedBy: limitationOf(collected),\n            relayed: relayedOf(collected),\n        };\n\n        let bytes = 0;\n        let sawSender = false;\n        let bestArea = 0;\n        let width = 0;\n        let height = 0;\n        let fps = 0;\n\n        for (const sender of collected.senders) {\n            if (kind !== \"all\" && sender.kind !== kind) continue;\n            sawSender = true;\n            bytes += sender.bytes;\n\n            const area = sender.width * sender.height;\n            if (area < bestArea) continue;\n            bestArea = area;\n            width = sender.width;\n            height = sender.height;\n            fps = sender.fps;\n        }\n\n        if (!sawSender) {\n            last = { ...last, ...path };\n            return last;\n        }\n\n        const elapsed = lastBytes === null ? 0 : (now - lastSampleAt) / 1000;\n        const delta = lastBytes === null ? 0 : bytes - lastBytes;\n        const kbps = elapsed > 0 && delta > 0 ? Math.round((delta * 8) / 1000 / elapsed) : 0;\n\n        lastBytes = bytes;\n        lastSampleAt = now;\n        last = {\n            kbps,\n            width: width > 0 ? width : last.width,\n            height: height > 0 ? height : last.height,\n            fps: fps > 0 ? fps : last.fps,\n            ...path,\n        };\n        return last;\n    }\n\n    return {\n        read,\n        sample: async (connection: RTCPeerConnection): Promise<LinkStats> =>\n            read(await connection.getStats()),\n        reset: (): void => {\n            lastBytes = null;\n            lastSampleAt = 0;\n            last = { ...last, kbps: 0 };\n        },\n    };\n}\n"],"mappings":"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"}