@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/LICENSE +202 -0
- package/README.md +242 -0
- package/dist/call.d.ts +255 -0
- package/dist/credential.d.ts +42 -0
- package/dist/devices.d.ts +40 -0
- package/dist/errors.d.ts +29 -0
- package/dist/events.d.ts +11 -0
- package/dist/index.d.ts +16 -0
- package/dist/latency.d.ts +25 -0
- package/dist/profiles.d.ts +18 -0
- package/dist/qencode-calls.esm.bundle.js +47 -0
- package/dist/qencode-calls.esm.bundle.js.map +7 -0
- package/dist/qencode-calls.esm.js +9 -0
- package/dist/qencode-calls.esm.js.map +7 -0
- package/dist/qencode-calls.umd.js +47 -0
- package/dist/qencode-calls.umd.js.map +7 -0
- package/dist/regions.d.ts +22 -0
- package/dist/render.d.ts +35 -0
- package/dist/stats.d.ts +143 -0
- package/dist/telemetry.d.ts +73 -0
- package/dist/version.d.ts +2 -0
- package/package.json +73 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Region } from './credential';
|
|
2
|
+
export interface RegionChoice {
|
|
3
|
+
region: Region;
|
|
4
|
+
/** Probe result per region name in milliseconds; Infinity when unreachable. Empty when no probe ran. */
|
|
5
|
+
probe: Record<string, number>;
|
|
6
|
+
/** How the choice was made. */
|
|
7
|
+
how: 'single' | 'pinned' | 'probe' | 'fallback';
|
|
8
|
+
}
|
|
9
|
+
export interface ProbeOptions {
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
fetchFn?: typeof fetch;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
}
|
|
14
|
+
/** Turns a signaling URL into the media node's HTTPS root, which LiveKit answers with "OK". */
|
|
15
|
+
export declare function probeUrl(signalingUrl: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Times one request to a region after one warm-up request. `no-cors` so the timing works even
|
|
18
|
+
* when the node sends no CORS headers; the body is not readable and does not need to be.
|
|
19
|
+
*/
|
|
20
|
+
export declare function probeRegion(region: Region, opts?: ProbeOptions): Promise<number>;
|
|
21
|
+
/** Picks the region to connect through. Pinned wins; one region skips the probe; otherwise the fastest probe. */
|
|
22
|
+
export declare function chooseRegion(regions: Region[], pinned?: string | null, opts?: ProbeOptions): Promise<RegionChoice>;
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** A renderable track. `attach` returns the element for chaining; `detach` with no element releases all. */
|
|
2
|
+
export interface VideoHandle {
|
|
3
|
+
readonly kind: 'video' | 'audio';
|
|
4
|
+
readonly mediaStreamTrack: MediaStreamTrack;
|
|
5
|
+
/** Accepts a `<video>`/`<audio>` element or a `<qencode-video>` element; returns the media element that plays it. */
|
|
6
|
+
attach(element: HTMLMediaElement | QencodeVideoElement): HTMLMediaElement;
|
|
7
|
+
detach(element?: HTMLMediaElement | QencodeVideoElement): void;
|
|
8
|
+
}
|
|
9
|
+
type EngineTrack = {
|
|
10
|
+
kind: string;
|
|
11
|
+
mediaStreamTrack: MediaStreamTrack;
|
|
12
|
+
attach(element: HTMLMediaElement): HTMLMediaElement;
|
|
13
|
+
detach(element?: HTMLMediaElement): HTMLMediaElement | HTMLMediaElement[];
|
|
14
|
+
};
|
|
15
|
+
/** @internal Wraps an engine track so no engine type reaches the public API. */
|
|
16
|
+
export declare function wrapTrack(track: EngineTrack): VideoHandle;
|
|
17
|
+
/**
|
|
18
|
+
* `<qencode-video>`: a frame that renders a VideoHandle at its own aspect ratio.
|
|
19
|
+
* Attributes: `mirror` (self view), `fit="contain|cover"`. Property: `track` (VideoHandle | null).
|
|
20
|
+
* Audio is never played by this element; the SDK plays remote audio itself.
|
|
21
|
+
*/
|
|
22
|
+
export declare class QencodeVideoElement extends HTMLElement {
|
|
23
|
+
private _track;
|
|
24
|
+
private readonly video;
|
|
25
|
+
constructor();
|
|
26
|
+
/** The inner `<video>` that plays the track. */
|
|
27
|
+
get videoElement(): HTMLVideoElement;
|
|
28
|
+
get track(): VideoHandle | null;
|
|
29
|
+
set track(t: VideoHandle | null);
|
|
30
|
+
disconnectedCallback(): void;
|
|
31
|
+
private updateAspect;
|
|
32
|
+
}
|
|
33
|
+
/** Registers `<qencode-video>` once; safe to call repeatedly and outside browsers. */
|
|
34
|
+
export declare function registerVideoElement(tag?: string): boolean;
|
|
35
|
+
export {};
|
package/dist/stats.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-second quality numbers from WebRTC getStats, ported from the bench page. Everything here is
|
|
3
|
+
* pure over an RTCStatsReport-like map so it can be tested without a browser.
|
|
4
|
+
*/
|
|
5
|
+
export type Transport = 'udp' | 'tcp' | 'relay-udp' | 'relay-tcp';
|
|
6
|
+
export type CandidateType = 'host' | 'srflx' | 'prflx' | 'relay';
|
|
7
|
+
export type AudioRoute = 'speaker' | 'earpiece' | 'wired' | 'bluetooth' | 'unknown';
|
|
8
|
+
export type Quality = 'good' | 'fair' | 'poor';
|
|
9
|
+
export type Direction = 'recv' | 'send';
|
|
10
|
+
/** What this participant receives from the peer. */
|
|
11
|
+
export interface RecvStats {
|
|
12
|
+
rttMs: number | null;
|
|
13
|
+
jitterBufferMs: number | null;
|
|
14
|
+
decodeMs: number | null;
|
|
15
|
+
fps: number | null;
|
|
16
|
+
width: number | null;
|
|
17
|
+
height: number | null;
|
|
18
|
+
kbps: number | null;
|
|
19
|
+
lossPct: number | null;
|
|
20
|
+
jitterMs: number | null;
|
|
21
|
+
freezes: number | null;
|
|
22
|
+
freezeMs: number | null;
|
|
23
|
+
/** Receive-to-render time per frame: Δ`totalProcessingDelay` / Δ`framesDecoded`. */
|
|
24
|
+
processingMs: number | null;
|
|
25
|
+
/**
|
|
26
|
+
* Sender capture to local now through the abs-capture-time header extension, when the
|
|
27
|
+
* browser and the media node carry it (Chromium; null elsewhere). A cross-check for the
|
|
28
|
+
* estimate, not a substitute for a measured glass-to-glass number.
|
|
29
|
+
*/
|
|
30
|
+
absCaptureLatencyMs: number | null;
|
|
31
|
+
codec: string | null;
|
|
32
|
+
transport: Transport | null;
|
|
33
|
+
candidateType: CandidateType | null;
|
|
34
|
+
}
|
|
35
|
+
/** What this participant sends. */
|
|
36
|
+
export interface SendStats {
|
|
37
|
+
rttMs: number | null;
|
|
38
|
+
encodeMs: number | null;
|
|
39
|
+
fps: number | null;
|
|
40
|
+
width: number | null;
|
|
41
|
+
height: number | null;
|
|
42
|
+
kbps: number | null;
|
|
43
|
+
/** The encoder's current bitrate target, kbps. */
|
|
44
|
+
targetKbps: number | null;
|
|
45
|
+
qualityLimitation: string | null;
|
|
46
|
+
/** Loss the media node reports for our outbound stream (remote-inbound-rtp), percent. */
|
|
47
|
+
lossPct: number | null;
|
|
48
|
+
codec: string | null;
|
|
49
|
+
transport: Transport | null;
|
|
50
|
+
candidateType: CandidateType | null;
|
|
51
|
+
}
|
|
52
|
+
/** One snapshot, emitted once per second on `stats`. */
|
|
53
|
+
export interface CallStats {
|
|
54
|
+
/** Epoch milliseconds. */
|
|
55
|
+
ts: number;
|
|
56
|
+
recv: RecvStats;
|
|
57
|
+
send: SendStats;
|
|
58
|
+
/** The peer's own RTT to the media node, exchanged over the data channel; null until received. */
|
|
59
|
+
peerRttMs: number | null;
|
|
60
|
+
/** rtt/2 + peerRtt/2 + jitter buffer + decode + one frame interval. An estimate, not a measurement. */
|
|
61
|
+
estimatedLatencyMs: number | null;
|
|
62
|
+
quality: {
|
|
63
|
+
recv: Quality;
|
|
64
|
+
send: Quality;
|
|
65
|
+
};
|
|
66
|
+
region: string | null;
|
|
67
|
+
nodeId: string | null;
|
|
68
|
+
serverVersion: string | null;
|
|
69
|
+
joinMs: number | null;
|
|
70
|
+
audioRoute: AudioRoute;
|
|
71
|
+
}
|
|
72
|
+
type StatsLike = {
|
|
73
|
+
forEach(cb: (s: any) => void): void;
|
|
74
|
+
get(id: string): any;
|
|
75
|
+
};
|
|
76
|
+
/** Delta tracker across polls; one instance per direction per call. */
|
|
77
|
+
export declare class DeltaTracker {
|
|
78
|
+
private prev;
|
|
79
|
+
/** Returns per-interval deltas of the given counters, or null on the first sample. */
|
|
80
|
+
step(nowMs: number, counters: Record<string, number | undefined>): {
|
|
81
|
+
dt: number;
|
|
82
|
+
d: (k: string) => number;
|
|
83
|
+
} | null;
|
|
84
|
+
reset(): void;
|
|
85
|
+
}
|
|
86
|
+
export declare function pickSelectedPair(report: StatsLike): any | null;
|
|
87
|
+
export declare function transportOf(report: StatsLike, pair: any | null): {
|
|
88
|
+
transport: Transport | null;
|
|
89
|
+
candidateType: CandidateType | null;
|
|
90
|
+
rttMs: number | null;
|
|
91
|
+
};
|
|
92
|
+
export declare function collectRecv(report: StatsLike, tracker: DeltaTracker, nowMs: number): RecvStats;
|
|
93
|
+
export declare function collectSend(report: StatsLike, tracker: DeltaTracker, nowMs: number): SendStats;
|
|
94
|
+
type SyncSource = {
|
|
95
|
+
timestamp: number;
|
|
96
|
+
captureTimestamp?: number;
|
|
97
|
+
senderCaptureTimeOffset?: number;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Latency from the sender's capture clock to now, from `RTCRtpReceiver.getSynchronizationSources()`
|
|
101
|
+
* when the abs-capture-time extension is present. `captureTimestamp` and `senderCaptureTimeOffset`
|
|
102
|
+
* are milliseconds since the NTP epoch on the sender's clock (already offset-corrected by the
|
|
103
|
+
* browser); `timestamp` is when the last frame reached the track, relative either to the epoch
|
|
104
|
+
* (spec) or to the page's time origin (some builds), so both are accepted. Null when absent.
|
|
105
|
+
*/
|
|
106
|
+
export declare function absCaptureLatency(sources: ArrayLike<SyncSource> | null | undefined, unixNowMs: number, perfNowMs: number): number | null;
|
|
107
|
+
/** rtt/2 + peerRtt/2 + jitter buffer + decode + one frame interval. Null until the pieces exist. */
|
|
108
|
+
export declare function estimateLatency(p: {
|
|
109
|
+
rttMs: number | null;
|
|
110
|
+
peerRttMs: number | null;
|
|
111
|
+
jitterBufferMs: number | null;
|
|
112
|
+
decodeMs: number | null;
|
|
113
|
+
fps: number | null;
|
|
114
|
+
}): number | null;
|
|
115
|
+
/**
|
|
116
|
+
* good / fair / poor with hysteresis: a new level must be observed on two consecutive polls
|
|
117
|
+
* before it is reported, so a single bad second does not flip the indicator.
|
|
118
|
+
*
|
|
119
|
+
* poor: loss > 3 % or jitter buffer > 200 ms or any freeze in the interval
|
|
120
|
+
* fair: loss > 1 % or jitter buffer > 120 ms or rtt > 300 ms
|
|
121
|
+
* good: otherwise
|
|
122
|
+
*/
|
|
123
|
+
export declare class QualityTracker {
|
|
124
|
+
private current;
|
|
125
|
+
private candidate;
|
|
126
|
+
private streak;
|
|
127
|
+
static classify(s: {
|
|
128
|
+
lossPct: number | null;
|
|
129
|
+
jitterBufferMs: number | null;
|
|
130
|
+
freezes: number | null;
|
|
131
|
+
rttMs: number | null;
|
|
132
|
+
}): Quality;
|
|
133
|
+
/** Feed one poll; returns the reported level after hysteresis. */
|
|
134
|
+
update(s: {
|
|
135
|
+
lossPct: number | null;
|
|
136
|
+
jitterBufferMs: number | null;
|
|
137
|
+
freezes: number | null;
|
|
138
|
+
rttMs: number | null;
|
|
139
|
+
}): Quality;
|
|
140
|
+
get value(): Quality;
|
|
141
|
+
reset(): void;
|
|
142
|
+
}
|
|
143
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { CallStats, Direction } from './stats';
|
|
2
|
+
/** App-supplied fields for one telemetry row; see `Telemetry.push`. */
|
|
3
|
+
export type TelemetryFields = Record<string, unknown>;
|
|
4
|
+
/** Contract columns an app may fill: its own measured glass-to-glass latency over the last window. */
|
|
5
|
+
export declare const MEASURED_COLUMNS: ReadonlySet<string>;
|
|
6
|
+
/** One row of the calls stats contract (`POST /v1/calls/{id}/stats`). */
|
|
7
|
+
export interface TelemetrySample {
|
|
8
|
+
ts: number;
|
|
9
|
+
direction: Direction;
|
|
10
|
+
peer_identity?: string | null;
|
|
11
|
+
/** App-measured glass-to-glass latency, ms, over the app's own window; the SDK never fills these. */
|
|
12
|
+
g2g_p50?: number | null;
|
|
13
|
+
g2g_p95?: number | null;
|
|
14
|
+
g2g_samples?: number | null;
|
|
15
|
+
rtt_ms?: number | null;
|
|
16
|
+
net_one_way_ms?: number | null;
|
|
17
|
+
jb_ms?: number | null;
|
|
18
|
+
decode_ms?: number | null;
|
|
19
|
+
encode_ms?: number | null;
|
|
20
|
+
fps?: number | null;
|
|
21
|
+
kbps?: number | null;
|
|
22
|
+
loss_pct?: number | null;
|
|
23
|
+
jitter_ms?: number | null;
|
|
24
|
+
freezes?: number | null;
|
|
25
|
+
freeze_ms?: number | null;
|
|
26
|
+
transport?: string | null;
|
|
27
|
+
candidate_type?: string | null;
|
|
28
|
+
codec?: string | null;
|
|
29
|
+
width?: number | null;
|
|
30
|
+
height?: number | null;
|
|
31
|
+
server_region?: string | null;
|
|
32
|
+
server_version?: string | null;
|
|
33
|
+
node_id?: string | null;
|
|
34
|
+
join_ms?: number | null;
|
|
35
|
+
extra?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
export interface TelemetryOptions {
|
|
38
|
+
apiBase: string;
|
|
39
|
+
callId: string | null;
|
|
40
|
+
token: string;
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
sdkVersion: string;
|
|
43
|
+
/** Flush interval in ms; 5000 by default. */
|
|
44
|
+
intervalMs?: number;
|
|
45
|
+
fetchFn?: typeof fetch;
|
|
46
|
+
userAgent?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Batches stats snapshots and posts them every 5 s with the participant token. Silent when
|
|
50
|
+
* disabled or when the credential carries no call id (manual credentials). Never throws.
|
|
51
|
+
*/
|
|
52
|
+
export declare class Telemetry {
|
|
53
|
+
private readonly opts;
|
|
54
|
+
private queue;
|
|
55
|
+
private timer;
|
|
56
|
+
private firstPost;
|
|
57
|
+
private joinMs;
|
|
58
|
+
readonly enabled: boolean;
|
|
59
|
+
readonly url: string | null;
|
|
60
|
+
constructor(opts: TelemetryOptions);
|
|
61
|
+
start(joinMs: number | null): void;
|
|
62
|
+
/**
|
|
63
|
+
* Converts one snapshot into the two contract rows and queues them. `extra` adds app fields per
|
|
64
|
+
* direction: the measured-latency columns (`MEASURED_COLUMNS`) are stored as columns, anything
|
|
65
|
+
* else goes under `extra` next to the SDK's own keys, which win on a name clash.
|
|
66
|
+
*/
|
|
67
|
+
push(s: CallStats, peerIdentity: string | null, extra?: {
|
|
68
|
+
recv?: TelemetryFields | null;
|
|
69
|
+
send?: TelemetryFields | null;
|
|
70
|
+
}): void;
|
|
71
|
+
flush(): Promise<void>;
|
|
72
|
+
stop(): Promise<void>;
|
|
73
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qencode/calls",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Qencode Calls web SDK: low-latency 1:1 audio and video calls inside your own UI.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/qencode-calls.umd.js",
|
|
8
|
+
"module": "./dist/qencode-calls.esm.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/qencode-calls.esm.js",
|
|
14
|
+
"require": "./dist/qencode-calls.umd.js",
|
|
15
|
+
"default": "./dist/qencode-calls.esm.js"
|
|
16
|
+
},
|
|
17
|
+
"./bundle": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/qencode-calls.esm.bundle.js",
|
|
20
|
+
"default": "./dist/qencode-calls.esm.bundle.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": [
|
|
29
|
+
"./dist/qencode-calls.umd.js"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
33
|
+
"build:esm": "esbuild src/index.ts --bundle --format=esm --platform=browser --target=es2020 --external:livekit-client --outfile=dist/qencode-calls.esm.js --sourcemap --minify",
|
|
34
|
+
"build:umd": "esbuild src/index.ts --bundle --format=iife --global-name=QencodeCalls --platform=browser --target=es2020 --outfile=dist/qencode-calls.umd.js --sourcemap --minify",
|
|
35
|
+
"build:types": "tsc -p tsconfig.build.json",
|
|
36
|
+
"build": "npm run typecheck && npm run build:esm && npm run build:esm-bundle && npm run build:umd && npm run build:types",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"build:esm-bundle": "esbuild src/index.ts --bundle --format=esm --platform=browser --target=es2020 --outfile=dist/qencode-calls.esm.bundle.js --sourcemap --minify"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"livekit-client": "2.22.2"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"esbuild": "^0.24.0",
|
|
46
|
+
"jsdom": "^25.0.0",
|
|
47
|
+
"typescript": "^5.6.0",
|
|
48
|
+
"vitest": "^2.1.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/Qencode-Corp/calls-sdk.git",
|
|
56
|
+
"directory": "web"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://github.com/Qencode-Corp/calls-sdk#readme",
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/Qencode-Corp/calls-sdk/issues"
|
|
61
|
+
},
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
},
|
|
65
|
+
"keywords": [
|
|
66
|
+
"qencode",
|
|
67
|
+
"webrtc",
|
|
68
|
+
"video-calls",
|
|
69
|
+
"calls",
|
|
70
|
+
"sdk",
|
|
71
|
+
"livekit"
|
|
72
|
+
]
|
|
73
|
+
}
|