import { type ReactiveStore } from "./reactive"; export declare const FEATURE_DESKTOP_MEDIA: number; export declare const C2S_MEDIA_CONTROL = 62; export declare const C2S_MEDIA_DATA = 63; export declare const S2C_MEDIA_CONTROL = 53; export declare const RUNTIME_PIPEWIRE: number; export declare const RUNTIME_MICROPHONE: number; export declare const RUNTIME_CAMERA: number; export declare const RUNTIME_PORTAL_FRONTEND: number; export declare const RUNTIME_PORTAL_ACCESS: number; export declare const RUNTIME_PORTAL_SCREENCAST: number; export declare const RUNTIME_MPRIS: number; export declare const ACTIVE_MICROPHONE: number; export declare const ACTIVE_CAMERA: number; export declare const ACTIVE_SCREENCAST: number; export declare const CAPTURE_MICROPHONE: number; export declare const CAPTURE_CAMERA: number; export declare const CAPTURE_PORTAL_UI: number; export declare const AUDIO_CODEC_PCM: number; export declare const AUDIO_CODEC_OPUS: number; export declare const VIDEO_CODEC_MJPEG: number; export declare const VIDEO_CODEC_H264: number; export declare const VIDEO_CODEC_AV1: number; export declare const VIDEO_CODEC_H264_444: number; export declare const VIDEO_CODEC_AV1_444: number; export declare const VIDEO_CODECS_LEGACY: number; export declare const VIDEO_CODECS_ALL: number; export declare const MPRIS_UPDATE_RESET: number; export declare const MPRIS_UPDATE_SYNC: number; export declare const MPRIS_UPDATE_REPLAY: number; export declare const MPRIS_UPDATE_MAX_DECOMPRESSED: number; export declare const MPRIS_PLAYER_MAX = 32; export declare const MPRIS_ARTIST_MAX = 16; export declare const MPRIS_STRING_MAX: number; export declare const MPRIS_ARTWORK_MAX: number; export declare const MPRIS_CAN_CONTROL: number; export declare const MPRIS_CAN_PLAY: number; export declare const MPRIS_CAN_PAUSE: number; export declare const MPRIS_CAN_GO_NEXT: number; export declare const MPRIS_CAN_GO_PREVIOUS: number; export declare const MPRIS_CAN_SEEK: number; export declare const MPRIS_CAN_RAISE: number; export declare const MPRIS_CAN_SET_VOLUME: number; export declare const MPRIS_CAN_SET_SHUFFLE: number; export declare const MPRIS_CAN_SET_LOOP_STATUS: number; export declare const MPRIS_CAN_SET_RATE: number; export interface MediaCapabilities { microphone: boolean; camera: boolean; portalUi: boolean; audioCodecs: number; videoCodecs: number; maxWidth: number; maxHeight: number; maxFps: number; } export interface ScreenCastState { sessionId: number; appId: string; surfaceIds: readonly number[]; } export interface DesktopMediaState { runtimeFlags: number; activeFlags: number; microphoneOwner: bigint; cameraOwner: bigint; screencasts: readonly ScreenCastState[]; } export type MediaLeaseStatus = "inactive" | "starting" | "active"; export interface MediaLeaseState { kind: "microphone" | "camera"; status: MediaLeaseStatus; leaseId: number; codec: number; width: number; height: number; fps: number; credit: number; error: string | null; } export interface MicrophoneOptions { /** Defaults to Opus when WebCodecs can encode it, otherwise PCM. */ codec?: "pcm" | "opus"; } export interface CameraOptions { /** * Omit to choose the best exact format supported by this browser. `h264` * and `av1` retain their 8-bit 4:2:0 meaning unless `chroma` is explicit. */ codec?: "mjpeg" | "h264" | "av1"; /** Exact chroma sampling for H.264/AV1. Motion JPEG does not expose this. */ chroma?: "420" | "444"; width?: number; height?: number; fps?: number; /** * How many bits the picture is worth. Scales the computed bitrate for the * compressed codecs and the JPEG quantizer for Motion JPEG — the same * intent expressed in whichever currency the codec takes. */ quality?: CameraQuality; } export type CameraQuality = "low" | "balanced" | "high"; export interface PortalChoiceValue { id: string; value: string; } export interface PortalChoice { id: string; label: string; options: readonly PortalChoiceValue[]; initialValue: string; } interface PortalRequestBase { requestId: number; deadlineMs: number; parentSurfaceId: number | null; appId: string; } export interface PortalAccessRequest extends PortalRequestBase { kind: "access"; title: string; subtitle: string; body: string; denyLabel: string; grantLabel: string; iconName: string; choices: readonly PortalChoice[]; } export interface ScreenCastCandidate { surfaceId: number; width: number; height: number; title: string; appId: string; thumbnailPng: Uint8Array; } export interface PortalScreenCastRequest extends PortalRequestBase { kind: "screencast"; multiple: boolean; candidates: readonly ScreenCastCandidate[]; } export type PortalRequest = PortalAccessRequest | PortalScreenCastRequest; export type PlaybackStatus = "stopped" | "paused" | "playing"; export type LoopStatus = "none" | "track" | "playlist"; /** * How a player's cover arrives. * * Catalogue-backed players (Spotify and friends) name their cover with an * `https:` URL and keep no local copy, so the server forwards that URL and the * browser loads and caches it: re-encoding it server-side would put ~150 KiB of * PNG in every upsert. Art that exists only on the server's disk cannot be * named to a browser, so it still arrives as bytes. */ export type MprisArtwork = { kind: "url"; url: string; } | { kind: "png"; png: Uint8Array; }; export declare const ARTWORK_KIND_NONE = 0; export declare const ARTWORK_KIND_URL = 1; export declare const ARTWORK_KIND_PNG = 2; /** * The only schemes this client will put in an image source. Enforced here as * well as on the server, because the value reaches the DOM. */ export declare function artworkUrlAllowed(url: string): boolean; export interface MprisPlayer { playerId: number; revision: number; trackRevision: number; active: boolean; playbackStatus: PlaybackStatus; loopStatus: LoopStatus; shuffle: boolean; capabilityFlags: number; rate: number; minimumRate: number; maximumRate: number; volume: number; positionUs: number; lengthUs: number; identity: string; desktopEntry: string; title: string; album: string; artists: readonly string[]; artwork: MprisArtwork | null; /** Local monotonic receipt anchor; never compared with a server clock. */ receivedAtMs: number; } export type MprisAction = { kind: "select"; } | { kind: "play"; } | { kind: "pause"; } | { kind: "playPause"; } | { kind: "stop"; } | { kind: "next"; } | { kind: "previous"; } | { kind: "seek"; offsetUs: number; } | { kind: "setPosition"; positionUs: number; trackRevision: number; } | { kind: "volume"; volume: number; } | { kind: "shuffle"; shuffle: boolean; } | { kind: "loopStatus"; loopStatus: LoopStatus; } | { kind: "rate"; rate: number; } | { kind: "raise"; }; type MprisRecord = { kind: "delete"; playerId: number; } | { kind: "upsert"; player: MprisPlayer; }; export type ParsedControl = { kind: "state"; state: DesktopMediaState; } | { kind: "lease"; nonce: number; status: number; mediaKind: number; leaseId: number; codec: number; width: number; height: number; fps: number; initialCredit: number; } | { kind: "revoked"; leaseId: number; reason: number; } | { kind: "credit"; leaseId: number; bytes: number; flags: number; } | { kind: "portalRequest"; request: PortalRequest; } | { kind: "portalCancel"; requestId: number; reason: number; } | { kind: "mprisUpdate"; flags: number; records: MprisRecord[]; } | { kind: "mprisResult"; nonce: number; status: number; playerId: number; revision: number; } | { kind: "serverCapabilities"; videoCodecs: number; }; export declare function parseMediaControl(message: Uint8Array): ParsedControl | null; export declare function buildMediaCapabilitiesMessage(capabilities: MediaCapabilities): Uint8Array; export declare function buildMediaStartMessage(nonce: number, kind: "microphone" | "camera", codec: number, width?: number, height?: number, fps?: number): Uint8Array; export declare function buildMediaStopMessage(leaseId: number): Uint8Array; export declare function buildMediaDataMessage(fields: { leaseId: number; sequence: number; captureUs: number; kind: "microphone" | "camera"; codec: number; flags: number; fragmentIndex: number; fragmentCount: number; frameLength: number; data: Uint8Array; }): Uint8Array; export declare function buildMprisSubscribeMessage(enabled: boolean): Uint8Array; export declare function buildMprisActionMessage(nonce: number, playerId: number, action: MprisAction): Uint8Array; export declare function buildPortalReplyMessage(request: PortalRequest, decision: "deny" | "grant" | "cancelled", surfaceIds?: readonly number[], choices?: readonly PortalChoiceValue[]): Uint8Array; export declare function buildScreenCastStopMessage(sessionId: number): Uint8Array; export declare class MprisStore implements ReactiveStore { #private; get revision(): number; get players(): ReadonlyMap; get activePlayerId(): number | null; get activePlayer(): MprisPlayer | null; subscribe(listener: () => void): () => void; subscribe(enabled: boolean): void; setSender(sender: ((message: Uint8Array) => void) | null): void; select(playerId: number): Promise; act(playerId: number, action: MprisAction): Promise; positionUs(playerId: number, nowMs?: number): number; handle(control: ParsedControl): boolean; reconnect(): void; reset(error?: Error): void; } export declare function supportsOpusMicrophone(): boolean; /** Performs the asynchronous WebCodecs codec check without opening a device. */ export declare function probeOpusMicrophone(): Promise; type CameraWireCodec = 0 | 1 | 2 | 3 | 4; /** Human name for a wire codec — the negotiated answer is worth showing, not * just logging: "the camera is stuck on Motion JPEG" is unanswerable from a * panel that only reports the size and cadence it settled on. */ export declare function cameraCodecLabel(codec: number): string; /** * Bytes per second this camera configuration is expected to produce. * * The server sizes the lease window from the same arithmetic, so the two * agree on what a second of video costs — keep them in step. */ export declare function cameraBytesPerSecond(codec: CameraWireCodec, width: number, height: number, fps: number, scale?: number): number; /** * Chooses how hard the camera encoder should push, from whether the link is * keeping up. * * Dropping frames keeps the picture current but spends the whole shortfall * on stutter; encoding smaller frames instead spends it on detail, which is * the better trade for a webcam. So congestion should lower the bitrate, not * just thin the stream. * * The two arms are deliberately asymmetric in speed but both present: back * off quickly, because the delay is already being felt, and recover slowly, * because probing upward costs another round of congestion when it is wrong. * An arm that can only ever degrade is the failure this is written against — * a link that recovers has to be able to earn its quality back, or one bad * minute quietly sets the quality for the rest of the session. */ export declare class CameraRateGovernor { #private; static readonly MIN_SCALE = 0.25; static readonly MAX_SCALE = 1; static readonly BACKOFF = 0.75; static readonly RECOVER = 1.15; /** Consecutive clear intervals required before probing upward again. */ static readonly RECOVER_AFTER = 5; get scale(): number; /** Fold in one observation interval; returns the scale to encode at. */ observe(congested: boolean): number; reset(): void; } /** * Whether a keyframe's bitstream carries what the wire codec promises. * * Split out from the probe so it can be tested without a `VideoEncoder`: the * rule it encodes is the whole reason a browser keeps or loses a codec. */ export declare function cameraBitstreamMatchesCodec(codec: Exclude, data: Uint8Array): boolean; /** What a keyframe leaving the encoder is worth on the wire. `header`, when * present, is the parameter-set prefix to remember for the keyframes that * arrive without one. */ export type CameraKeyframeDecision = { action: "send"; data: Uint8Array; header: Uint8Array | null; } | { action: "reject"; } | { action: "drop"; }; /** * Prepare an encoded keyframe for the wire, or refuse it. * * The server decodes from a keyframe alone, so one must arrive self-contained: * an encoder that emits its parameter sets once has them prepended from * `cachedHeader`, and a keyframe with neither is dropped rather than sent as a * picture nothing can start from. * * The format check is deliberately the *same* rule the support probe uses — * [`cameraBitstreamMatchesCodec`], chroma rather than the exact profile. * Holding the live stream to a stricter rule than the probe is what kept macOS * on Motion JPEG after the probe was relaxed: VideoToolbox answers blit's * Baseline request with Main or High, the panel offered H.264 because the probe * now accepts that, and then the first keyframe of every session was rejected * here and took the lease down with it. * * Split out of the capture class so that rule can be tested without a * `VideoEncoder`, exactly as the probe's is. */ export declare function cameraKeyframeForWire(codec: Exclude, data: Uint8Array, cachedHeader: Uint8Array | null): CameraKeyframeDecision; /** * What a camera codec probe found, kept so a UI can say which side refused. * * "This browser cannot encode it or no desktop accepts it" is not a diagnosis, * and a camera silently pinned to Motion JPEG is exactly the case where the * difference matters: `config-unsupported` is a browser with no such encoder, * `no-keyframe` is one that accepted the config and produced nothing (a slow * or wedged hardware session), and `wrong-format` is one whose bitstream does * not carry the chroma the wire codec promises the server. */ export type CameraCodecProbeOutcome = "supported" | "no-webcodecs" | "no-test-frame" | "config-unsupported" | "encoder-error" | "no-keyframe" | "wrong-format"; /** The last probe result per wire codec. Motion JPEG never appears: it needs no * encoder, and [`supportsMjpegCamera`] is the whole of its support test. */ export declare function cameraCodecProbeOutcomes(): ReadonlyMap; /** * Probe exact camera encoder profiles. A bit is returned only after the * browser both accepts the requested config and emits the matching profile. * * Every codec's verdict is also recorded in [`cameraCodecProbeOutcomes`], so a * missing bit can be explained rather than merely reported. */ export declare function probeCameraCodecs(maxWidth?: number, maxHeight?: number, maxFps?: number): Promise; /** One line per camera codec, for a log or a bug report: what the browser said * when asked to encode it. */ export declare function cameraCodecProbeReport(): string; export declare class MediaStore implements ReactiveStore { #private; get revision(): number; get state(): DesktopMediaState; /** * Camera formats understood by the peer. Old servers never announce this, * so the safe initial value contains only the two legacy registry entries. */ get serverVideoCodecs(): number; get microphone(): MediaLeaseState; get camera(): MediaLeaseState; get cameraTrack(): MediaStreamTrack | null; get requests(): ReadonlyMap; subscribe(listener: () => void): () => void; setSender(sender: ((message: Uint8Array) => void) | null): void; /** * How to ask the transport what it still owes the network. * * Lease credit alone cannot keep the camera current: it is returned only * once the server has *decoded* a frame, so a whole window's worth can be * sitting in this socket before any of it is acknowledged, and every byte * of it is delay in front of the picture. The queue length is the one * number that says so while it is happening. */ setBackpressureProbe(probe: (() => number | undefined) | null): void; advertise(capabilities: MediaCapabilities): void; setCapabilities(capabilities: MediaCapabilities): void; startMicrophone(track: MediaStreamTrack, options?: MicrophoneOptions): Promise; startCamera(track: MediaStreamTrack, options?: CameraOptions): Promise; stop(kind: "microphone" | "camera"): void; onPortalRequest(listener: (request: PortalRequest) => void): () => void; reply(requestId: number, decision: "deny" | "grant" | "cancelled", surfaceIds?: readonly number[], choices?: readonly PortalChoiceValue[]): void; stopScreenCast(sessionId: number): void; handle(control: ParsedControl): boolean; reset(error?: Error): void; } export {}; //# sourceMappingURL=media.d.ts.map