@voltkit-rtc/client 0.7.3 → 0.9.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/CHANGELOG.md +86 -0
- package/README.md +346 -2
- package/dist/cjs/adaptive.d.ts +104 -0
- package/dist/cjs/adaptive.d.ts.map +1 -0
- package/dist/cjs/adaptive.js +263 -0
- package/dist/cjs/adaptive.js.map +1 -0
- package/dist/cjs/devices.d.ts +42 -0
- package/dist/cjs/devices.d.ts.map +1 -0
- package/dist/cjs/devices.js +56 -0
- package/dist/cjs/devices.js.map +1 -0
- package/dist/cjs/diagnostics.d.ts +65 -0
- package/dist/cjs/diagnostics.d.ts.map +1 -0
- package/dist/cjs/diagnostics.js +69 -0
- package/dist/cjs/diagnostics.js.map +1 -0
- package/dist/cjs/effects-core.d.ts +95 -0
- package/dist/cjs/effects-core.d.ts.map +1 -0
- package/dist/cjs/effects-core.js +154 -0
- package/dist/cjs/effects-core.js.map +1 -0
- package/dist/cjs/effects-gl.d.ts +79 -0
- package/dist/cjs/effects-gl.d.ts.map +1 -0
- package/dist/cjs/effects-gl.js +351 -0
- package/dist/cjs/effects-gl.js.map +1 -0
- package/dist/cjs/fallback.d.ts +81 -0
- package/dist/cjs/fallback.d.ts.map +1 -0
- package/dist/cjs/fallback.js +148 -0
- package/dist/cjs/fallback.js.map +1 -0
- package/dist/cjs/health.d.ts +70 -0
- package/dist/cjs/health.d.ts.map +1 -0
- package/dist/cjs/health.js +116 -0
- package/dist/cjs/health.js.map +1 -0
- package/dist/cjs/index.d.ts +396 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +1367 -81
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/link.d.ts +99 -0
- package/dist/cjs/link.d.ts.map +1 -0
- package/dist/cjs/link.js +219 -0
- package/dist/cjs/link.js.map +1 -0
- package/dist/cjs/noise-suppression.d.ts +44 -0
- package/dist/cjs/noise-suppression.d.ts.map +1 -0
- package/dist/cjs/noise-suppression.js +103 -0
- package/dist/cjs/noise-suppression.js.map +1 -0
- package/dist/cjs/preflight.d.ts +79 -0
- package/dist/cjs/preflight.d.ts.map +1 -0
- package/dist/cjs/preflight.js +159 -0
- package/dist/cjs/preflight.js.map +1 -0
- package/dist/cjs/processors.d.ts +34 -0
- package/dist/cjs/processors.d.ts.map +1 -0
- package/dist/cjs/processors.js +10 -0
- package/dist/cjs/processors.js.map +1 -0
- package/dist/cjs/speakers.d.ts +34 -0
- package/dist/cjs/speakers.d.ts.map +1 -0
- package/dist/cjs/speakers.js +71 -0
- package/dist/cjs/speakers.js.map +1 -0
- package/dist/cjs/video-effects.d.ts +93 -0
- package/dist/cjs/video-effects.d.ts.map +1 -0
- package/dist/cjs/video-effects.js +493 -0
- package/dist/cjs/video-effects.js.map +1 -0
- package/dist/esm/adaptive.d.ts +104 -0
- package/dist/esm/adaptive.d.ts.map +1 -0
- package/dist/esm/adaptive.js +256 -0
- package/dist/esm/adaptive.js.map +1 -0
- package/dist/esm/devices.d.ts +42 -0
- package/dist/esm/devices.d.ts.map +1 -0
- package/dist/esm/devices.js +50 -0
- package/dist/esm/devices.js.map +1 -0
- package/dist/esm/diagnostics.d.ts +65 -0
- package/dist/esm/diagnostics.d.ts.map +1 -0
- package/dist/esm/diagnostics.js +65 -0
- package/dist/esm/diagnostics.js.map +1 -0
- package/dist/esm/effects-core.d.ts +95 -0
- package/dist/esm/effects-core.d.ts.map +1 -0
- package/dist/esm/effects-core.js +142 -0
- package/dist/esm/effects-core.js.map +1 -0
- package/dist/esm/effects-gl.d.ts +79 -0
- package/dist/esm/effects-gl.d.ts.map +1 -0
- package/dist/esm/effects-gl.js +347 -0
- package/dist/esm/effects-gl.js.map +1 -0
- package/dist/esm/fallback.d.ts +81 -0
- package/dist/esm/fallback.d.ts.map +1 -0
- package/dist/esm/fallback.js +142 -0
- package/dist/esm/fallback.js.map +1 -0
- package/dist/esm/health.d.ts +70 -0
- package/dist/esm/health.d.ts.map +1 -0
- package/dist/esm/health.js +111 -0
- package/dist/esm/health.js.map +1 -0
- package/dist/esm/index.d.ts +396 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1365 -81
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/link.d.ts +99 -0
- package/dist/esm/link.d.ts.map +1 -0
- package/dist/esm/link.js +214 -0
- package/dist/esm/link.js.map +1 -0
- package/dist/esm/noise-suppression.d.ts +44 -0
- package/dist/esm/noise-suppression.d.ts.map +1 -0
- package/dist/esm/noise-suppression.js +99 -0
- package/dist/esm/noise-suppression.js.map +1 -0
- package/dist/esm/preflight.d.ts +79 -0
- package/dist/esm/preflight.d.ts.map +1 -0
- package/dist/esm/preflight.js +153 -0
- package/dist/esm/preflight.js.map +1 -0
- package/dist/esm/processors.d.ts +34 -0
- package/dist/esm/processors.d.ts.map +1 -0
- package/dist/esm/processors.js +9 -0
- package/dist/esm/processors.js.map +1 -0
- package/dist/esm/speakers.d.ts +34 -0
- package/dist/esm/speakers.d.ts.map +1 -0
- package/dist/esm/speakers.js +67 -0
- package/dist/esm/speakers.js.map +1 -0
- package/dist/esm/video-effects.d.ts +93 -0
- package/dist/esm/video-effects.d.ts.map +1 -0
- package/dist/esm/video-effects.js +488 -0
- package/dist/esm/video-effects.js.map +1 -0
- package/package.json +31 -1
- package/src/adaptive.ts +295 -0
- package/src/devices.ts +70 -0
- package/src/diagnostics.ts +94 -0
- package/src/effects-core.ts +177 -0
- package/src/effects-gl.ts +384 -0
- package/src/fallback.ts +169 -0
- package/src/health.ts +127 -0
- package/src/index.ts +1364 -81
- package/src/link.ts +255 -0
- package/src/noise-suppression.ts +135 -0
- package/src/preflight.ts +191 -0
- package/src/processors.ts +35 -0
- package/src/speakers.ts +66 -0
- package/src/video-effects.ts +552 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/link.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,8GAA8G;AAC9G,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAC1C,+FAA+F;AAC/F,eAAO,MAAM,kBAAkB,QAAS,CAAC;AACzC,6DAA6D;AAC7D,eAAO,MAAM,OAAO,QAAS,CAAC;AAC9B,sFAAsF;AACtF,eAAO,MAAM,eAAe,OAAO,CAAC;AACpC,qHAAqH;AACrH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEvG,mCAAmC;AACnC,MAAM,MAAM,cAAc;AACxB,qDAAqD;AACnD,WAAW;AACb,4EAA4E;GAC1E,aAAa;AACf,iHAAiH;GAC/G,SAAS,CAAC;AAEd,MAAM,WAAW,aAAa;IAC5B,mHAAmH;IACnH,MAAM,IAAI,OAAO,CAAC;IAClB,gFAAgF;IAChF,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACzD,6CAA6C;IAC7C,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,yGAAyG;IACzG,YAAY,CAAC,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAAC;IACxD,QAAQ,CAAC,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAC/C,UAAU,CAAC,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CACpC;AAWD,gFAAgF;AAChF,qBAAa,cAAc;IAKb,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgC;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0C;IACnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA4B;gBAE1B,IAAI,EAAE,aAAa;IAKhD,yFAAyF;IACzF,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,IAAI;IAoCxD,qFAAqF;IACrF,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAQjC,mDAAmD;IACnD,KAAK,IAAI,IAAI;IAIb,qFAAqF;IACrF,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO;YAI7B,OAAO;IAoCrB,OAAO,CAAC,OAAO;IAMf,OAAO,CAAC,MAAM;CAIf;AAED,MAAM,WAAW,YAAY;IAC3B,mFAAmF;IACnF,MAAM,IAAI,OAAO,CAAC;IAClB,sGAAsG;IACtG,IAAI,IAAI,IAAI,CAAC;IACb,wEAAwE;IACxE,IAAI,IAAI,IAAI,CAAC;IACb,GAAG,CAAC,IAAI,MAAM,CAAC;IACf,WAAW,CAAC,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAClD,aAAa,CAAC,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CACvC;AAED,kDAAkD;AAClD,qBAAa,cAAc;IAMb,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEN,IAAI,EAAE,YAAY;IAI/C,4CAA4C;IAC5C,KAAK,IAAI,IAAI;IAWb,IAAI,IAAI,IAAI;IAMZ,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,uCAAuC;IACvC,QAAQ,IAAI,IAAI;IAKhB,oFAAoF;IACpF,QAAQ,IAAI,IAAI;IAQhB,KAAK,IAAI,IAAI;CAed"}
|
package/dist/cjs/link.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Keeping the connection alive between drops (docs/implementation-progress/RTC_PERF_PROGRESS.md G1, G2).
|
|
4
|
+
*
|
|
5
|
+
* Two failures the socket's own close never reports:
|
|
6
|
+
*
|
|
7
|
+
* - **The media path dies while the socket lives** (Wi-Fi to mobile data, a NAT rebinding, UDP blocked mid-call).
|
|
8
|
+
* The video freezes for good. Twilio and LiveKit restart ICE on the same transports first -- new ICE credentials
|
|
9
|
+
* from the server, the same producers and consumers -- and rejoin only when that does not bring the path back.
|
|
10
|
+
* {@link MediaPathWatch} does the same: a transport `disconnected` for {@link DISCONNECTED_GRACE_MS} or `failed`
|
|
11
|
+
* restarts ICE; not `connected` within {@link RESTART_TIMEOUT_MS} of starting recovery (including the request), or a server without `restartIce`, is a
|
|
12
|
+
* full reconnect.
|
|
13
|
+
* - **The socket dies silently**: the network went away and TCP has not noticed yet, which can take minutes. The
|
|
14
|
+
* server pings, but a browser cannot see those. LiveKit pings from the client and reconnects when no pong comes.
|
|
15
|
+
* {@link SocketLiveness} sends `ping` after {@link IDLE_MS} with nothing received, and calls the socket dead when
|
|
16
|
+
* nothing at all arrives within {@link PING_TIMEOUT_MS} of it. Any frame counts, so an older server's
|
|
17
|
+
* `unknown-method` reply proves the socket as well as a pong.
|
|
18
|
+
*
|
|
19
|
+
* Pure: timers and the clock are injectable, so the decisions are unit-tested on their own.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.SocketLiveness = exports.MediaPathWatch = exports.LIVENESS_TICK_MS = exports.PING_TIMEOUT_MS = exports.IDLE_MS = exports.RESTART_TIMEOUT_MS = exports.DISCONNECTED_GRACE_MS = void 0;
|
|
23
|
+
/** A transport `disconnected` this long is restarted (it often recovers by itself within a second or two). */
|
|
24
|
+
exports.DISCONNECTED_GRACE_MS = 3000;
|
|
25
|
+
/** The whole ICE recovery, including the request, has this long before the session rejoins. */
|
|
26
|
+
exports.RESTART_TIMEOUT_MS = 10_000;
|
|
27
|
+
/** With nothing received for this long, the client pings. */
|
|
28
|
+
exports.IDLE_MS = 15_000;
|
|
29
|
+
/** A ping with nothing at all received for this long after it: the socket is dead. */
|
|
30
|
+
exports.PING_TIMEOUT_MS = 8000;
|
|
31
|
+
/** How often liveness is checked. Background tabs may run it less often; the decisions use the clock, not counts. */
|
|
32
|
+
exports.LIVENESS_TICK_MS = 5000;
|
|
33
|
+
/** Watches each media transport's connection state and recovers a dead path. */
|
|
34
|
+
class MediaPathWatch {
|
|
35
|
+
host;
|
|
36
|
+
entries = new Map();
|
|
37
|
+
setTimer;
|
|
38
|
+
clearTimer;
|
|
39
|
+
constructor(host) {
|
|
40
|
+
this.host = host;
|
|
41
|
+
this.setTimer = host.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
42
|
+
this.clearTimer = host.clearTimer ?? ((h) => clearTimeout(h));
|
|
43
|
+
}
|
|
44
|
+
/** A transport's connection state changed (mediasoup-client `connectionstatechange`). */
|
|
45
|
+
update(transportId, state) {
|
|
46
|
+
let entry = this.entries.get(transportId);
|
|
47
|
+
if (!entry) {
|
|
48
|
+
entry = { state, timer: null, restarting: false, lost: false, revision: 0 };
|
|
49
|
+
this.entries.set(transportId, entry);
|
|
50
|
+
}
|
|
51
|
+
entry.state = state;
|
|
52
|
+
switch (state) {
|
|
53
|
+
case 'connected':
|
|
54
|
+
entry.revision++;
|
|
55
|
+
this.cancel(entry);
|
|
56
|
+
entry.restarting = false;
|
|
57
|
+
this.setLost(transportId, entry, false);
|
|
58
|
+
return;
|
|
59
|
+
case 'closed':
|
|
60
|
+
this.forget(transportId);
|
|
61
|
+
return;
|
|
62
|
+
case 'disconnected':
|
|
63
|
+
// A restart is already waiting for `connected`; its own deadline decides.
|
|
64
|
+
if (entry.restarting || entry.timer !== null)
|
|
65
|
+
return;
|
|
66
|
+
entry.timer = this.setTimer(() => {
|
|
67
|
+
entry.timer = null;
|
|
68
|
+
if (entry.state === 'disconnected' || entry.state === 'failed')
|
|
69
|
+
void this.restart(transportId, entry);
|
|
70
|
+
}, exports.DISCONNECTED_GRACE_MS);
|
|
71
|
+
return;
|
|
72
|
+
case 'failed':
|
|
73
|
+
if (entry.restarting)
|
|
74
|
+
return; // duplicate notification; the deadline decides
|
|
75
|
+
this.cancel(entry);
|
|
76
|
+
void this.restart(transportId, entry);
|
|
77
|
+
return;
|
|
78
|
+
default:
|
|
79
|
+
// new / connecting: a fresh transport, or one mid-restart.
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** The transport went away with its session (teardown): no more decisions for it. */
|
|
84
|
+
forget(transportId) {
|
|
85
|
+
const entry = this.entries.get(transportId);
|
|
86
|
+
if (!entry)
|
|
87
|
+
return;
|
|
88
|
+
this.cancel(entry);
|
|
89
|
+
this.entries.delete(transportId);
|
|
90
|
+
if (entry.lost)
|
|
91
|
+
this.host.onPathChange?.(transportId, false);
|
|
92
|
+
}
|
|
93
|
+
/** Every transport went away (teardown, close). */
|
|
94
|
+
reset() {
|
|
95
|
+
for (const id of [...this.entries.keys()])
|
|
96
|
+
this.forget(id);
|
|
97
|
+
}
|
|
98
|
+
/** For tests and the network-change nudge: is this transport being recovered now? */
|
|
99
|
+
restartingNow(transportId) {
|
|
100
|
+
return this.entries.get(transportId)?.restarting === true;
|
|
101
|
+
}
|
|
102
|
+
async restart(transportId, entry) {
|
|
103
|
+
if (!this.host.active() || this.entries.get(transportId) !== entry)
|
|
104
|
+
return;
|
|
105
|
+
entry.restarting = true;
|
|
106
|
+
const revision = ++entry.revision;
|
|
107
|
+
// Include both the RPC and the platform call in the deadline: neither is
|
|
108
|
+
// guaranteed to settle when a network/interface disappears.
|
|
109
|
+
this.cancel(entry);
|
|
110
|
+
entry.timer = this.setTimer(() => {
|
|
111
|
+
entry.timer = null;
|
|
112
|
+
if (!entry.restarting || this.entries.get(transportId) !== entry)
|
|
113
|
+
return;
|
|
114
|
+
entry.revision++;
|
|
115
|
+
entry.restarting = false;
|
|
116
|
+
if (this.host.active())
|
|
117
|
+
this.host.reconnect('media-timeout');
|
|
118
|
+
}, exports.RESTART_TIMEOUT_MS);
|
|
119
|
+
this.setLost(transportId, entry, true);
|
|
120
|
+
let outcome;
|
|
121
|
+
try {
|
|
122
|
+
outcome = await this.host.restartIce(transportId);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
outcome = 'unsupported';
|
|
126
|
+
}
|
|
127
|
+
if (this.entries.get(transportId) !== entry || entry.revision !== revision)
|
|
128
|
+
return;
|
|
129
|
+
if (outcome === 'skipped') {
|
|
130
|
+
this.cancel(entry);
|
|
131
|
+
entry.restarting = false;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (outcome === 'unsupported') {
|
|
135
|
+
this.cancel(entry);
|
|
136
|
+
entry.restarting = false;
|
|
137
|
+
if (this.host.active())
|
|
138
|
+
this.host.reconnect('media-failed');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// `connected` clears the deadline, including when it arrives during the await.
|
|
142
|
+
}
|
|
143
|
+
setLost(transportId, entry, lost) {
|
|
144
|
+
if (entry.lost === lost)
|
|
145
|
+
return;
|
|
146
|
+
entry.lost = lost;
|
|
147
|
+
this.host.onPathChange?.(transportId, lost);
|
|
148
|
+
}
|
|
149
|
+
cancel(entry) {
|
|
150
|
+
if (entry.timer !== null)
|
|
151
|
+
this.clearTimer(entry.timer);
|
|
152
|
+
entry.timer = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
exports.MediaPathWatch = MediaPathWatch;
|
|
156
|
+
/** Notices a socket that died without closing. */
|
|
157
|
+
class SocketLiveness {
|
|
158
|
+
host;
|
|
159
|
+
lastReceived = 0;
|
|
160
|
+
pingSentAt = null;
|
|
161
|
+
timer = null;
|
|
162
|
+
now;
|
|
163
|
+
constructor(host) {
|
|
164
|
+
this.host = host;
|
|
165
|
+
this.now = host.now ?? Date.now;
|
|
166
|
+
}
|
|
167
|
+
/** Start watching a newly opened socket. */
|
|
168
|
+
start() {
|
|
169
|
+
this.stop();
|
|
170
|
+
this.lastReceived = this.now();
|
|
171
|
+
this.pingSentAt = null;
|
|
172
|
+
const every = this.host.setInterval ?? ((fn, ms) => setInterval(fn, ms));
|
|
173
|
+
this.timer = every(() => this.check(), exports.LIVENESS_TICK_MS);
|
|
174
|
+
// A Node/SSR process with no other work need not stay alive for a browser
|
|
175
|
+
// connection watch. Browser timers are numbers and have no unref.
|
|
176
|
+
this.timer?.unref?.();
|
|
177
|
+
}
|
|
178
|
+
stop() {
|
|
179
|
+
if (this.timer !== null)
|
|
180
|
+
(this.host.clearInterval ?? ((h) => clearInterval(h)))(this.timer);
|
|
181
|
+
this.timer = null;
|
|
182
|
+
this.pingSentAt = null;
|
|
183
|
+
}
|
|
184
|
+
get running() {
|
|
185
|
+
return this.timer !== null;
|
|
186
|
+
}
|
|
187
|
+
/** Any frame arrived on the socket. */
|
|
188
|
+
received() {
|
|
189
|
+
this.lastReceived = this.now();
|
|
190
|
+
this.pingSentAt = null;
|
|
191
|
+
}
|
|
192
|
+
/** Check now instead of at the next tick (the browser said the network changed). */
|
|
193
|
+
checkNow() {
|
|
194
|
+
if (this.timer === null || !this.host.active())
|
|
195
|
+
return;
|
|
196
|
+
if (this.pingSentAt === null) {
|
|
197
|
+
this.pingSentAt = this.now();
|
|
198
|
+
this.host.ping();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
check() {
|
|
202
|
+
if (this.timer === null || !this.host.active())
|
|
203
|
+
return;
|
|
204
|
+
const now = this.now();
|
|
205
|
+
if (this.pingSentAt !== null) {
|
|
206
|
+
if (now - this.pingSentAt >= exports.PING_TIMEOUT_MS) {
|
|
207
|
+
this.stop();
|
|
208
|
+
this.host.dead();
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (now - this.lastReceived >= exports.IDLE_MS) {
|
|
213
|
+
this.pingSentAt = now;
|
|
214
|
+
this.host.ping();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
exports.SocketLiveness = SocketLiveness;
|
|
219
|
+
//# sourceMappingURL=link.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.js","sourceRoot":"","sources":["../../src/link.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI noise suppression for the microphone (docs/implementation-progress/RTC_PERF_PROGRESS.md P3-4): what Agora's AI
|
|
3
|
+
* Denoiser, LiveKit's Krisp filter and ZEGOCLOUD's AI noise suppression do, with RNNoise -- the open neural-network
|
|
4
|
+
* denoiser Jitsi uses -- through `@sapphi-red/web-noise-suppressor` (MIT): a Web Audio worklet in the browser's
|
|
5
|
+
* audio thread. Fans, traffic and room rumble fall away (measured in Chromium: rumble-like noise down by more than
|
|
6
|
+
* 60 dB, steady white hiss by 7-12 dB); voices stay.
|
|
7
|
+
*
|
|
8
|
+
* Imported on its own (`@voltkit-rtc/client/noise-suppression`), so an app that does not use it never ships it:
|
|
9
|
+
*
|
|
10
|
+
* import { createNoiseSuppressor } from '@voltkit-rtc/client/noise-suppression';
|
|
11
|
+
* await client.setAudioProcessor(createNoiseSuppressor());
|
|
12
|
+
*
|
|
13
|
+
* The worklet (64 kB) and the model (150 kB) are fetched when it starts: from jsDelivr, at the exact version this
|
|
14
|
+
* SDK was tested with, unless you serve them yourself (`workletUrl`, `wasmUrl`, `simdWasmUrl`) -- copy them from
|
|
15
|
+
* `node_modules/@sapphi-red/web-noise-suppressor/dist` to your own site for a strict Content-Security-Policy.
|
|
16
|
+
*/
|
|
17
|
+
import type { AudioTrackProcessor } from './processors.js';
|
|
18
|
+
/** The package version the default URLs point at; equal to this SDK's pinned dependency (a test checks it). */
|
|
19
|
+
export declare const NOISE_SUPPRESSOR_VERSION = "0.4.1";
|
|
20
|
+
export interface NoiseSuppressorOptions {
|
|
21
|
+
/** Where `rnnoise/workletProcessor.js` is served. Default: jsDelivr, pinned. */
|
|
22
|
+
workletUrl?: string;
|
|
23
|
+
/** Where `rnnoise.wasm` is served. Default: jsDelivr, pinned. */
|
|
24
|
+
wasmUrl?: string;
|
|
25
|
+
/** Where `rnnoise_simd.wasm` is served (used where the browser supports SIMD). Default: jsDelivr, pinned. */
|
|
26
|
+
simdWasmUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
/** What the processor needs from the browser; stood in for by tests. */
|
|
29
|
+
export interface NoiseSuppressorDeps {
|
|
30
|
+
createContext(): AudioContext;
|
|
31
|
+
loadModel(urls: {
|
|
32
|
+
url: string;
|
|
33
|
+
simdUrl: string;
|
|
34
|
+
}): Promise<ArrayBuffer>;
|
|
35
|
+
createNode(context: AudioContext, wasmBinary: ArrayBuffer): Promise<DenoiseNode> | DenoiseNode;
|
|
36
|
+
createStream(tracks: MediaStreamTrack[]): MediaStream;
|
|
37
|
+
}
|
|
38
|
+
type DenoiseNode = AudioNode & {
|
|
39
|
+
destroy?(): void;
|
|
40
|
+
};
|
|
41
|
+
/** An `AudioTrackProcessor` for `client.setAudioProcessor()` that removes background noise with RNNoise. */
|
|
42
|
+
export declare function createNoiseSuppressor(options?: NoiseSuppressorOptions, deps?: NoiseSuppressorDeps): AudioTrackProcessor;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=noise-suppression.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noise-suppression.d.ts","sourceRoot":"","sources":["../../src/noise-suppression.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAE3D,+GAA+G;AAC/G,eAAO,MAAM,wBAAwB,UAAU,CAAC;AAGhD,MAAM,WAAW,sBAAsB;IACrC,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6GAA6G;IAC7G,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IAClC,aAAa,IAAI,YAAY,CAAC;IAC9B,SAAS,CAAC,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACxE,UAAU,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CAAC;IAC/F,YAAY,CAAC,MAAM,EAAE,gBAAgB,EAAE,GAAG,WAAW,CAAC;CACvD;AAED,KAAK,WAAW,GAAG,SAAS,GAAG;IAAE,OAAO,CAAC,IAAI,IAAI,CAAA;CAAE,CAAC;AAsBpD,4GAA4G;AAC5G,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,EAAE,IAAI,GAAE,mBAAiC,GAAG,mBAAmB,CAwExI"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NOISE_SUPPRESSOR_VERSION = void 0;
|
|
4
|
+
exports.createNoiseSuppressor = createNoiseSuppressor;
|
|
5
|
+
/** The package version the default URLs point at; equal to this SDK's pinned dependency (a test checks it). */
|
|
6
|
+
exports.NOISE_SUPPRESSOR_VERSION = '0.4.1';
|
|
7
|
+
const CDN = `https://cdn.jsdelivr.net/npm/@sapphi-red/web-noise-suppressor@${exports.NOISE_SUPPRESSOR_VERSION}/dist`;
|
|
8
|
+
let library = null;
|
|
9
|
+
/**
|
|
10
|
+
* The library is loaded on first use, not with this module: it extends `AudioWorkletNode` as it loads, which exists
|
|
11
|
+
* only in browsers, so a static import would break a server render (Next.js) or Node.
|
|
12
|
+
*/
|
|
13
|
+
const load = () => (library ??= Promise.resolve().then(() => require('@sapphi-red/web-noise-suppressor')).catch((err) => {
|
|
14
|
+
library = null;
|
|
15
|
+
throw err;
|
|
16
|
+
}));
|
|
17
|
+
const browserDeps = {
|
|
18
|
+
// RNNoise works on 48 kHz audio.
|
|
19
|
+
createContext: () => new AudioContext({ sampleRate: 48000 }),
|
|
20
|
+
loadModel: async (urls) => (await load()).loadRnnoise(urls),
|
|
21
|
+
createNode: async (context, wasmBinary) => new (await load()).RnnoiseWorkletNode(context, { wasmBinary, maxChannels: 1 }),
|
|
22
|
+
createStream: (tracks) => new MediaStream(tracks),
|
|
23
|
+
};
|
|
24
|
+
/** An `AudioTrackProcessor` for `client.setAudioProcessor()` that removes background noise with RNNoise. */
|
|
25
|
+
function createNoiseSuppressor(options = {}, deps = browserDeps) {
|
|
26
|
+
let context = null;
|
|
27
|
+
let ready = null;
|
|
28
|
+
let graph = null;
|
|
29
|
+
let resumeOnGesture = null;
|
|
30
|
+
const release = () => {
|
|
31
|
+
if (!graph)
|
|
32
|
+
return;
|
|
33
|
+
try {
|
|
34
|
+
graph.source.disconnect();
|
|
35
|
+
graph.node.disconnect();
|
|
36
|
+
graph.node.destroy?.();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Already gone.
|
|
40
|
+
}
|
|
41
|
+
graph.output.stop();
|
|
42
|
+
graph = null;
|
|
43
|
+
};
|
|
44
|
+
/** A context made without a tap starts suspended: no sound would go out until the page is touched. */
|
|
45
|
+
const keepRunning = (ctx) => {
|
|
46
|
+
void ctx.resume?.().catch(() => undefined);
|
|
47
|
+
if (ctx.state === 'running' || typeof document === 'undefined' || resumeOnGesture)
|
|
48
|
+
return;
|
|
49
|
+
resumeOnGesture = () => {
|
|
50
|
+
void ctx.resume().catch(() => undefined);
|
|
51
|
+
if (ctx.state === 'running' && resumeOnGesture) {
|
|
52
|
+
for (const type of ['pointerdown', 'keydown'])
|
|
53
|
+
document.removeEventListener(type, resumeOnGesture, { capture: true });
|
|
54
|
+
resumeOnGesture = null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
for (const type of ['pointerdown', 'keydown'])
|
|
58
|
+
document.addEventListener(type, resumeOnGesture, { capture: true });
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
name: 'noise-suppression',
|
|
62
|
+
async process(track) {
|
|
63
|
+
const ctx = (context ??= deps.createContext());
|
|
64
|
+
ready ??= Promise.all([
|
|
65
|
+
deps.loadModel({ url: options.wasmUrl ?? `${CDN}/rnnoise.wasm`, simdUrl: options.simdWasmUrl ?? `${CDN}/rnnoise_simd.wasm` }),
|
|
66
|
+
ctx.audioWorklet.addModule(options.workletUrl ?? `${CDN}/rnnoise/workletProcessor.js`),
|
|
67
|
+
]).then(([model]) => model);
|
|
68
|
+
let model;
|
|
69
|
+
try {
|
|
70
|
+
model = await ready;
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
ready = null; // try again next time (a network blip)
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
const node = await deps.createNode(ctx, model);
|
|
77
|
+
release();
|
|
78
|
+
const source = ctx.createMediaStreamSource(deps.createStream([track]));
|
|
79
|
+
const destination = ctx.createMediaStreamDestination();
|
|
80
|
+
source.connect(node);
|
|
81
|
+
node.connect(destination);
|
|
82
|
+
const output = destination.stream.getAudioTracks()[0];
|
|
83
|
+
if (!output)
|
|
84
|
+
throw new Error('the audio graph gave no track');
|
|
85
|
+
graph = { source, node, output };
|
|
86
|
+
keepRunning(ctx);
|
|
87
|
+
return output;
|
|
88
|
+
},
|
|
89
|
+
async destroy() {
|
|
90
|
+
release();
|
|
91
|
+
if (resumeOnGesture && typeof document !== 'undefined') {
|
|
92
|
+
for (const type of ['pointerdown', 'keydown'])
|
|
93
|
+
document.removeEventListener(type, resumeOnGesture, { capture: true });
|
|
94
|
+
}
|
|
95
|
+
resumeOnGesture = null;
|
|
96
|
+
const ctx = context;
|
|
97
|
+
context = null;
|
|
98
|
+
ready = null;
|
|
99
|
+
await ctx?.close().catch(() => undefined);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=noise-suppression.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noise-suppression.js","sourceRoot":"","sources":["../../src/noise-suppression.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-call check (docs/implementation-progress/RTC_PERF_PROGRESS.md L1).
|
|
3
|
+
*
|
|
4
|
+
* - Twilio's `runPreflight` (read from its code) joins a test room, publishes synthetic audio and video and reports
|
|
5
|
+
* timings, the connection path, round trip, jitter and loss.
|
|
6
|
+
* - LiveKit's `ConnectionCheck` runs separate checks -- WebSocket, WebRTC, TURN, publishing audio and video, UDP
|
|
7
|
+
* against TCP -- each with messages and warnings.
|
|
8
|
+
* - Agora's `startLastmileProbeTest` measures the last mile before a call.
|
|
9
|
+
*
|
|
10
|
+
* This joins the room the token names (mint one for a test room nobody else is in: the session is real, and billed
|
|
11
|
+
* like any other for its few seconds), publishes what it is given -- or a tone and a picture it makes -- and reports
|
|
12
|
+
* what it saw. The orchestration takes a small target interface, so it is tested without a browser.
|
|
13
|
+
*/
|
|
14
|
+
import { type NetworkQualityLevel } from './network.js';
|
|
15
|
+
import type { StatsEntry } from './stats.js';
|
|
16
|
+
import type { MediaWarningCode } from './health.js';
|
|
17
|
+
export interface PreflightOptions {
|
|
18
|
+
/** What to publish (cloned; your tracks are left running). Without it, a tone and a moving picture are made. */
|
|
19
|
+
media?: MediaStream;
|
|
20
|
+
/** How long media is measured once it flows. Default 5 s. */
|
|
21
|
+
durationMs?: number;
|
|
22
|
+
}
|
|
23
|
+
export type PreflightStepName = 'join' | 'media';
|
|
24
|
+
export interface PreflightStep {
|
|
25
|
+
name: PreflightStepName;
|
|
26
|
+
ok: boolean;
|
|
27
|
+
/** How long it took, in ms. */
|
|
28
|
+
ms: number | null;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface PreflightReport {
|
|
32
|
+
/** Joined, and media reached the server. */
|
|
33
|
+
ok: boolean;
|
|
34
|
+
steps: PreflightStep[];
|
|
35
|
+
/** How media travelled: UDP is best; TCP or a relay means a firewall in the way (it works, with more delay). */
|
|
36
|
+
path: {
|
|
37
|
+
protocol: 'udp' | 'tcp' | null;
|
|
38
|
+
relayed: boolean;
|
|
39
|
+
} | null;
|
|
40
|
+
rttMs: number | null;
|
|
41
|
+
lossPct: number | null;
|
|
42
|
+
/** The upload the browser estimated it could use, in kbps. */
|
|
43
|
+
uploadKbps: number | null;
|
|
44
|
+
level: NetworkQualityLevel | null;
|
|
45
|
+
/** Camera and microphone problems seen during the test (see the mediaWarning event). */
|
|
46
|
+
warnings: MediaWarningCode[];
|
|
47
|
+
}
|
|
48
|
+
/** What a pre-call check needs from a client. */
|
|
49
|
+
export interface PreflightTarget {
|
|
50
|
+
connect(): Promise<unknown>;
|
|
51
|
+
publish(stream: MediaStream): Promise<void>;
|
|
52
|
+
/** The send transport's connection state, or null before there is one. */
|
|
53
|
+
sendState(): string | null;
|
|
54
|
+
/** The send transport's statistics, or null without one. */
|
|
55
|
+
sendStats(): Promise<Iterable<StatsEntry> | null>;
|
|
56
|
+
onWarning(listener: (code: MediaWarningCode, active: boolean) => void): () => void;
|
|
57
|
+
close(): void;
|
|
58
|
+
}
|
|
59
|
+
export interface PreflightEnv {
|
|
60
|
+
now(): number;
|
|
61
|
+
sleep(ms: number): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
/** Media has this long to connect once published. */
|
|
64
|
+
export declare const MEDIA_CONNECT_TIMEOUT_MS = 10000;
|
|
65
|
+
/** Statistics are read this often while measuring. */
|
|
66
|
+
export declare const PREFLIGHT_SAMPLE_MS = 1000;
|
|
67
|
+
/** The selected candidate pair's protocol, and whether either end is a relay. */
|
|
68
|
+
export declare function connectionPath(report: Iterable<StatsEntry>): PreflightReport['path'];
|
|
69
|
+
/** Run the check on `target`, closing it at the end whatever happens. */
|
|
70
|
+
export declare function runPreflight(target: PreflightTarget, stream: MediaStream, options?: PreflightOptions, env?: PreflightEnv): Promise<PreflightReport>;
|
|
71
|
+
/**
|
|
72
|
+
* A tone and a moving picture to publish when the app gives none (Twilio's preflight makes synthetic media too).
|
|
73
|
+
* Returns the stream and a function that stops everything it made. Browser only.
|
|
74
|
+
*/
|
|
75
|
+
export declare function syntheticMedia(): {
|
|
76
|
+
stream: MediaStream;
|
|
77
|
+
stop: () => void;
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=preflight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.d.ts","sourceRoot":"","sources":["../../src/preflight.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAwC,KAAK,mBAAmB,EAAwB,MAAM,cAAc,CAAC;AACpH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,MAAM,WAAW,gBAAgB;IAC/B,gHAAgH;IAChH,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEjD,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,iBAAiB,CAAC;IACxB,EAAE,EAAE,OAAO,CAAC;IACZ,+BAA+B;IAC/B,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,4CAA4C;IAC5C,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,gHAAgH;IAChH,IAAI,EAAE;QAAE,QAAQ,EAAE,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAClC,wFAAwF;IACxF,QAAQ,EAAE,gBAAgB,EAAE,CAAC;CAC9B;AAED,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5B,OAAO,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,0EAA0E;IAC1E,SAAS,IAAI,MAAM,GAAG,IAAI,CAAC;IAC3B,4DAA4D;IAC5D,SAAS,IAAI,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,CAAC;IAClD,SAAS,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,gBAAgB,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACnF,KAAK,IAAI,IAAI,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,IAAI,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAID,qDAAqD;AACrD,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAC/C,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAExC,iFAAiF;AACjF,wBAAgB,cAAc,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,eAAe,CAAC,MAAM,CAAC,CAgBpF;AAED,yEAAyE;AACzE,wBAAsB,YAAY,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,gBAAqB,EAAE,GAAG,GAAE,YAAsB,GAAG,OAAO,CAAC,eAAe,CAAC,CAsDtK;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,IAAI,CAAA;CAAE,CAqC1E"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PREFLIGHT_SAMPLE_MS = exports.MEDIA_CONNECT_TIMEOUT_MS = void 0;
|
|
4
|
+
exports.connectionPath = connectionPath;
|
|
5
|
+
exports.runPreflight = runPreflight;
|
|
6
|
+
exports.syntheticMedia = syntheticMedia;
|
|
7
|
+
/**
|
|
8
|
+
* Pre-call check (docs/implementation-progress/RTC_PERF_PROGRESS.md L1).
|
|
9
|
+
*
|
|
10
|
+
* - Twilio's `runPreflight` (read from its code) joins a test room, publishes synthetic audio and video and reports
|
|
11
|
+
* timings, the connection path, round trip, jitter and loss.
|
|
12
|
+
* - LiveKit's `ConnectionCheck` runs separate checks -- WebSocket, WebRTC, TURN, publishing audio and video, UDP
|
|
13
|
+
* against TCP -- each with messages and warnings.
|
|
14
|
+
* - Agora's `startLastmileProbeTest` measures the last mile before a call.
|
|
15
|
+
*
|
|
16
|
+
* This joins the room the token names (mint one for a test room nobody else is in: the session is real, and billed
|
|
17
|
+
* like any other for its few seconds), publishes what it is given -- or a tone and a picture it makes -- and reports
|
|
18
|
+
* what it saw. The orchestration takes a small target interface, so it is tested without a browser.
|
|
19
|
+
*/
|
|
20
|
+
const network_js_1 = require("./network.js");
|
|
21
|
+
const realEnv = { now: () => Date.now(), sleep: (ms) => new Promise((r) => setTimeout(r, ms)) };
|
|
22
|
+
/** Media has this long to connect once published. */
|
|
23
|
+
exports.MEDIA_CONNECT_TIMEOUT_MS = 10_000;
|
|
24
|
+
/** Statistics are read this often while measuring. */
|
|
25
|
+
exports.PREFLIGHT_SAMPLE_MS = 1000;
|
|
26
|
+
/** The selected candidate pair's protocol, and whether either end is a relay. */
|
|
27
|
+
function connectionPath(report) {
|
|
28
|
+
const entries = [...report];
|
|
29
|
+
const byId = new Map();
|
|
30
|
+
for (const e of entries)
|
|
31
|
+
if (typeof e.id === 'string')
|
|
32
|
+
byId.set(e.id, e);
|
|
33
|
+
let pair;
|
|
34
|
+
for (const t of entries.filter((e) => e.type === 'transport')) {
|
|
35
|
+
if (typeof t.selectedCandidatePairId === 'string')
|
|
36
|
+
pair = byId.get(t.selectedCandidatePairId);
|
|
37
|
+
if (pair)
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
pair ??= entries.find((e) => e.type === 'candidate-pair' && e.nominated === true && e.state === 'succeeded');
|
|
41
|
+
if (!pair)
|
|
42
|
+
return null;
|
|
43
|
+
const local = typeof pair.localCandidateId === 'string' ? byId.get(pair.localCandidateId) : undefined;
|
|
44
|
+
const remote = typeof pair.remoteCandidateId === 'string' ? byId.get(pair.remoteCandidateId) : undefined;
|
|
45
|
+
const protocol = local?.protocol === 'udp' || local?.protocol === 'tcp' ? local.protocol : null;
|
|
46
|
+
const relayed = local?.candidateType === 'relay' || remote?.candidateType === 'relay';
|
|
47
|
+
return { protocol, relayed };
|
|
48
|
+
}
|
|
49
|
+
/** Run the check on `target`, closing it at the end whatever happens. */
|
|
50
|
+
async function runPreflight(target, stream, options = {}, env = realEnv) {
|
|
51
|
+
const report = { ok: false, steps: [], path: null, rttMs: null, lossPct: null, uploadKbps: null, level: null, warnings: [] };
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
const stopWarnings = target.onWarning((code, active) => { if (active)
|
|
54
|
+
seen.add(code); });
|
|
55
|
+
try {
|
|
56
|
+
let started = env.now();
|
|
57
|
+
try {
|
|
58
|
+
await target.connect();
|
|
59
|
+
report.steps.push({ name: 'join', ok: true, ms: env.now() - started });
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
report.steps.push({ name: 'join', ok: false, ms: null, error: String(err?.message ?? err) });
|
|
63
|
+
return report;
|
|
64
|
+
}
|
|
65
|
+
started = env.now();
|
|
66
|
+
try {
|
|
67
|
+
await target.publish(stream);
|
|
68
|
+
while (target.sendState() !== 'connected') {
|
|
69
|
+
const state = target.sendState();
|
|
70
|
+
if (state === 'failed' || state === 'closed')
|
|
71
|
+
throw new Error(`media connection ${state}`);
|
|
72
|
+
if (env.now() - started > exports.MEDIA_CONNECT_TIMEOUT_MS)
|
|
73
|
+
throw new Error('media did not connect in time (a firewall blocking UDP and TCP?)');
|
|
74
|
+
await env.sleep(100);
|
|
75
|
+
}
|
|
76
|
+
report.steps.push({ name: 'media', ok: true, ms: env.now() - started });
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
report.steps.push({ name: 'media', ok: false, ms: null, error: String(err?.message ?? err) });
|
|
80
|
+
return report;
|
|
81
|
+
}
|
|
82
|
+
// Measure: the worst round trip and loss over the run, the last upload estimate.
|
|
83
|
+
const duration = options.durationMs ?? 5000;
|
|
84
|
+
let previous = new Map();
|
|
85
|
+
const until = env.now() + duration;
|
|
86
|
+
while (env.now() < until) {
|
|
87
|
+
await env.sleep(exports.PREFLIGHT_SAMPLE_MS);
|
|
88
|
+
const stats = await target.sendStats().catch(() => null);
|
|
89
|
+
if (!stats)
|
|
90
|
+
continue;
|
|
91
|
+
const entries = [...stats];
|
|
92
|
+
report.path = connectionPath(entries) ?? report.path;
|
|
93
|
+
const now = new Map([['send', (0, network_js_1.readTransportStats)(entries)]]);
|
|
94
|
+
const q = (0, network_js_1.networkQualityOf)(now, previous, true);
|
|
95
|
+
previous = now;
|
|
96
|
+
if (q.rttMs !== null)
|
|
97
|
+
report.rttMs = Math.max(report.rttMs ?? 0, q.rttMs);
|
|
98
|
+
if (q.lossPct !== null)
|
|
99
|
+
report.lossPct = Math.max(report.lossPct ?? 0, q.lossPct);
|
|
100
|
+
if (q.availableKbps !== null)
|
|
101
|
+
report.uploadKbps = q.availableKbps;
|
|
102
|
+
report.level = q.level;
|
|
103
|
+
}
|
|
104
|
+
report.ok = true;
|
|
105
|
+
return report;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
report.warnings = [...seen];
|
|
109
|
+
stopWarnings();
|
|
110
|
+
target.close();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A tone and a moving picture to publish when the app gives none (Twilio's preflight makes synthetic media too).
|
|
115
|
+
* Returns the stream and a function that stops everything it made. Browser only.
|
|
116
|
+
*/
|
|
117
|
+
function syntheticMedia() {
|
|
118
|
+
const tracks = [];
|
|
119
|
+
const stops = [];
|
|
120
|
+
const AudioCtx = globalThis.AudioContext;
|
|
121
|
+
if (AudioCtx) {
|
|
122
|
+
const ctx = new AudioCtx();
|
|
123
|
+
const osc = ctx.createOscillator();
|
|
124
|
+
const gain = ctx.createGain();
|
|
125
|
+
gain.gain.value = 0.05;
|
|
126
|
+
const dest = ctx.createMediaStreamDestination();
|
|
127
|
+
osc.connect(gain).connect(dest);
|
|
128
|
+
osc.start();
|
|
129
|
+
tracks.push(...dest.stream.getAudioTracks());
|
|
130
|
+
stops.push(() => { osc.stop(); void ctx.close(); });
|
|
131
|
+
}
|
|
132
|
+
if (typeof document !== 'undefined') {
|
|
133
|
+
const canvas = document.createElement('canvas');
|
|
134
|
+
canvas.width = 640;
|
|
135
|
+
canvas.height = 360;
|
|
136
|
+
const g = canvas.getContext('2d');
|
|
137
|
+
let frame = 0;
|
|
138
|
+
const draw = () => {
|
|
139
|
+
if (!g)
|
|
140
|
+
return;
|
|
141
|
+
frame += 1;
|
|
142
|
+
g.fillStyle = `hsl(${frame % 360}, 60%, 40%)`;
|
|
143
|
+
g.fillRect(0, 0, canvas.width, canvas.height);
|
|
144
|
+
g.fillStyle = '#fff';
|
|
145
|
+
g.fillRect((frame * 8) % canvas.width, 150, 60, 60);
|
|
146
|
+
};
|
|
147
|
+
draw();
|
|
148
|
+
const timer = setInterval(draw, 1000 / 30);
|
|
149
|
+
const captured = canvas.captureStream?.(30);
|
|
150
|
+
if (captured)
|
|
151
|
+
tracks.push(...captured.getVideoTracks());
|
|
152
|
+
stops.push(() => clearInterval(timer));
|
|
153
|
+
}
|
|
154
|
+
const stream = new MediaStream(tracks);
|
|
155
|
+
return { stream, stop: () => { for (const t of tracks)
|
|
156
|
+
t.stop(); for (const s of stops)
|
|
157
|
+
s(); } };
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=preflight.js.map
|