@furious.luke/argus-js
v0.3.1
Published
Browser client for the Argus video streaming platform — publish WebRTC video to Argus media servers.
Maintainers
Readme
@furious.luke/argus-js
Browser client for the Argus video streaming platform. It publishes a MediaStream (camera, screen share, etc.) from a browser to an Argus media server over WebRTC, handling gateway selection, the signaling handshake, and ICE negotiation for you.
Zero runtime dependencies — it uses only the browser's built-in WebSocket and RTCPeerConnection.
Where this fits
Argus is a distributed video ingestion service. A stream flows through three parties:
- Your server mints a short-lived join token using its secret API key (see the Go client, published as the
github.com/furious-luke/argus-gomodule, or the control-plane HTTP API). - The browser (this library) uses that token to publish video to the nearest Argus media server. It never sees the API key.
- Your server later fetches frames from the stream on demand.
This library is only the browser publishing half. It deliberately does not talk to the control plane or mint tokens — that requires your secret API key and must stay server-side.
┌──────────┐ 1. request join token ┌────────────┐
│ browser │ ───────────────────────▶ │ your server│ ──(API key)──▶ Argus control plane
│ │ ◀─────────────────────── │ │ ◀── token + gateway_urls ──
│ argus-js │ 2. token + gateway_urls └────────────┘
│ │
│ │ 3. publish WebRTC video ────────────────▶ Argus media server
└──────────┘Installation
npm install @furious.luke/argus-jsUsage
Your server first creates a stream and returns the join token bundle to the browser (the token and gateway_urls from Argus's POST /api/streams response). Then:
import { Publisher, captureCamera } from "@furious.luke/argus-js";
// `join` is the { token, gateway_urls } bundle your server obtained from Argus.
const publisher = new Publisher({
gatewayURLs: join.gateway_urls,
token: join.token,
callbacks: {
onConnected: () => console.log("streaming live"),
onConnectionStateChange: (state) => console.log("state:", state),
onRecoveryStateChange: (event) => console.log("media recovery:", event),
// Show a button or prompt. From its click handler, call captureScreen()
// and then publisher.replaceStream(newStream).
onRecoveryRequired: (event) => console.warn("screen share must be restarted", event),
onError: (err) => console.error("publish error:", err),
},
});
// captureCamera() applies sensible capture defaults; any MediaStream works too.
const stream = await captureCamera();
await publisher.start(stream);
// After start() resolves, this token lets your server fetch frames for the stream.
console.log("read token:", publisher.frameReadToken);Capture helpers
captureCamera() and captureScreen() wrap getUserMedia / getDisplayMedia with defaults tuned for streaming — a capped resolution and modest frame rate, audio off. They matter most for screen sharing: on HiDPI/Retina displays a raw getDisplayMedia captures at native resolution (often 3456px+ / effectively 4k), wasting upload bandwidth and downstream decode for no benefit. captureScreen() caps the width instead.
import { captureScreen } from "@furious.luke/argus-js";
const stream = await captureScreen();
await publisher.start(stream);Both accept overrides (shallow-merged over the defaults), and any MediaStream you build yourself still works if you'd rather manage constraints directly.
Switching sources without reconnecting
replaceStream renegotiates in place, e.g. to toggle between camera and screen:
const screen = await captureScreen();
await publisher.replaceStream(screen);Stopping
publisher.stop(); // stops all tracks and tears down the peer connectionAPI
new Publisher(options)
| Option | Type | Description |
| --- | --- | --- |
| gatewayURLs | string[] | Required. The gateway_urls from the join response. All are raced simultaneously; the fastest to accept wins. |
| token | string | Required. The short-lived join token from the join response. |
| iceServers | RTCIceServer[] | Optional extra ICE servers (e.g. your own STUN). TURN is supplied automatically by the winning gateway. |
| signalingReconnectTimeoutMs | number | How long to retry a dropped signaling socket against the selected regional gateway. Defaults to 20 seconds. |
| callbacks | PublisherCallbacks | Optional lifecycle callbacks (see below). |
Methods & properties
| Member | Description |
| --- | --- |
| start(stream) | Races the gateways, completes the handshake, and sends the SDP offer. Resolves once the offer is sent — use onConnected to know when media is actually flowing. |
| replaceStream(stream) | Replaces the published tracks and renegotiates in place. |
| stop() | Stops all local tracks and closes the peer connection. |
| frameReadToken | The one-hour read token from the gateway's ready message, or null before connecting. The publisher uses it internally to resume signaling; hand it to your server to fetch frames. |
| peerConnection | The underlying RTCPeerConnection, or null if not started. |
| isConnected | true when the peer connection state is "connected". |
PublisherCallbacks
| Callback | When |
| --- | --- |
| onConnected() | The peer connection reached "connected" — media is flowing. |
| onConnectionStateChange(state) | The RTCPeerConnectionState changed. |
| onRecoveryStateChange(event) | Argus detected stalled media and the publisher started, escalated, completed, or failed automatic recovery. |
| onRecoveryRequired(event) | Automatic recovery could not restore media, or capture ended and the host must ask the user for a new screen share. |
| onError(error) | A fatal error occurred (signaling error, initial gateway failure, or signaling resume timed out). |
How start() works
- Gateway race. Every URL in
gatewayURLsis opened at once with the token in the query string. The first to complete the two-phase handshake (accepted→proceed→ready) wins; the rest are closed. This picks the lowest-latency region without a separate probe. - TURN + read token. The winning gateway's
readymessage carries per-session TURN credentials (merged into the ICE configuration) and the read token exposed asframeReadToken. - WebRTC. A peer connection is created, tracks are added, and an offer is sent. Remote ICE candidates that arrive before the SDP answer are buffered and flushed once the answer is applied.
After this initial race, the publisher is pinned to the selected regional
gateway. If signaling drops, it reconnects to that same gateway with the
one-hour read token and waits for resumed; it does not race regions, rebuild
the peer connection, or repeat the ready handshake. If the retry deadline
expires, the publisher closes the stream and calls onError.
Automatic media recovery
Argus measures freshness from complete encoded samples received by the media server, not from pixel changes. An unchanged screen therefore remains healthy. If samples stop for 15 seconds, the media server tells the publisher that the track has stalled and stops serving its retained frame as current. The publisher first detaches and reattaches the live sender and renegotiates. If samples do not resume within four seconds, it performs an ICE restart and waits another eight seconds. These fixed timings deliberately are not application configuration.
onRecoveryStateChange reports the recovery transitions. If both automatic
steps fail, onRecoveryRequired is called and the host can acquire a replacement
stream and pass it to replaceStream. A browser cannot silently reacquire a
screen share after the user or operating system ends it, so capture_ended
always requires host UI and a fresh captureScreen() call.
Browser support
Requires a browser with WebRTC (RTCPeerConnection) and WebSocket — all current evergreen browsers. There is no Node.js runtime support; this is a browser-only package.
Development
npm install
npm run build # bundle ESM + CJS + types into dist/ via tsup
npm test # run the vitest suite (jsdom)
npm run typecheck # tsc --noEmitPublishing
The package is published to npm under the @furious-luke scope. prepublishOnly runs typecheck, tests, and build first, so a release is:
npm version <patch|minor|major>
npm publish(The scope is configured for public access via publishConfig.)
