@furious.luke/argus-js 0.1.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.
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Message types exchanged over the WebSocket signaling channel.
3
+ */
4
+ type SignalMessage = {
5
+ type: "offer";
6
+ sdp: string;
7
+ sdp_type: "offer";
8
+ } | {
9
+ type: "answer";
10
+ sdp: string;
11
+ sdp_type: "answer";
12
+ } | {
13
+ type: "ice_candidate";
14
+ candidate: string;
15
+ sdp_mid?: string;
16
+ sdp_mline_index?: number;
17
+ username_fragment?: string;
18
+ } | {
19
+ type: "connection_state";
20
+ state: string;
21
+ } | {
22
+ type: "error";
23
+ error: string;
24
+ } | {
25
+ type: "accepted";
26
+ } | {
27
+ type: "proceed";
28
+ } | {
29
+ type: "ready";
30
+ turn_url?: string;
31
+ turn_username?: string;
32
+ turn_credential?: string;
33
+ read_token?: string;
34
+ };
35
+ /**
36
+ * Callbacks emitted by the Publisher during its lifecycle.
37
+ */
38
+ interface PublisherCallbacks {
39
+ /** Called when the WebRTC peer connection state changes. */
40
+ onConnectionStateChange?: (state: RTCPeerConnectionState) => void;
41
+ /** Called when a fatal error occurs (e.g. signaling error, WebSocket close). */
42
+ onError?: (error: Error) => void;
43
+ /** Called when the browser has successfully connected to the media server. */
44
+ onConnected?: () => void;
45
+ }
46
+ /**
47
+ * TURN and read-token info delivered in the gateway `ready` message.
48
+ */
49
+ interface GatewayReadyInfo {
50
+ turn_url?: string;
51
+ turn_username?: string;
52
+ turn_credential?: string;
53
+ read_token?: string;
54
+ }
55
+ /**
56
+ * Options for publishing a media stream.
57
+ */
58
+ interface PublisherOptions {
59
+ /** Gateway WebSocket URLs returned by POST /api/streams `gateway_urls`. All are raced simultaneously. */
60
+ gatewayURLs: string[];
61
+ /** The short-lived join token from POST /api/streams. */
62
+ token: string;
63
+ /** Optional extra ICE servers (e.g. STUN). TURN is supplied by the winning gateway. */
64
+ iceServers?: RTCIceServer[];
65
+ /** Callbacks for lifecycle events. */
66
+ callbacks?: PublisherCallbacks;
67
+ }
68
+
69
+ /**
70
+ * Publisher streams a browser {@link MediaStream} to an Argus media server over
71
+ * WebRTC. Given the `gateway_urls` and `token` from a join-token response, it
72
+ * races the candidate gateways to the fastest one, completes the two-phase
73
+ * signaling handshake, and manages the peer connection — offer/answer exchange,
74
+ * ICE candidate trickling, and track (re)negotiation.
75
+ *
76
+ * After {@link Publisher.start} resolves, {@link Publisher.frameReadToken} holds
77
+ * the token your application server needs to fetch frames for this stream.
78
+ *
79
+ * @example Publish the default camera:
80
+ * ```ts
81
+ * const pub = new Publisher({
82
+ * gatewayURLs: joinResp.gateway_urls,
83
+ * token: joinResp.token,
84
+ * callbacks: { onConnected: () => console.log("live!") },
85
+ * });
86
+ *
87
+ * const stream = await navigator.mediaDevices.getUserMedia({ video: true });
88
+ * await pub.start(stream);
89
+ * ```
90
+ */
91
+ declare class Publisher {
92
+ private opts;
93
+ private sig;
94
+ private pc;
95
+ private hasAnswer;
96
+ private pendingRemoteCandidates;
97
+ private localStream;
98
+ private readToken;
99
+ constructor(opts: PublisherOptions);
100
+ /** The read token received from the gateway after connecting, for fetching frames. */
101
+ get frameReadToken(): string | null;
102
+ /**
103
+ * Starts the publisher: races all gateways to find the fastest, completes
104
+ * the two-phase handshake, creates the peer connection, and sends the SDP
105
+ * offer. Resolves when the offer has been sent (not when ICE completes —
106
+ * use onConnected for that).
107
+ */
108
+ start(stream: MediaStream): Promise<void>;
109
+ /** Replaces the currently published stream with a new one. */
110
+ replaceStream(stream: MediaStream): Promise<void>;
111
+ /** Stops publishing and tears down the peer connection. */
112
+ stop(): void;
113
+ /** Returns the current RTCPeerConnection, or null if not started. */
114
+ get peerConnection(): RTCPeerConnection | null;
115
+ /** Returns true if the peer connection is in the "connected" state. */
116
+ get isConnected(): boolean;
117
+ private raceGateways;
118
+ private handleSignal;
119
+ /** Waits for ICE gathering to reach the "complete" state. */
120
+ private gatherComplete;
121
+ }
122
+
123
+ /**
124
+ * Options for {@link captureCamera}.
125
+ *
126
+ * These are merged shallowly over the library defaults: any field you provide
127
+ * replaces the default for that field entirely (e.g. passing `video` overrides
128
+ * the default video constraints rather than merging into them). Omit a field to
129
+ * keep its default.
130
+ */
131
+ interface CaptureCameraOptions {
132
+ /**
133
+ * Video constraints, or `true`/`false`. Defaults to a modest resolution and
134
+ * frame rate (see {@link captureCamera}). Set `false` to disable video.
135
+ */
136
+ video?: MediaTrackConstraints | boolean;
137
+ /**
138
+ * Audio constraints, or `true`/`false`. Defaults to `false` — Argus is a
139
+ * video-frame streaming system, so audio is off unless you ask for it.
140
+ */
141
+ audio?: MediaTrackConstraints | boolean;
142
+ }
143
+ /**
144
+ * Options for {@link captureScreen}.
145
+ *
146
+ * These are merged shallowly over the library defaults: any field you provide
147
+ * replaces the default for that field entirely. Omit a field to keep its
148
+ * default.
149
+ */
150
+ interface CaptureScreenOptions {
151
+ /**
152
+ * Video constraints, or `true`. Defaults to a capped width and a low frame
153
+ * rate (see {@link captureScreen}).
154
+ */
155
+ video?: MediaTrackConstraints | boolean;
156
+ /**
157
+ * Audio constraints, or `true`/`false`. Defaults to `false` — screen shares
158
+ * rarely need audio for a video-frame streaming system.
159
+ */
160
+ audio?: MediaTrackConstraints | boolean;
161
+ }
162
+ /**
163
+ * Captures the user's camera via `navigator.mediaDevices.getUserMedia`,
164
+ * applying sensible defaults for a video-frame streaming system.
165
+ *
166
+ * Defaults:
167
+ * - `video`: `{ width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }`
168
+ * — a modest resolution that keeps upload bandwidth reasonable. Cameras are
169
+ * rarely the 4k bandwidth problem that screen capture is, so this is an
170
+ * `ideal` (a hint) rather than a hard cap.
171
+ * - `audio`: `false` — this is a video-frame streaming system.
172
+ *
173
+ * Any option you pass replaces the corresponding default outright (shallow
174
+ * merge), so pass a full `video` constraints object if you want to tweak it.
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * const stream = await captureCamera();
179
+ * await publisher.start(stream);
180
+ * ```
181
+ *
182
+ * @example Front camera with audio:
183
+ * ```ts
184
+ * const stream = await captureCamera({
185
+ * video: { facingMode: "user" },
186
+ * audio: true,
187
+ * });
188
+ * ```
189
+ */
190
+ declare function captureCamera(opts?: CaptureCameraOptions): Promise<MediaStream>;
191
+ /**
192
+ * Captures a screen / window / tab via
193
+ * `navigator.mediaDevices.getDisplayMedia`, applying sensible defaults tuned to
194
+ * avoid the HiDPI/Retina bandwidth trap.
195
+ *
196
+ * Defaults:
197
+ * - `video`: `{ width: { max: 1920 }, frameRate: { ideal: 5, max: 10 } }`.
198
+ * The `width` is a **`max`, not an `ideal`**: on a 2x-Retina display the
199
+ * browser would otherwise capture at native resolution (often 3456px+ /
200
+ * effectively 4k), wasting upload bandwidth and downstream decode cost for no
201
+ * visible benefit. Capping the max roughly halves a 2x-Retina share while
202
+ * leaving smaller displays untouched. Screen content is mostly static, so the
203
+ * low frame rate saves further bandwidth.
204
+ * - `audio`: `false`.
205
+ *
206
+ * IMPORTANT — do NOT add `resizeMode: "none"` here. That value forbids the
207
+ * browser from downscaling the source, which turns the `width: { max: 1920 }`
208
+ * cap into a no-op on exactly the Retina displays it targets. By omitting
209
+ * `resizeMode` we let the user agent scale to satisfy the constraint (its
210
+ * default behaviour), which is the entire point of this helper. It is tempting
211
+ * to add `resizeMode: "none"` back for "sharpness" — don't.
212
+ *
213
+ * Any option you pass replaces the corresponding default outright (shallow
214
+ * merge), so pass a full `video` constraints object if you want to tweak it.
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * const stream = await captureScreen();
219
+ * await publisher.start(stream);
220
+ * ```
221
+ */
222
+ declare function captureScreen(opts?: CaptureScreenOptions): Promise<MediaStream>;
223
+
224
+ export { type CaptureCameraOptions, type CaptureScreenOptions, type GatewayReadyInfo, Publisher, type PublisherCallbacks, type PublisherOptions, type SignalMessage, captureCamera, captureScreen };
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Message types exchanged over the WebSocket signaling channel.
3
+ */
4
+ type SignalMessage = {
5
+ type: "offer";
6
+ sdp: string;
7
+ sdp_type: "offer";
8
+ } | {
9
+ type: "answer";
10
+ sdp: string;
11
+ sdp_type: "answer";
12
+ } | {
13
+ type: "ice_candidate";
14
+ candidate: string;
15
+ sdp_mid?: string;
16
+ sdp_mline_index?: number;
17
+ username_fragment?: string;
18
+ } | {
19
+ type: "connection_state";
20
+ state: string;
21
+ } | {
22
+ type: "error";
23
+ error: string;
24
+ } | {
25
+ type: "accepted";
26
+ } | {
27
+ type: "proceed";
28
+ } | {
29
+ type: "ready";
30
+ turn_url?: string;
31
+ turn_username?: string;
32
+ turn_credential?: string;
33
+ read_token?: string;
34
+ };
35
+ /**
36
+ * Callbacks emitted by the Publisher during its lifecycle.
37
+ */
38
+ interface PublisherCallbacks {
39
+ /** Called when the WebRTC peer connection state changes. */
40
+ onConnectionStateChange?: (state: RTCPeerConnectionState) => void;
41
+ /** Called when a fatal error occurs (e.g. signaling error, WebSocket close). */
42
+ onError?: (error: Error) => void;
43
+ /** Called when the browser has successfully connected to the media server. */
44
+ onConnected?: () => void;
45
+ }
46
+ /**
47
+ * TURN and read-token info delivered in the gateway `ready` message.
48
+ */
49
+ interface GatewayReadyInfo {
50
+ turn_url?: string;
51
+ turn_username?: string;
52
+ turn_credential?: string;
53
+ read_token?: string;
54
+ }
55
+ /**
56
+ * Options for publishing a media stream.
57
+ */
58
+ interface PublisherOptions {
59
+ /** Gateway WebSocket URLs returned by POST /api/streams `gateway_urls`. All are raced simultaneously. */
60
+ gatewayURLs: string[];
61
+ /** The short-lived join token from POST /api/streams. */
62
+ token: string;
63
+ /** Optional extra ICE servers (e.g. STUN). TURN is supplied by the winning gateway. */
64
+ iceServers?: RTCIceServer[];
65
+ /** Callbacks for lifecycle events. */
66
+ callbacks?: PublisherCallbacks;
67
+ }
68
+
69
+ /**
70
+ * Publisher streams a browser {@link MediaStream} to an Argus media server over
71
+ * WebRTC. Given the `gateway_urls` and `token` from a join-token response, it
72
+ * races the candidate gateways to the fastest one, completes the two-phase
73
+ * signaling handshake, and manages the peer connection — offer/answer exchange,
74
+ * ICE candidate trickling, and track (re)negotiation.
75
+ *
76
+ * After {@link Publisher.start} resolves, {@link Publisher.frameReadToken} holds
77
+ * the token your application server needs to fetch frames for this stream.
78
+ *
79
+ * @example Publish the default camera:
80
+ * ```ts
81
+ * const pub = new Publisher({
82
+ * gatewayURLs: joinResp.gateway_urls,
83
+ * token: joinResp.token,
84
+ * callbacks: { onConnected: () => console.log("live!") },
85
+ * });
86
+ *
87
+ * const stream = await navigator.mediaDevices.getUserMedia({ video: true });
88
+ * await pub.start(stream);
89
+ * ```
90
+ */
91
+ declare class Publisher {
92
+ private opts;
93
+ private sig;
94
+ private pc;
95
+ private hasAnswer;
96
+ private pendingRemoteCandidates;
97
+ private localStream;
98
+ private readToken;
99
+ constructor(opts: PublisherOptions);
100
+ /** The read token received from the gateway after connecting, for fetching frames. */
101
+ get frameReadToken(): string | null;
102
+ /**
103
+ * Starts the publisher: races all gateways to find the fastest, completes
104
+ * the two-phase handshake, creates the peer connection, and sends the SDP
105
+ * offer. Resolves when the offer has been sent (not when ICE completes —
106
+ * use onConnected for that).
107
+ */
108
+ start(stream: MediaStream): Promise<void>;
109
+ /** Replaces the currently published stream with a new one. */
110
+ replaceStream(stream: MediaStream): Promise<void>;
111
+ /** Stops publishing and tears down the peer connection. */
112
+ stop(): void;
113
+ /** Returns the current RTCPeerConnection, or null if not started. */
114
+ get peerConnection(): RTCPeerConnection | null;
115
+ /** Returns true if the peer connection is in the "connected" state. */
116
+ get isConnected(): boolean;
117
+ private raceGateways;
118
+ private handleSignal;
119
+ /** Waits for ICE gathering to reach the "complete" state. */
120
+ private gatherComplete;
121
+ }
122
+
123
+ /**
124
+ * Options for {@link captureCamera}.
125
+ *
126
+ * These are merged shallowly over the library defaults: any field you provide
127
+ * replaces the default for that field entirely (e.g. passing `video` overrides
128
+ * the default video constraints rather than merging into them). Omit a field to
129
+ * keep its default.
130
+ */
131
+ interface CaptureCameraOptions {
132
+ /**
133
+ * Video constraints, or `true`/`false`. Defaults to a modest resolution and
134
+ * frame rate (see {@link captureCamera}). Set `false` to disable video.
135
+ */
136
+ video?: MediaTrackConstraints | boolean;
137
+ /**
138
+ * Audio constraints, or `true`/`false`. Defaults to `false` — Argus is a
139
+ * video-frame streaming system, so audio is off unless you ask for it.
140
+ */
141
+ audio?: MediaTrackConstraints | boolean;
142
+ }
143
+ /**
144
+ * Options for {@link captureScreen}.
145
+ *
146
+ * These are merged shallowly over the library defaults: any field you provide
147
+ * replaces the default for that field entirely. Omit a field to keep its
148
+ * default.
149
+ */
150
+ interface CaptureScreenOptions {
151
+ /**
152
+ * Video constraints, or `true`. Defaults to a capped width and a low frame
153
+ * rate (see {@link captureScreen}).
154
+ */
155
+ video?: MediaTrackConstraints | boolean;
156
+ /**
157
+ * Audio constraints, or `true`/`false`. Defaults to `false` — screen shares
158
+ * rarely need audio for a video-frame streaming system.
159
+ */
160
+ audio?: MediaTrackConstraints | boolean;
161
+ }
162
+ /**
163
+ * Captures the user's camera via `navigator.mediaDevices.getUserMedia`,
164
+ * applying sensible defaults for a video-frame streaming system.
165
+ *
166
+ * Defaults:
167
+ * - `video`: `{ width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }`
168
+ * — a modest resolution that keeps upload bandwidth reasonable. Cameras are
169
+ * rarely the 4k bandwidth problem that screen capture is, so this is an
170
+ * `ideal` (a hint) rather than a hard cap.
171
+ * - `audio`: `false` — this is a video-frame streaming system.
172
+ *
173
+ * Any option you pass replaces the corresponding default outright (shallow
174
+ * merge), so pass a full `video` constraints object if you want to tweak it.
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * const stream = await captureCamera();
179
+ * await publisher.start(stream);
180
+ * ```
181
+ *
182
+ * @example Front camera with audio:
183
+ * ```ts
184
+ * const stream = await captureCamera({
185
+ * video: { facingMode: "user" },
186
+ * audio: true,
187
+ * });
188
+ * ```
189
+ */
190
+ declare function captureCamera(opts?: CaptureCameraOptions): Promise<MediaStream>;
191
+ /**
192
+ * Captures a screen / window / tab via
193
+ * `navigator.mediaDevices.getDisplayMedia`, applying sensible defaults tuned to
194
+ * avoid the HiDPI/Retina bandwidth trap.
195
+ *
196
+ * Defaults:
197
+ * - `video`: `{ width: { max: 1920 }, frameRate: { ideal: 5, max: 10 } }`.
198
+ * The `width` is a **`max`, not an `ideal`**: on a 2x-Retina display the
199
+ * browser would otherwise capture at native resolution (often 3456px+ /
200
+ * effectively 4k), wasting upload bandwidth and downstream decode cost for no
201
+ * visible benefit. Capping the max roughly halves a 2x-Retina share while
202
+ * leaving smaller displays untouched. Screen content is mostly static, so the
203
+ * low frame rate saves further bandwidth.
204
+ * - `audio`: `false`.
205
+ *
206
+ * IMPORTANT — do NOT add `resizeMode: "none"` here. That value forbids the
207
+ * browser from downscaling the source, which turns the `width: { max: 1920 }`
208
+ * cap into a no-op on exactly the Retina displays it targets. By omitting
209
+ * `resizeMode` we let the user agent scale to satisfy the constraint (its
210
+ * default behaviour), which is the entire point of this helper. It is tempting
211
+ * to add `resizeMode: "none"` back for "sharpness" — don't.
212
+ *
213
+ * Any option you pass replaces the corresponding default outright (shallow
214
+ * merge), so pass a full `video` constraints object if you want to tweak it.
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * const stream = await captureScreen();
219
+ * await publisher.start(stream);
220
+ * ```
221
+ */
222
+ declare function captureScreen(opts?: CaptureScreenOptions): Promise<MediaStream>;
223
+
224
+ export { type CaptureCameraOptions, type CaptureScreenOptions, type GatewayReadyInfo, Publisher, type PublisherCallbacks, type PublisherOptions, type SignalMessage, captureCamera, captureScreen };