{"version":3,"file":"mesh-quality.cjs","names":[],"sources":["../../src/webrtc/mesh-quality.ts"],"sourcesContent":["import { setSenderBitrate } from \"./sender-bitrate\";\nimport type { MeshQuality, MeshSlot } from \"./mesh-types\";\n\n/** Uplink assumed available when the caller names no budget, in kbps. */\nconst DEFAULT_UPLINK_BUDGET_KBPS = 6000;\n\n/** Floor a video slot keeps after the division, in kbps. */\nconst DEFAULT_MIN_VIDEO_KBPS = 300;\n\n/** Budget below which `maintain-framerate` stops being worth holding, in kbps. */\nconst DEFAULT_FLUID_FLOOR_KBPS = 900;\n\n/**\n * Divide the video caps by the size of the room.\n *\n * A mesh sends one copy of everything per participant, so the uplink is the\n * shared resource and the caps are what compete for it. Nothing is divided while\n * the caller is alone with one peer — that is the case the largest sizes exist\n * for, and dividing a budget that is not being shared would make them\n * unreachable in the only call where they fit.\n *\n * The floor is what keeps the division honest: without it a busy room allocates\n * tens of kbps per stream, and everybody loses the picture instead of the excess\n * giving way.\n *\n * @param quality - The caps as asked for.\n * @param peers - How many links are live.\n * @returns The caps to actually apply. The input is never mutated, so a room\n *     that empties out climbs back to what was asked for.\n */\nexport function scaleForRoom(quality: MeshQuality, peers: number): MeshQuality {\n    const video = quality.video ?? {};\n    if (peers <= 1) return quality;\n\n    const asked = Object.values(video).reduce<number>((sum, cap) => sum + (cap ?? 0), 0);\n    if (asked === 0) return quality;\n\n    const perPeer = (quality.uplinkBudgetKbps ?? DEFAULT_UPLINK_BUDGET_KBPS) / peers;\n    if (asked <= perPeer) return quality;\n\n    const floor = quality.minVideoKbps ?? DEFAULT_MIN_VIDEO_KBPS;\n    const factor = perPeer / asked;\n    const scaled: Record<string, number | null> = {};\n    for (const [slot, cap] of Object.entries(video)) {\n        scaled[slot] = cap === null ? null : Math.max(floor, Math.round(cap * factor));\n    }\n    return { ...quality, video: scaled };\n}\n\n/**\n * Decide what the encoder gives up, letting physics override the preference.\n *\n * `maintain-framerate` is honoured while there are bits enough for the frames to\n * be worth keeping. Once the room's division has taken the budget below the\n * fluid floor, holding the rate halves what each frame receives and the picture\n * is worse than the lower rate it replaced — so below that line the preference\n * is overridden rather than obeyed.\n *\n * Two things decide *which* budget answers that question:\n *\n * - **`null` is the most generous case, not the absent one.** A slot with no cap\n *   is unbounded, which is exactly where fluidity should hold. Reading it as\n *   missing — and then deciding from a modest camera beside it — is the answer\n *   backwards.\n * - **{@link MeshQuality.degradationAnchor} names the slot the choice was\n *   about.** Without it the largest cap across the video slots answers, which\n *   is right when the slots are interchangeable and wrong when they are not.\n *\n * @param quality - What was asked for, including the anchor and the floor.\n * @param effective - What the room's share actually allows.\n * @returns The degradation preference to write onto the senders, or `undefined`\n *     when the caller expressed no preference.\n */\nexport function resolveDegradation(\n    quality: MeshQuality,\n    effective: MeshQuality,\n): RTCDegradationPreference | undefined {\n    const asked = quality.degradationPreference;\n    if (asked !== \"maintain-framerate\") return asked;\n\n    const video = effective.video ?? {};\n    const floor = quality.fluidFloorKbps ?? DEFAULT_FLUID_FLOOR_KBPS;\n    const anchor = quality.degradationAnchor;\n\n    if (anchor !== undefined) {\n        if (!(anchor in video)) return asked;\n        const cap = video[anchor];\n        if (cap === null) return asked;\n        return cap >= floor ? \"maintain-framerate\" : \"maintain-resolution\";\n    }\n\n    const caps = Object.values(video);\n    if (caps.length === 0) return asked;\n    if (caps.some((cap) => cap === null)) return asked;\n\n    const budget = Math.max(...caps.filter((cap): cap is number => cap !== null));\n    return budget >= floor ? \"maintain-framerate\" : \"maintain-resolution\";\n}\n\n/**\n * Read a video sender's parameters back with the motion settings written on.\n *\n * Read fresh on every call because `setParameters` only accepts the object the\n * **same** sender's `getParameters` returned, so a rejected attempt cannot be\n * retried with the object that was rejected.\n *\n * @param sender - The video sender to read from.\n * @param degradation - What to give up first, or `undefined` to leave it alone.\n * @param fps - Frame ceiling, or `undefined` to leave it alone.\n * @returns Parameters ready to be written back to `sender`.\n */\nfunction videoParameters(\n    sender: RTCRtpSender,\n    degradation: RTCDegradationPreference | undefined,\n    fps: number | undefined,\n): RTCRtpSendParameters {\n    const params = sender.getParameters();\n    if (degradation !== undefined) params.degradationPreference = degradation;\n    if (fps !== undefined) {\n        for (const encoding of params.encodings ?? []) encoding.maxFramerate = fps;\n    }\n    return params;\n}\n\n/**\n * Write the quality settings onto one link's senders.\n *\n * `degradationPreference` goes only on video: it describes trading resolution\n * against frame rate, which an audio sender has no analogue for, and some\n * browsers reject it outright there.\n *\n * The retry without `degradationPreference` is for Firefox, which rejects the\n * member entirely — sending both together would lose the frame-rate cap to an\n * objection about something else.\n *\n * @param transceivers - The link's transceivers, in slot order.\n * @param slots - The slot list those transceivers were allocated from.\n * @param effective - Caps after the room's division.\n * @param degradation - Already resolved against the fluid floor.\n */\nexport async function applyQualityToLink(\n    transceivers: readonly RTCRtpTransceiver[],\n    slots: readonly MeshSlot[],\n    effective: MeshQuality,\n    degradation: RTCDegradationPreference | undefined,\n): Promise<void> {\n    for (const [index, slot] of slots.entries()) {\n        const sender = transceivers[index]?.sender;\n        if (!sender) continue;\n\n        if (slot.kind === \"audio\") {\n            const bps = effective.audio?.[slot.name];\n            if (bps !== undefined) await setSenderBitrate(sender, bps);\n            continue;\n        }\n\n        const kbps = effective.video?.[slot.name];\n        if (kbps !== undefined) await setSenderBitrate(sender, kbps === null ? null : kbps * 1000);\n\n        if (degradation === undefined && effective.maxFramerate === undefined) continue;\n        try {\n            await sender.setParameters(\n                videoParameters(sender, degradation, effective.maxFramerate),\n            );\n        } catch {\n            try {\n                await sender.setParameters(\n                    videoParameters(sender, undefined, effective.maxFramerate),\n                );\n            } catch {\n                /* the sender refused both; the bitrate cap above still applies */\n            }\n        }\n    }\n}\n"],"mappings":"wCAIA,IAAM,EAA6B,IAG7B,EAAyB,IAGzB,EAA2B,IAoBjC,SAAgB,EAAa,EAAsB,EAA4B,CAC3E,IAAM,EAAQ,EAAQ,OAAS,CAAC,EAChC,GAAI,GAAS,EAAG,OAAO,EAEvB,IAAM,EAAQ,OAAO,OAAO,CAAK,CAAC,CAAC,QAAgB,EAAK,IAAQ,GAAO,GAAO,GAAI,CAAC,EACnF,GAAI,IAAU,EAAG,OAAO,EAExB,IAAM,GAAW,EAAQ,kBAAoB,GAA8B,EAC3E,GAAI,GAAS,EAAS,OAAO,EAE7B,IAAM,EAAQ,EAAQ,cAAgB,EAChC,EAAS,EAAU,EACnB,EAAwC,CAAC,EAC/C,IAAK,GAAM,CAAC,EAAM,KAAQ,OAAO,QAAQ,CAAK,EAC1C,EAAO,GAAQ,IAAQ,KAAO,KAAO,KAAK,IAAI,EAAO,KAAK,MAAM,EAAM,CAAM,CAAC,EAEjF,MAAO,CAAE,GAAG,EAAS,MAAO,CAAO,CACvC,CA0BA,SAAgB,EACZ,EACA,EACoC,CACpC,IAAM,EAAQ,EAAQ,sBACtB,GAAI,IAAU,qBAAsB,OAAO,EAE3C,IAAM,EAAQ,EAAU,OAAS,CAAC,EAC5B,EAAQ,EAAQ,gBAAkB,EAClC,EAAS,EAAQ,kBAEvB,GAAI,IAAW,IAAA,GAAW,CACtB,GAAI,EAAE,KAAU,GAAQ,OAAO,EAC/B,IAAM,EAAM,EAAM,GAElB,OADI,IAAQ,KAAa,EAClB,GAAO,EAAQ,qBAAuB,qBACjD,CAEA,IAAM,EAAO,OAAO,OAAO,CAAK,EAKhC,OAJI,EAAK,SAAW,GAChB,EAAK,KAAM,GAAQ,IAAQ,IAAI,EAAU,EAE9B,KAAK,IAAI,GAAG,EAAK,OAAQ,GAAuB,IAAQ,IAAI,CACpE,GAAU,EAAQ,qBAAuB,qBACpD,CAcA,SAAS,EACL,EACA,EACA,EACoB,CACpB,IAAM,EAAS,EAAO,cAAc,EAEpC,GADI,IAAgB,IAAA,KAAW,EAAO,sBAAwB,GAC1D,IAAQ,IAAA,GACR,IAAK,IAAM,KAAY,EAAO,WAAa,CAAC,EAAG,EAAS,aAAe,EAE3E,OAAO,CACX,CAkBA,eAAsB,EAClB,EACA,EACA,EACA,EACa,CACb,IAAK,GAAM,CAAC,EAAO,KAAS,EAAM,QAAQ,EAAG,CACzC,IAAM,EAAS,EAAa,EAAM,EAAE,OACpC,GAAI,CAAC,EAAQ,SAEb,GAAI,EAAK,OAAS,QAAS,CACvB,IAAM,EAAM,EAAU,QAAQ,EAAK,MAC/B,IAAQ,IAAA,IAAW,MAAM,EAAA,iBAAiB,EAAQ,CAAG,EACzD,QACJ,CAEA,IAAM,EAAO,EAAU,QAAQ,EAAK,MACpC,GAAI,IAAS,IAAA,IAAW,MAAM,EAAA,iBAAiB,EAAQ,IAAS,KAAO,KAAO,EAAO,GAAI,EAErF,IAAgB,IAAA,IAAa,EAAU,eAAiB,IAAA,GAC5D,GAAI,CACA,MAAM,EAAO,cACT,EAAgB,EAAQ,EAAa,EAAU,YAAY,CAC/D,CACJ,MAAQ,CACJ,GAAI,CACA,MAAM,EAAO,cACT,EAAgB,EAAQ,IAAA,GAAW,EAAU,YAAY,CAC7D,CACJ,MAAQ,CAER,CACJ,CACJ,CACJ"}