@qencode/calls 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/call.d.ts ADDED
@@ -0,0 +1,255 @@
1
+ import { CallError } from './errors';
2
+ import { type CallCredential } from './credential';
3
+ import { type VideoProfileName, type VideoProfile } from './profiles';
4
+ import { type LatencyMode } from './latency';
5
+ import { type CallStats, type Quality, type Direction } from './stats';
6
+ import { type TelemetryFields } from './telemetry';
7
+ import { Devices, type DeviceList } from './devices';
8
+ import { type VideoHandle } from './render';
9
+ export type CallState = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'ended';
10
+ export type EndReason = 'left' | 'peerLeft' | 'roomClosed' | 'network' | 'credential' | 'error' | 'roomFull';
11
+ export type LogLevelName = 'debug' | 'info' | 'warn' | 'error' | 'silent';
12
+ export interface Peer {
13
+ identity: string;
14
+ name: string | null;
15
+ audioMuted: boolean;
16
+ videoMuted: boolean;
17
+ }
18
+ /** A data message from the peer. Strings and JSON objects arrive as sent; bytes arrive as Uint8Array. */
19
+ export type MessagePayload = string | Uint8Array | Record<string, unknown> | unknown[];
20
+ export interface ModerationEvent {
21
+ level: 'warn' | 'mute' | 'end';
22
+ classes: string[];
23
+ at: number;
24
+ }
25
+ /** Video codecs an app may ask for. H.264 is the default; the engine negotiates VP8 when a peer cannot decode the choice. */
26
+ export type VideoCodec = 'h264' | 'vp8' | 'vp9' | 'av1';
27
+ export declare const VIDEO_CODECS: readonly VideoCodec[];
28
+ /**
29
+ * Builds the video track to publish for a profile: size a canvas, open a screen, wrap a processed
30
+ * camera. Called on every publish and republish, so a profile switch reaches it with the new size.
31
+ */
32
+ export type VideoSourceFactory = (profile: VideoProfile) => MediaStreamTrack | Promise<MediaStreamTrack>;
33
+ export type VideoSource = MediaStreamTrack | VideoSourceFactory;
34
+ /** Which receiver property took the jitter buffer target; null before the first remote video track. */
35
+ export type JitterBufferSupport = 'jitterBufferTarget' | 'playoutDelayHint' | 'unsupported' | null;
36
+ /**
37
+ * App fields for every telemetry row, called once per row with its direction. `g2g_p50`, `g2g_p95`
38
+ * and `g2g_samples` (the app's own measured glass-to-glass latency, ms) are stored as columns;
39
+ * every other key lands in the row's `extra` object.
40
+ */
41
+ export type TelemetryExtra = (direction: Direction) => TelemetryFields | null | undefined;
42
+ export interface CallOptions {
43
+ /** Publish the microphone on connect. Default true. */
44
+ audio?: boolean;
45
+ /** Publish the camera on connect. Default true. */
46
+ video?: boolean;
47
+ /** Named media tier. Default p540_60. */
48
+ videoProfile?: VideoProfileName;
49
+ /** `lowest` (default) or `smooth`. */
50
+ latencyMode?: LatencyMode;
51
+ /** Pin a region name from the credential; default picks the fastest by probe. */
52
+ region?: string | null;
53
+ /** Post quality stats to Qencode every 5 s. Default true. */
54
+ telemetry?: boolean;
55
+ /** Resume after network changes for up to 60 s. Default true. */
56
+ autoReconnect?: boolean;
57
+ /** Default 'warn'. Never logs media or credentials. */
58
+ logLevel?: LogLevelName;
59
+ /** API base used only by telemetry. Default https://api.qencode.com, or the credential's api_base. */
60
+ apiBase?: string;
61
+ /** Initial camera and microphone device ids. */
62
+ cameraId?: string;
63
+ microphoneId?: string;
64
+ /** End the call when the peer leaves. Default true: a 1:1 call is over when one side is gone. */
65
+ endOnPeerLeft?: boolean;
66
+ /**
67
+ * Let the engine adapt what it sends and receives to how the video is displayed (default true).
68
+ * The receiver asks for the layer matching the rendered size, and a video element that is
69
+ * hidden or in a background tab pauses that track on the server. Set false to always
70
+ * receive and send the full profile regardless of visibility, as a measurement bench does.
71
+ */
72
+ adaptiveStream?: boolean;
73
+ /**
74
+ * Publish this track instead of opening the camera: a canvas, a screen, a processed camera. A
75
+ * factory is called with the profile on every publish and republish. The SDK stops a track a
76
+ * factory returned when it is replaced or the call ends; a track passed directly stays yours to
77
+ * stop. `devices.setCamera` does not apply while a custom source is published.
78
+ */
79
+ videoSource?: VideoSource;
80
+ /** Video codec to publish. Default `h264`. */
81
+ codec?: VideoCodec;
82
+ /** Override the profile's simulcast setting. Default follows the profile. */
83
+ simulcast?: boolean;
84
+ /** Jitter buffer target in ms, overriding the latency mode's. `setLatencyMode` clears it. */
85
+ jitterBufferTargetMs?: number;
86
+ /** Fields appended to every telemetry row. */
87
+ telemetryExtra?: TelemetryExtra;
88
+ /** Forces TURN relay, for measuring the relay path. */
89
+ forceRelay?: boolean;
90
+ }
91
+ type OptionalKey = 'cameraId' | 'microphoneId' | 'region' | 'forceRelay' | 'videoSource' | 'simulcast' | 'jitterBufferTargetMs' | 'telemetryExtra';
92
+ export type CallEvents = {
93
+ stateChanged: [CallState, EndReason | null];
94
+ peerJoined: [Peer];
95
+ peerLeft: [Peer];
96
+ remoteVideo: [VideoHandle | null];
97
+ remoteAudio: [VideoHandle | null];
98
+ peerMuted: ['audio' | 'video', boolean];
99
+ stats: [CallStats];
100
+ qualityChanged: [Quality, Direction];
101
+ message: [MessagePayload, boolean];
102
+ credentialExpiring: [number];
103
+ devicesChanged: [DeviceList];
104
+ moderation: [ModerationEvent];
105
+ error: [CallError];
106
+ };
107
+ /**
108
+ * One 1:1 call. Create it from a credential minted by your backend, attach the video handles to
109
+ * your views, and call `connect()`. The engine underneath is not part of this API.
110
+ */
111
+ export declare class Call {
112
+ static create(credential: CallCredential, options?: CallOptions): Call;
113
+ readonly devices: Devices;
114
+ readonly options: Readonly<Required<Omit<CallOptions, OptionalKey>> & Pick<CallOptions, OptionalKey>>;
115
+ private readonly emitter;
116
+ private cred;
117
+ private _state;
118
+ private _endReason;
119
+ private room;
120
+ private profile;
121
+ private latency;
122
+ private _codec;
123
+ private simulcastOverride;
124
+ private jbOverride;
125
+ private jbSupport;
126
+ private videoSource;
127
+ /** The MediaStreamTrack currently published from `videoSource`, and whether a factory made it (then the SDK stops it). */
128
+ private sourceTrack;
129
+ private sourceOwned;
130
+ private regionChoice;
131
+ private _peer;
132
+ private agentPresent;
133
+ private remoteVideoTrack;
134
+ private remoteAudioTrack;
135
+ private _localVideo;
136
+ private _remoteVideo;
137
+ private _remoteAudio;
138
+ private audioEl;
139
+ private statsTimer;
140
+ private expiryTimer;
141
+ private reconnectTimer;
142
+ private telemetry;
143
+ private readonly recvTracker;
144
+ private readonly sendTracker;
145
+ private readonly recvQuality;
146
+ private readonly sendQuality;
147
+ private peerRttMs;
148
+ private joinMs;
149
+ private _stats;
150
+ private msgTimes;
151
+ private leaving;
152
+ private pendingEnd;
153
+ private constructor();
154
+ get state(): CallState;
155
+ get endReason(): EndReason | null;
156
+ get peer(): Peer | null;
157
+ get localVideo(): VideoHandle | null;
158
+ get remoteVideo(): VideoHandle | null;
159
+ get remoteAudio(): VideoHandle | null;
160
+ get stats(): CallStats | null;
161
+ get videoProfile(): VideoProfileName;
162
+ get latencyMode(): LatencyMode;
163
+ get videoCodec(): VideoCodec;
164
+ /** Whether video is published with simulcast layers: the override when set, else the profile's setting. */
165
+ get simulcast(): boolean;
166
+ /** The jitter buffer target in force: the override when set, else the latency mode's. */
167
+ get jitterBufferTargetMs(): number;
168
+ get jitterBufferSupport(): JitterBufferSupport;
169
+ /** Region probe results in ms by name, or null when no probe ran (one region, or a pinned one). */
170
+ get regionProbe(): Record<string, number> | null;
171
+ get identity(): string;
172
+ get callId(): string | null;
173
+ get region(): string | null;
174
+ get credentialExpiresAt(): number | null;
175
+ get agentJoined(): boolean;
176
+ on<K extends keyof CallEvents>(event: K, handler: (...args: CallEvents[K]) => void): () => void;
177
+ once<K extends keyof CallEvents>(event: K, handler: (...args: CallEvents[K]) => void): () => void;
178
+ off<K extends keyof CallEvents>(event: K, handler: (...args: CallEvents[K]) => void): void;
179
+ connect(): Promise<void>;
180
+ leave(): Promise<void>;
181
+ /** Replace the credential (same call, same identity) so the next reconnect uses a fresh token. */
182
+ updateCredential(credential: CallCredential): void;
183
+ /**
184
+ * A media server refuses a join with a bare websocket close, and a browser never sees the
185
+ * reason. When the server's validate endpoint still accepts the very same credential, the
186
+ * refusal was not the token, the room or the network; on a two-person room it is capacity.
187
+ * One short request on the failure path only.
188
+ */
189
+ private joinRefusedByServer;
190
+ setMicrophoneEnabled(enabled: boolean): Promise<void>;
191
+ setCameraEnabled(enabled: boolean): Promise<void>;
192
+ /**
193
+ * Switch resolution, frame rate and bitrate cap mid-call. A simulcast change or a factory video
194
+ * source republishes the track; a camera restarts capture in place; a fixed custom track only
195
+ * gets the new encoding parameters.
196
+ */
197
+ setVideoProfile(name: VideoProfileName): Promise<void>;
198
+ setLatencyMode(mode: LatencyMode): Promise<void>;
199
+ /**
200
+ * Pin the receiver's jitter buffer target in ms regardless of the latency mode (0 asks the
201
+ * browser for its floor). Returns which receiver property took it; `unsupported` on Firefox.
202
+ * `setLatencyMode` clears the pin.
203
+ */
204
+ setJitterBufferTarget(ms: number): JitterBufferSupport;
205
+ /** Change the codec or the simulcast setting mid-call; the video track is republished. */
206
+ setVideoEncoding(opts: {
207
+ codec?: VideoCodec;
208
+ simulcast?: boolean | null;
209
+ }): Promise<void>;
210
+ /** Replace the video source mid-call; `null` returns to the camera. Republishes when video is published. */
211
+ setVideoSource(source: VideoSource | null): Promise<void>;
212
+ /** Send up to 15 KB to the peer. Strings and JSON-able objects arrive as sent; Uint8Array arrives as bytes. */
213
+ sendMessage(payload: MessagePayload, reliable?: boolean): Promise<void>;
214
+ private buildRoom;
215
+ private publishOptions;
216
+ private captureOptions;
217
+ private audioCaptureOptions;
218
+ private publishAudio;
219
+ private publishVideo;
220
+ /** Takes the track from `videoSource`; a factory's result is owned by the SDK, a passed track is not. */
221
+ private resolveSource;
222
+ private releaseSource;
223
+ private unpublishVideo;
224
+ /** Unpublish and publish again with the current source, codec, simulcast and profile. A muted camera stays off. */
225
+ private republishVideo;
226
+ private applyEncoding;
227
+ private applyJitterTarget;
228
+ private adoptExistingParticipants;
229
+ private countHumans;
230
+ private onParticipantConnected;
231
+ private onParticipantDisconnected;
232
+ private onTrackSubscribed;
233
+ private onTrackUnsubscribed;
234
+ private onMuteChange;
235
+ private onData;
236
+ private onReconnecting;
237
+ private onReconnected;
238
+ private onDisconnected;
239
+ private startStats;
240
+ private pollStats;
241
+ private extraFields;
242
+ private audioRoute;
243
+ private sendHello;
244
+ private scheduleExpiryWarning;
245
+ private ensureAudioElement;
246
+ private requireRoom;
247
+ private setState;
248
+ private endWith;
249
+ private finish;
250
+ private teardown;
251
+ }
252
+ /** Envelope: 1 tag byte (1 string, 2 json, 3 bytes) followed by the payload. */
253
+ export declare function encodeMessage(payload: MessagePayload): Uint8Array;
254
+ export declare function decodeMessage(bytes: Uint8Array): MessagePayload;
255
+ export {};
@@ -0,0 +1,42 @@
1
+ /** One region a credential may connect through. */
2
+ export interface Region {
3
+ name: string;
4
+ url: string;
5
+ }
6
+ /**
7
+ * The token response from `POST /v1/calls/{id}/tokens`, passed to the app unchanged by the
8
+ * customer's backend. Only `token` and `url` are required; the rest is read from the JWT
9
+ * payload when missing.
10
+ */
11
+ export interface CallCredential {
12
+ token: string;
13
+ url: string;
14
+ regions?: Region[];
15
+ identity?: string;
16
+ room_name?: string;
17
+ /** ISO 8601 string, epoch seconds, or epoch milliseconds. */
18
+ expires_at?: string | number;
19
+ call_id?: string;
20
+ /** Optional API base for telemetry; overrides the SDK default. */
21
+ api_base?: string;
22
+ }
23
+ export interface ParsedCredential {
24
+ token: string;
25
+ url: string;
26
+ regions: Region[];
27
+ identity: string;
28
+ roomName: string;
29
+ /** Epoch milliseconds, or null when unknown. */
30
+ expiresAt: number | null;
31
+ callId: string | null;
32
+ apiBase: string | null;
33
+ }
34
+ /** Fires `credentialExpiring` this long before `expiresAt`. */
35
+ export declare const EXPIRY_WARNING_MS: number;
36
+ export declare function parseCredential(input: CallCredential): ParsedCredential;
37
+ export declare function isExpired(c: ParsedCredential, now?: number): boolean;
38
+ /** Milliseconds until the expiry warning should fire; 0 when already due; null when unknown. */
39
+ export declare function msUntilExpiryWarning(c: ParsedCredential, now?: number): number | null;
40
+ export declare function parseExpiry(v: string | number | undefined): number | null;
41
+ /** Decodes the JWT payload without verifying the signature; the media server verifies it. */
42
+ export declare function decodeJwtPayload(token: string): Record<string, unknown>;
@@ -0,0 +1,40 @@
1
+ import type { Room } from 'livekit-client';
2
+ export interface DeviceInfo {
3
+ id: string;
4
+ label: string;
5
+ }
6
+ export interface DeviceList {
7
+ cameras: DeviceInfo[];
8
+ microphones: DeviceInfo[];
9
+ speakers: DeviceInfo[];
10
+ }
11
+ /**
12
+ * Device selection. Labels are empty until the first getUserMedia permission is granted; after
13
+ * `connect()` the list is complete. Speaker selection needs `setSinkId`, which Safari lacks.
14
+ */
15
+ export declare class Devices {
16
+ private readonly emitter;
17
+ private room;
18
+ private readonly audioElements;
19
+ private speakerId;
20
+ private customVideo;
21
+ private readonly onDeviceChange;
22
+ /** @internal */
23
+ _attachRoom(room: Room | null): void;
24
+ /** @internal The call publishes a custom video source, so the engine must not replace it with a camera. */
25
+ _setCustomVideo(on: boolean): void;
26
+ /** @internal */
27
+ _registerAudioElement(el: HTMLMediaElement): void;
28
+ /** @internal */
29
+ _start(): void;
30
+ /** @internal */
31
+ _stop(): void;
32
+ list(): Promise<DeviceList>;
33
+ get canSelectSpeaker(): boolean;
34
+ setCamera(deviceId: string): Promise<void>;
35
+ setMicrophone(deviceId: string): Promise<void>;
36
+ setSpeaker(deviceId: string): Promise<void>;
37
+ onChange(handler: (list: DeviceList) => void): () => void;
38
+ private switch;
39
+ private applySink;
40
+ }
@@ -0,0 +1,29 @@
1
+ /** Stable error codes, identical on every platform (web, iOS, Android). */
2
+ export type CallErrorCode = 'credentialInvalid' | 'credentialExpired' | 'roomFull' | 'roomClosed' | 'permissionDenied' | 'deviceUnavailable' | 'network' | 'unsupported' | 'internal';
3
+ export declare class CallError extends Error {
4
+ readonly code: CallErrorCode;
5
+ /** Whether trying again, possibly after user action, can succeed. */
6
+ readonly retryable: boolean;
7
+ readonly cause?: unknown;
8
+ constructor(code: CallErrorCode, message: string, opts?: {
9
+ retryable?: boolean;
10
+ cause?: unknown;
11
+ });
12
+ }
13
+ export declare function isCallError(e: unknown): e is CallError;
14
+ /**
15
+ * Map an engine or browser error to a CallError. Engine mapping (livekit-client 2.22):
16
+ *
17
+ * - message matches "room is full" / "max participants" -> roomFull
18
+ * - ConnectionError NotAllowed (HTTP 401/403 on join) -> credentialInvalid
19
+ * - ConnectionError ServerUnreachable / WebSocket / Timeout -> network
20
+ * (Call.connect() turns a WebSocket refusal into roomFull when the server's validate
21
+ * endpoint still accepts the credential; the server sends no reason a browser can read)
22
+ * - ConnectionError ServiceNotFound (wrong URL path) -> credentialInvalid (the url in the credential is wrong)
23
+ * - ConnectionError Cancelled / LeaveRequest -> internal, not retryable (connect() was abandoned by leave())
24
+ * - ConnectionError InternalError -> internal
25
+ * - DOMException NotAllowedError / PermissionDeniedError -> permissionDenied
26
+ * - DOMException NotFoundError / NotReadableError / OverconstrainedError / AbortError -> deviceUnavailable
27
+ * - anything else -> internal, with the original message attached
28
+ */
29
+ export declare function mapEngineError(e: unknown): CallError;
@@ -0,0 +1,11 @@
1
+ /** Minimal typed event emitter. Handlers run synchronously on the calling thread; a throwing
2
+ * handler is reported through console.error and never breaks the emitter or the SDK. */
3
+ export type Listener<T extends unknown[]> = (...args: T) => void;
4
+ export declare class Emitter<M extends Record<string, unknown[]>> {
5
+ private readonly handlers;
6
+ on<K extends keyof M>(event: K, handler: Listener<M[K]>): () => void;
7
+ once<K extends keyof M>(event: K, handler: Listener<M[K]>): () => void;
8
+ off<K extends keyof M>(event: K, handler: Listener<M[K]>): void;
9
+ emit<K extends keyof M>(event: K, ...args: M[K]): void;
10
+ removeAll(): void;
11
+ }
@@ -0,0 +1,16 @@
1
+ export { Call, encodeMessage, decodeMessage } from './call';
2
+ export type { CallOptions, CallEvents, CallState, EndReason, Peer, MessagePayload, ModerationEvent, LogLevelName, VideoCodec, VideoSource, VideoSourceFactory, JitterBufferSupport, TelemetryExtra } from './call';
3
+ export { VIDEO_CODECS } from './call';
4
+ export type { TelemetryFields } from './telemetry';
5
+ export { CallError, isCallError } from './errors';
6
+ export type { CallErrorCode } from './errors';
7
+ export type { CallCredential, Region } from './credential';
8
+ export { PROFILES, DEFAULT_PROFILE } from './profiles';
9
+ export type { VideoProfileName, VideoProfile } from './profiles';
10
+ export { LATENCY_SETTINGS, DEFAULT_LATENCY_MODE } from './latency';
11
+ export type { LatencyMode, LatencySettings } from './latency';
12
+ export type { CallStats, RecvStats, SendStats, Quality, Direction, Transport, CandidateType, AudioRoute } from './stats';
13
+ export type { DeviceInfo, DeviceList } from './devices';
14
+ export type { VideoHandle } from './render';
15
+ export { QencodeVideoElement, registerVideoElement } from './render';
16
+ export { SDK_VERSION } from './version';
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Latency modes move several engine knobs together.
3
+ *
4
+ * - `lowest`: jitter buffer target at the platform minimum (0 ms asks the browser for its floor),
5
+ * degradation drops resolution before frame rate, 10 ms Opus frames where the platform lets us.
6
+ * - `smooth`: jitter buffer target 150 ms, degradation drops frame rate before resolution,
7
+ * 20 ms Opus frames.
8
+ */
9
+ export type LatencyMode = 'lowest' | 'smooth';
10
+ export interface LatencySettings {
11
+ jitterBufferTargetMs: number;
12
+ degradationPreference: RTCDegradationPreference;
13
+ audioFrameMs: 10 | 20;
14
+ }
15
+ export declare const LATENCY_SETTINGS: Readonly<Record<LatencyMode, LatencySettings>>;
16
+ export declare const DEFAULT_LATENCY_MODE: LatencyMode;
17
+ export declare function latencySettings(mode: LatencyMode | undefined): LatencySettings;
18
+ /**
19
+ * Applies a jitter buffer target to a receiver. Returns which property took it:
20
+ * `jitterBufferTarget` (Chromium 108+, Safari 17.4+), `playoutDelayHint` (older Chromium), or
21
+ * `null` when the browser exposes neither (Firefox). Never throws.
22
+ */
23
+ export declare function applyJitterBufferTarget(receiver: RTCRtpReceiver | undefined | null, ms: number): 'jitterBufferTarget' | 'playoutDelayHint' | null;
24
+ /** Applies a degradation preference to a live sender without republishing. Returns false when unsupported. */
25
+ export declare function applyDegradationPreference(sender: RTCRtpSender | undefined | null, pref: RTCDegradationPreference): Promise<boolean>;
@@ -0,0 +1,18 @@
1
+ /** Named media tiers. Quality, cost and the pricing tier line up on these; apps pick a name, never raw constraints. */
2
+ export type VideoProfileName = 'audioOnly' | 'p360_30' | 'p540_30' | 'p540_60' | 'p720_30' | 'p720_60' | 'p1080_30';
3
+ export interface VideoProfile {
4
+ name: VideoProfileName;
5
+ width: number;
6
+ height: number;
7
+ fps: number;
8
+ /** Encoder bitrate cap in bits per second. */
9
+ maxBitrate: number;
10
+ /** Simulcast layers are published at 720p and above; below, the single layer is cheaper to encode. */
11
+ simulcast: boolean;
12
+ video: boolean;
13
+ }
14
+ export declare const PROFILES: Readonly<Record<VideoProfileName, VideoProfile>>;
15
+ export declare const DEFAULT_PROFILE: VideoProfileName;
16
+ /** Opus bitrate for the microphone track, bits per second. */
17
+ export declare const AUDIO_BITRATE = 48000;
18
+ export declare function resolveProfile(name: VideoProfileName | undefined): VideoProfile;