Thanks for the quick reply! Details below.
On which side is the nodeJS SDK used?
Client/consumer side. The viewer is an Electron desktop app. We run
@openziti/ziti-sdk-nodejs v0.29.1 in a forked Node worker (the system Node,
not the Chromium/renderer process). The media server (MediaMTX) is on the
remote device and is reached as a Ziti service <id>.pix. The SDK is NOT used
on the camera/server side.
WebRTC service setup
-
Media server: MediaMTX, exposing WHEP at
http://<id>.pix:8889/<camera>_<quality>/whep.
-
That host:port is an OpenZiti service; all \.pix HTTP requests are routed
through the SDK.
-
The player runs in the Chromium renderer: a standard WHEP client with an
RTCPeerConnection, recvonly audio+video transceivers, iceServers: []
(host candidates only, LAN). It POSTs the SDP offer
(Content-Type: application/sdp) and applies the SDP answer.
-
We use the SDK ONLY to proxy the WHEP HTTP signaling (POST offer /
DELETE session), via ziti.httpAgent(baseUrl) + Node http.request.
What works / what doesn't
-
Signaling works: the SDP offer POST goes through Ziti and MediaMTX returns
201 + a valid SDP answer.
-
Media never flows: the PeerConnection never reaches connected, ICE fails.
Our current understanding (please confirm or correct)
The SDK proxies the HTTP signaling fine, but the WebRTC media plane (ICE
connectivity checks + SRTP, over UDP) is handled by Chromium's
RTCPeerConnection, which has no knowledge of Ziti. The SDP answer's ICE
candidates are the server's LAN host candidates (e.g. 10.x.x.x), which are
unreachable remotely, and that media UDP does not traverse the SDK (we only
wrapped the HTTP agent). With the Ziti Tunneler the exact same setup
works, because the Tunneler intercepts at the OS/IP layer and tunnels the
media UDP as well.
Question
Is there a recommended way to carry the WebRTC media plane through the nodeJS
SDK (e.g. MediaMTX webrtcLocalTCPAddress single-port TCP + a local relay
that dials the Ziti service), or is the nodeJS SDK intended for signaling
only, with the media expected to rely on a tunneler / a separate transport? A
minimal working pattern for WebRTC/WHEP over the nodeJS SDK would help a lot.
async function startLive(streamBase, stream, video) {
const pc = new RTCPeerConnection({ iceServers:
});
pc.addTransceiver('video', { direction: 'recvonly' });
pc.addTransceiver('audio', { direction: 'recvonly' });
pc.ontrack = (e) => {
if (e.receiver && 'playoutDelayHint' in e.receiver) e.receiver.playoutDelayHint = 0.25;
video.srcObject = e.streams[0];
video.play().catch(() => {});
};
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
await waitIceGathering(pc, 500);
const whepUrl = ${streamBase}_${stream}/whep;
const res = await window.zitiAPI.mediaSignal(whepUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/sdp' },
body: pc.localDescription.sdp,
});
}
function waitIceGathering(pc, timeoutMs) {
return new Promise((resolve) => {
if (pc.iceGatheringState === 'complete') return resolve();
const check = () => { if (pc.iceGatheringState === 'complete') { cleanup(); resolve(); } };
const cleanup = () => pc.removeEventListener('icegatheringstatechange', check);
pc.addEventListener('icegatheringstatechange', check);
setTimeout(() => { cleanup(); resolve(); }, timeoutMs);
});
}
[post edited/cleaned by clint]