@furious.luke/argus-js 0.2.0 → 0.2.1
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/dist/index.cjs +2 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +0 -3
- package/dist/index.d.ts +0 -3
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -101,10 +101,9 @@ var Publisher = class {
|
|
|
101
101
|
this.readToken = readyInfo.read_token;
|
|
102
102
|
}
|
|
103
103
|
const iceServers = [...this.opts.iceServers ?? []];
|
|
104
|
-
|
|
105
|
-
if (turnUrls.length > 0) {
|
|
104
|
+
if (readyInfo.turn_urls && readyInfo.turn_urls.length > 0) {
|
|
106
105
|
iceServers.push({
|
|
107
|
-
urls:
|
|
106
|
+
urls: readyInfo.turn_urls,
|
|
108
107
|
username: readyInfo.turn_username,
|
|
109
108
|
credential: readyInfo.turn_credential
|
|
110
109
|
});
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/signaling.ts","../src/publisher.ts","../src/capture.ts"],"sourcesContent":["export { Publisher } from \"./publisher\";\nexport type {\n SignalMessage,\n PublisherCallbacks,\n PublisherOptions,\n GatewayReadyInfo,\n} from \"./types\";\nexport { captureCamera, captureScreen } from \"./capture\";\nexport type {\n CaptureCameraOptions,\n CaptureScreenOptions,\n} from \"./capture\";\n","import type { SignalMessage } from \"./types\";\n\n/**\n * Wraps an open WebSocket to the Argus gateway and handles the JSON message\n * envelope: incoming text frames are parsed into {@link SignalMessage}s and\n * outgoing messages are serialised.\n *\n * This is an internal helper. The {@link Publisher} opens the socket itself\n * (racing all candidate gateways) and hands the winner here, so this class only\n * ever adopts an already-open socket rather than dialing one.\n *\n * @internal\n */\nexport class SignalingChannel {\n private ws: WebSocket;\n\n /** Fired for every incoming JSON message. */\n onMessage: ((msg: SignalMessage) => void) | null = null;\n /** Fired when the underlying WebSocket closes. */\n onClose: (() => void) | null = null;\n /** Fired when an error occurs on the WebSocket. */\n onError: ((err: Error) => void) | null = null;\n\n private constructor(ws: WebSocket) {\n this.ws = ws;\n }\n\n /**\n * Adopts an already-open WebSocket (e.g. the winner of a gateway race),\n * routing its events through the channel's callbacks. Any handlers previously\n * attached to the socket are replaced.\n */\n static wrap(ws: WebSocket): SignalingChannel {\n const ch = new SignalingChannel(ws);\n ws.onmessage = (ev: MessageEvent) => {\n const msg = parseSignal(ev.data);\n if (msg) ch.onMessage?.(msg);\n };\n ws.onerror = () => ch.onError?.(new Error(\"WebSocket error\"));\n ws.onclose = () => ch.onClose?.();\n return ch;\n }\n\n /** Sends a JSON message if the socket is open; a no-op otherwise. */\n send(msg: SignalMessage): void {\n if (this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n }\n\n /** Closes the underlying WebSocket. */\n close(): void {\n this.ws.close();\n }\n}\n\n/** Parses a WebSocket text frame into a SignalMessage, or null if malformed. */\nfunction parseSignal(data: unknown): SignalMessage | null {\n try {\n return JSON.parse(data as string) as SignalMessage;\n } catch {\n return null;\n }\n}\n","import { SignalingChannel } from \"./signaling\";\nimport type { GatewayReadyInfo, PublisherOptions, SignalMessage } from \"./types\";\n\n/**\n * Publisher streams a browser {@link MediaStream} to an Argus media server over\n * WebRTC. Given the `gateway_urls` and `token` from a join-token response, it\n * races the candidate gateways to the fastest one, completes the two-phase\n * signaling handshake, and manages the peer connection — offer/answer exchange,\n * ICE candidate trickling, and track (re)negotiation.\n *\n * After {@link Publisher.start} resolves, {@link Publisher.frameReadToken} holds\n * the token your application server needs to fetch frames for this stream.\n *\n * @example Publish the default camera:\n * ```ts\n * const pub = new Publisher({\n * gatewayURLs: joinResp.gateway_urls,\n * token: joinResp.token,\n * callbacks: { onConnected: () => console.log(\"live!\") },\n * });\n *\n * const stream = await navigator.mediaDevices.getUserMedia({ video: true });\n * await pub.start(stream);\n * ```\n */\nexport class Publisher {\n private opts: PublisherOptions;\n private sig: SignalingChannel | null = null;\n private pc: RTCPeerConnection | null = null;\n private hasAnswer = false;\n private pendingRemoteCandidates: RTCIceCandidateInit[] = [];\n private localStream: MediaStream | null = null;\n private readToken: string | null = null;\n\n constructor(opts: PublisherOptions) {\n this.opts = opts;\n }\n\n /** The read token received from the gateway after connecting, for fetching frames. */\n get frameReadToken(): string | null { return this.readToken; }\n\n /**\n * Starts the publisher: races all gateways to find the fastest, completes\n * the two-phase handshake, creates the peer connection, and sends the SDP\n * offer. Resolves when the offer has been sent (not when ICE completes —\n * use onConnected for that).\n */\n async start(stream: MediaStream): Promise<void> {\n this.localStream = stream;\n\n // Race all gateways; returns winning WebSocket + TURN/read-token info\n const { ws, readyInfo } = await this.raceGateways();\n\n // Store read token for caller\n if (readyInfo.read_token) {\n this.readToken = readyInfo.read_token;\n }\n\n // Build ICE servers: extra servers from opts + TURN from gateway. Prefer\n // the multi-transport turn_urls (UDP + TCP) so relay can fall back to TCP\n // on UDP-blocking networks; fall back to the legacy single turn_url. A\n // single credential is valid for every transport.\n const iceServers: RTCIceServer[] = [...(this.opts.iceServers ?? [])];\n const turnUrls =\n readyInfo.turn_urls && readyInfo.turn_urls.length > 0\n ? readyInfo.turn_urls\n : readyInfo.turn_url\n ? [readyInfo.turn_url]\n : [];\n if (turnUrls.length > 0) {\n iceServers.push({\n urls: turnUrls,\n username: readyInfo.turn_username,\n credential: readyInfo.turn_credential,\n });\n }\n\n this.pc = new RTCPeerConnection({ iceServers });\n\n this.pc.onicecandidate = (ev) => {\n if (!ev.candidate || !this.sig) return;\n const c = ev.candidate;\n this.sig.send({\n type: \"ice_candidate\",\n candidate: c.candidate,\n sdp_mid: c.sdpMid ?? undefined,\n sdp_mline_index: c.sdpMLineIndex ?? undefined,\n username_fragment: c.usernameFragment ?? undefined,\n });\n };\n\n this.pc.onconnectionstatechange = () => {\n const state = this.pc?.connectionState;\n if (state) this.opts.callbacks?.onConnectionStateChange?.(state);\n if (state === \"connected\") this.opts.callbacks?.onConnected?.();\n };\n\n for (const track of stream.getTracks()) {\n this.pc.addTrack(track, stream);\n }\n\n const offer = await this.pc.createOffer();\n await this.pc.setLocalDescription(offer);\n await this.gatherComplete();\n\n // Wrap winning WebSocket in SignalingChannel\n this.sig = SignalingChannel.wrap(ws);\n this.sig.onMessage = (msg) => this.handleSignal(msg);\n this.sig.onClose = () => this.opts.callbacks?.onError?.(new Error(\"signaling closed\"));\n this.sig.onError = (err) => this.opts.callbacks?.onError?.(err);\n\n const local = this.pc.localDescription;\n if (!local) throw new Error(\"local description missing after gather\");\n this.sig.send({ type: \"offer\", sdp: local.sdp, sdp_type: \"offer\" });\n }\n\n /** Replaces the currently published stream with a new one. */\n async replaceStream(stream: MediaStream): Promise<void> {\n if (!this.pc) throw new Error(\"publisher not started\");\n\n // Remove old tracks.\n const senders = this.pc.getSenders();\n for (const sender of senders) {\n if (sender.track) {\n this.pc.removeTrack(sender);\n }\n }\n\n // Add new tracks.\n for (const track of stream.getTracks()) {\n this.pc.addTrack(track, stream);\n }\n this.localStream = stream;\n\n // Renegotiate.\n const offer = await this.pc.createOffer();\n await this.pc.setLocalDescription(offer);\n await this.gatherComplete();\n\n const local = this.pc.localDescription;\n if (!local) throw new Error(\"local description missing\");\n this.sig?.send({\n type: \"offer\",\n sdp: local.sdp,\n sdp_type: \"offer\",\n });\n }\n\n /** Stops publishing and tears down the peer connection. */\n stop(): void {\n this.sig?.close();\n this.sig = null;\n\n this.localStream?.getTracks().forEach((t) => t.stop());\n this.localStream = null;\n\n this.pc?.close();\n this.pc = null;\n this.hasAnswer = false;\n this.pendingRemoteCandidates = [];\n this.readToken = null;\n }\n\n /** Returns the current RTCPeerConnection, or null if not started. */\n get peerConnection(): RTCPeerConnection | null {\n return this.pc;\n }\n\n /** Returns true if the peer connection is in the \"connected\" state. */\n get isConnected(): boolean {\n return this.pc?.connectionState === \"connected\";\n }\n\n // -------------------------------------------------------------------------\n // Private helpers\n // -------------------------------------------------------------------------\n\n private raceGateways(): Promise<{ ws: WebSocket; readyInfo: GatewayReadyInfo }> {\n return new Promise((resolve, reject) => {\n const { gatewayURLs, token } = this.opts;\n if (gatewayURLs.length === 0) {\n reject(new Error(\"no gateway URLs provided\"));\n return;\n }\n\n const sockets: WebSocket[] = [];\n let settled = false;\n\n const closeAll = (except?: WebSocket) => {\n for (const s of sockets) {\n if (s !== except) {\n s.onmessage = null;\n s.onerror = null;\n s.onclose = null;\n s.close();\n }\n }\n };\n\n const checkAllFailed = () => {\n if (settled) return;\n if (sockets.every(s => s.readyState === WebSocket.CLOSED || s.readyState === WebSocket.CLOSING)) {\n settled = true;\n reject(new Error(\"all gateways failed to connect\"));\n }\n };\n\n for (const gatewayURL of gatewayURLs) {\n const u = new URL(gatewayURL);\n u.searchParams.set(\"token\", token);\n const ws = new WebSocket(u.toString());\n sockets.push(ws);\n\n let accepted = false;\n\n ws.onmessage = (ev: MessageEvent) => {\n if (settled) return;\n try {\n const msg = JSON.parse(ev.data as string);\n if (!accepted && msg.type === \"accepted\") {\n accepted = true;\n ws.send(JSON.stringify({ type: \"proceed\" }));\n } else if (accepted && msg.type === \"ready\") {\n settled = true;\n closeAll(ws);\n resolve({ ws, readyInfo: msg as GatewayReadyInfo });\n }\n } catch {\n /* ignore malformed */\n }\n };\n\n ws.onerror = () => checkAllFailed();\n ws.onclose = () => checkAllFailed();\n }\n });\n }\n\n private handleSignal(msg: SignalMessage): void {\n switch (msg.type) {\n case \"answer\": {\n if (!this.pc) return;\n this.pc\n .setRemoteDescription(\n new RTCSessionDescription({ type: \"answer\", sdp: msg.sdp }),\n )\n .then(() => {\n this.hasAnswer = true;\n // Flush any ICE candidates that arrived before the answer.\n for (const init of this.pendingRemoteCandidates) {\n this.pc?.addIceCandidate(init).catch(() => {\n /* ignore stale candidates */\n });\n }\n this.pendingRemoteCandidates = [];\n })\n .catch((err) => {\n this.opts.callbacks?.onError?.(\n new Error(`failed to set remote description: ${err}`),\n );\n });\n break;\n }\n\n case \"ice_candidate\": {\n if (!this.pc) return;\n const init: RTCIceCandidateInit = {\n candidate: msg.candidate,\n sdpMid: msg.sdp_mid ?? null,\n sdpMLineIndex: msg.sdp_mline_index ?? null,\n usernameFragment: msg.username_fragment ?? null,\n };\n if (this.hasAnswer) {\n this.pc.addIceCandidate(init).catch(() => {\n /* ignore stale candidates */\n });\n } else {\n this.pendingRemoteCandidates.push(init);\n }\n break;\n }\n\n case \"connection_state\": {\n // The server echoes the peer connection state; the onconnectionstatechange\n // handler above already covers this, but callers can also react via\n // onConnectionStateChange.\n break;\n }\n\n case \"error\": {\n this.opts.callbacks?.onError?.(new Error(msg.error));\n break;\n }\n }\n }\n\n /** Waits for ICE gathering to reach the \"complete\" state. */\n private gatherComplete(): Promise<void> {\n return new Promise((resolve) => {\n const pc = this.pc;\n if (!pc) {\n resolve();\n return;\n }\n if (pc.iceGatheringState === \"complete\") {\n resolve();\n return;\n }\n const handler = () => {\n if (pc.iceGatheringState === \"complete\") {\n pc.removeEventListener(\"icegatheringstatechange\", handler);\n resolve();\n }\n };\n pc.addEventListener(\"icegatheringstatechange\", handler);\n });\n }\n}\n","/**\n * Options for {@link captureCamera}.\n *\n * These are merged shallowly over the library defaults: any field you provide\n * replaces the default for that field entirely (e.g. passing `video` overrides\n * the default video constraints rather than merging into them). Omit a field to\n * keep its default.\n */\nexport interface CaptureCameraOptions {\n /**\n * Video constraints, or `true`/`false`. Defaults to a modest resolution and\n * frame rate (see {@link captureCamera}). Set `false` to disable video.\n */\n video?: MediaTrackConstraints | boolean;\n /**\n * Audio constraints, or `true`/`false`. Defaults to `false` — Argus is a\n * video-frame streaming system, so audio is off unless you ask for it.\n */\n audio?: MediaTrackConstraints | boolean;\n /**\n * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the browser's permission\n * prompt and transient-activation check are anchored to that window rather\n * than the one holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\n}\n\n/**\n * Options for {@link captureScreen}.\n *\n * These are merged shallowly over the library defaults: any field you provide\n * replaces the default for that field entirely. Omit a field to keep its\n * default.\n */\nexport interface CaptureScreenOptions {\n /**\n * Video constraints, or `true`. Defaults to a capped width and a low frame\n * rate (see {@link captureScreen}).\n */\n video?: MediaTrackConstraints | boolean;\n /**\n * Audio constraints, or `true`/`false`. Defaults to `false` — screen shares\n * rarely need audio for a video-frame streaming system.\n */\n audio?: MediaTrackConstraints | boolean;\n /**\n * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the screen picker and its\n * transient-activation check are anchored to that window rather than the one\n * holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\n}\n\n/**\n * Captures the user's camera via `navigator.mediaDevices.getUserMedia`,\n * applying sensible defaults for a video-frame streaming system.\n *\n * Defaults:\n * - `video`: `{ width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }`\n * — a modest resolution that keeps upload bandwidth reasonable. Cameras are\n * rarely the 4k bandwidth problem that screen capture is, so this is an\n * `ideal` (a hint) rather than a hard cap.\n * - `audio`: `false` — this is a video-frame streaming system.\n *\n * Any option you pass replaces the corresponding default outright (shallow\n * merge), so pass a full `video` constraints object if you want to tweak it.\n *\n * @example\n * ```ts\n * const stream = await captureCamera();\n * await publisher.start(stream);\n * ```\n *\n * @example Front camera with audio:\n * ```ts\n * const stream = await captureCamera({\n * video: { facingMode: \"user\" },\n * audio: true,\n * });\n * ```\n */\nexport async function captureCamera(\n opts: CaptureCameraOptions = {},\n): Promise<MediaStream> {\n const constraints: MediaStreamConstraints = {\n video: opts.video ?? {\n width: { ideal: 1280 },\n height: { ideal: 720 },\n frameRate: { ideal: 30 },\n },\n audio: opts.audio ?? false,\n };\n const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return mediaDevices.getUserMedia(constraints);\n}\n\n/**\n * Captures a screen / window / tab via\n * `navigator.mediaDevices.getDisplayMedia`, applying sensible defaults tuned to\n * avoid the HiDPI/Retina bandwidth trap.\n *\n * Defaults:\n * - `video`: `{ width: { max: 1920 }, frameRate: { ideal: 5, max: 10 } }`.\n * The `width` is a **`max`, not an `ideal`**: on a 2x-Retina display the\n * browser would otherwise capture at native resolution (often 3456px+ /\n * effectively 4k), wasting upload bandwidth and downstream decode cost for no\n * visible benefit. Capping the max roughly halves a 2x-Retina share while\n * leaving smaller displays untouched. Screen content is mostly static, so the\n * low frame rate saves further bandwidth.\n * - `audio`: `false`.\n *\n * IMPORTANT — do NOT add `resizeMode: \"none\"` here. That value forbids the\n * browser from downscaling the source, which turns the `width: { max: 1920 }`\n * cap into a no-op on exactly the Retina displays it targets. By omitting\n * `resizeMode` we let the user agent scale to satisfy the constraint (its\n * default behaviour), which is the entire point of this helper. It is tempting\n * to add `resizeMode: \"none\"` back for \"sharpness\" — don't.\n *\n * Any option you pass replaces the corresponding default outright (shallow\n * merge), so pass a full `video` constraints object if you want to tweak it.\n *\n * @example\n * ```ts\n * const stream = await captureScreen();\n * await publisher.start(stream);\n * ```\n */\nexport async function captureScreen(\n opts: CaptureScreenOptions = {},\n): Promise<MediaStream> {\n const constraints: MediaStreamConstraints = {\n video: opts.video ?? {\n width: { max: 1920 },\n frameRate: { ideal: 5, max: 10 },\n },\n audio: opts.audio ?? false,\n };\n const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return mediaDevices.getDisplayMedia(constraints);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/signaling.ts","../src/publisher.ts","../src/capture.ts"],"sourcesContent":["export { Publisher } from \"./publisher\";\nexport type {\n SignalMessage,\n PublisherCallbacks,\n PublisherOptions,\n GatewayReadyInfo,\n} from \"./types\";\nexport { captureCamera, captureScreen } from \"./capture\";\nexport type {\n CaptureCameraOptions,\n CaptureScreenOptions,\n} from \"./capture\";\n","import type { SignalMessage } from \"./types\";\n\n/**\n * Wraps an open WebSocket to the Argus gateway and handles the JSON message\n * envelope: incoming text frames are parsed into {@link SignalMessage}s and\n * outgoing messages are serialised.\n *\n * This is an internal helper. The {@link Publisher} opens the socket itself\n * (racing all candidate gateways) and hands the winner here, so this class only\n * ever adopts an already-open socket rather than dialing one.\n *\n * @internal\n */\nexport class SignalingChannel {\n private ws: WebSocket;\n\n /** Fired for every incoming JSON message. */\n onMessage: ((msg: SignalMessage) => void) | null = null;\n /** Fired when the underlying WebSocket closes. */\n onClose: (() => void) | null = null;\n /** Fired when an error occurs on the WebSocket. */\n onError: ((err: Error) => void) | null = null;\n\n private constructor(ws: WebSocket) {\n this.ws = ws;\n }\n\n /**\n * Adopts an already-open WebSocket (e.g. the winner of a gateway race),\n * routing its events through the channel's callbacks. Any handlers previously\n * attached to the socket are replaced.\n */\n static wrap(ws: WebSocket): SignalingChannel {\n const ch = new SignalingChannel(ws);\n ws.onmessage = (ev: MessageEvent) => {\n const msg = parseSignal(ev.data);\n if (msg) ch.onMessage?.(msg);\n };\n ws.onerror = () => ch.onError?.(new Error(\"WebSocket error\"));\n ws.onclose = () => ch.onClose?.();\n return ch;\n }\n\n /** Sends a JSON message if the socket is open; a no-op otherwise. */\n send(msg: SignalMessage): void {\n if (this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n }\n\n /** Closes the underlying WebSocket. */\n close(): void {\n this.ws.close();\n }\n}\n\n/** Parses a WebSocket text frame into a SignalMessage, or null if malformed. */\nfunction parseSignal(data: unknown): SignalMessage | null {\n try {\n return JSON.parse(data as string) as SignalMessage;\n } catch {\n return null;\n }\n}\n","import { SignalingChannel } from \"./signaling\";\nimport type { GatewayReadyInfo, PublisherOptions, SignalMessage } from \"./types\";\n\n/**\n * Publisher streams a browser {@link MediaStream} to an Argus media server over\n * WebRTC. Given the `gateway_urls` and `token` from a join-token response, it\n * races the candidate gateways to the fastest one, completes the two-phase\n * signaling handshake, and manages the peer connection — offer/answer exchange,\n * ICE candidate trickling, and track (re)negotiation.\n *\n * After {@link Publisher.start} resolves, {@link Publisher.frameReadToken} holds\n * the token your application server needs to fetch frames for this stream.\n *\n * @example Publish the default camera:\n * ```ts\n * const pub = new Publisher({\n * gatewayURLs: joinResp.gateway_urls,\n * token: joinResp.token,\n * callbacks: { onConnected: () => console.log(\"live!\") },\n * });\n *\n * const stream = await navigator.mediaDevices.getUserMedia({ video: true });\n * await pub.start(stream);\n * ```\n */\nexport class Publisher {\n private opts: PublisherOptions;\n private sig: SignalingChannel | null = null;\n private pc: RTCPeerConnection | null = null;\n private hasAnswer = false;\n private pendingRemoteCandidates: RTCIceCandidateInit[] = [];\n private localStream: MediaStream | null = null;\n private readToken: string | null = null;\n\n constructor(opts: PublisherOptions) {\n this.opts = opts;\n }\n\n /** The read token received from the gateway after connecting, for fetching frames. */\n get frameReadToken(): string | null { return this.readToken; }\n\n /**\n * Starts the publisher: races all gateways to find the fastest, completes\n * the two-phase handshake, creates the peer connection, and sends the SDP\n * offer. Resolves when the offer has been sent (not when ICE completes —\n * use onConnected for that).\n */\n async start(stream: MediaStream): Promise<void> {\n this.localStream = stream;\n\n // Race all gateways; returns winning WebSocket + TURN/read-token info\n const { ws, readyInfo } = await this.raceGateways();\n\n // Store read token for caller\n if (readyInfo.read_token) {\n this.readToken = readyInfo.read_token;\n }\n\n // Build ICE servers: extra servers from opts + TURN from gateway. The\n // gateway supplies multi-transport turn_urls (UDP + TCP) so relay can fall\n // back to TCP on UDP-blocking networks. A single credential is valid for\n // every transport.\n const iceServers: RTCIceServer[] = [...(this.opts.iceServers ?? [])];\n if (readyInfo.turn_urls && readyInfo.turn_urls.length > 0) {\n iceServers.push({\n urls: readyInfo.turn_urls,\n username: readyInfo.turn_username,\n credential: readyInfo.turn_credential,\n });\n }\n\n this.pc = new RTCPeerConnection({ iceServers });\n\n this.pc.onicecandidate = (ev) => {\n if (!ev.candidate || !this.sig) return;\n const c = ev.candidate;\n this.sig.send({\n type: \"ice_candidate\",\n candidate: c.candidate,\n sdp_mid: c.sdpMid ?? undefined,\n sdp_mline_index: c.sdpMLineIndex ?? undefined,\n username_fragment: c.usernameFragment ?? undefined,\n });\n };\n\n this.pc.onconnectionstatechange = () => {\n const state = this.pc?.connectionState;\n if (state) this.opts.callbacks?.onConnectionStateChange?.(state);\n if (state === \"connected\") this.opts.callbacks?.onConnected?.();\n };\n\n for (const track of stream.getTracks()) {\n this.pc.addTrack(track, stream);\n }\n\n const offer = await this.pc.createOffer();\n await this.pc.setLocalDescription(offer);\n await this.gatherComplete();\n\n // Wrap winning WebSocket in SignalingChannel\n this.sig = SignalingChannel.wrap(ws);\n this.sig.onMessage = (msg) => this.handleSignal(msg);\n this.sig.onClose = () => this.opts.callbacks?.onError?.(new Error(\"signaling closed\"));\n this.sig.onError = (err) => this.opts.callbacks?.onError?.(err);\n\n const local = this.pc.localDescription;\n if (!local) throw new Error(\"local description missing after gather\");\n this.sig.send({ type: \"offer\", sdp: local.sdp, sdp_type: \"offer\" });\n }\n\n /** Replaces the currently published stream with a new one. */\n async replaceStream(stream: MediaStream): Promise<void> {\n if (!this.pc) throw new Error(\"publisher not started\");\n\n // Remove old tracks.\n const senders = this.pc.getSenders();\n for (const sender of senders) {\n if (sender.track) {\n this.pc.removeTrack(sender);\n }\n }\n\n // Add new tracks.\n for (const track of stream.getTracks()) {\n this.pc.addTrack(track, stream);\n }\n this.localStream = stream;\n\n // Renegotiate.\n const offer = await this.pc.createOffer();\n await this.pc.setLocalDescription(offer);\n await this.gatherComplete();\n\n const local = this.pc.localDescription;\n if (!local) throw new Error(\"local description missing\");\n this.sig?.send({\n type: \"offer\",\n sdp: local.sdp,\n sdp_type: \"offer\",\n });\n }\n\n /** Stops publishing and tears down the peer connection. */\n stop(): void {\n this.sig?.close();\n this.sig = null;\n\n this.localStream?.getTracks().forEach((t) => t.stop());\n this.localStream = null;\n\n this.pc?.close();\n this.pc = null;\n this.hasAnswer = false;\n this.pendingRemoteCandidates = [];\n this.readToken = null;\n }\n\n /** Returns the current RTCPeerConnection, or null if not started. */\n get peerConnection(): RTCPeerConnection | null {\n return this.pc;\n }\n\n /** Returns true if the peer connection is in the \"connected\" state. */\n get isConnected(): boolean {\n return this.pc?.connectionState === \"connected\";\n }\n\n // -------------------------------------------------------------------------\n // Private helpers\n // -------------------------------------------------------------------------\n\n private raceGateways(): Promise<{ ws: WebSocket; readyInfo: GatewayReadyInfo }> {\n return new Promise((resolve, reject) => {\n const { gatewayURLs, token } = this.opts;\n if (gatewayURLs.length === 0) {\n reject(new Error(\"no gateway URLs provided\"));\n return;\n }\n\n const sockets: WebSocket[] = [];\n let settled = false;\n\n const closeAll = (except?: WebSocket) => {\n for (const s of sockets) {\n if (s !== except) {\n s.onmessage = null;\n s.onerror = null;\n s.onclose = null;\n s.close();\n }\n }\n };\n\n const checkAllFailed = () => {\n if (settled) return;\n if (sockets.every(s => s.readyState === WebSocket.CLOSED || s.readyState === WebSocket.CLOSING)) {\n settled = true;\n reject(new Error(\"all gateways failed to connect\"));\n }\n };\n\n for (const gatewayURL of gatewayURLs) {\n const u = new URL(gatewayURL);\n u.searchParams.set(\"token\", token);\n const ws = new WebSocket(u.toString());\n sockets.push(ws);\n\n let accepted = false;\n\n ws.onmessage = (ev: MessageEvent) => {\n if (settled) return;\n try {\n const msg = JSON.parse(ev.data as string);\n if (!accepted && msg.type === \"accepted\") {\n accepted = true;\n ws.send(JSON.stringify({ type: \"proceed\" }));\n } else if (accepted && msg.type === \"ready\") {\n settled = true;\n closeAll(ws);\n resolve({ ws, readyInfo: msg as GatewayReadyInfo });\n }\n } catch {\n /* ignore malformed */\n }\n };\n\n ws.onerror = () => checkAllFailed();\n ws.onclose = () => checkAllFailed();\n }\n });\n }\n\n private handleSignal(msg: SignalMessage): void {\n switch (msg.type) {\n case \"answer\": {\n if (!this.pc) return;\n this.pc\n .setRemoteDescription(\n new RTCSessionDescription({ type: \"answer\", sdp: msg.sdp }),\n )\n .then(() => {\n this.hasAnswer = true;\n // Flush any ICE candidates that arrived before the answer.\n for (const init of this.pendingRemoteCandidates) {\n this.pc?.addIceCandidate(init).catch(() => {\n /* ignore stale candidates */\n });\n }\n this.pendingRemoteCandidates = [];\n })\n .catch((err) => {\n this.opts.callbacks?.onError?.(\n new Error(`failed to set remote description: ${err}`),\n );\n });\n break;\n }\n\n case \"ice_candidate\": {\n if (!this.pc) return;\n const init: RTCIceCandidateInit = {\n candidate: msg.candidate,\n sdpMid: msg.sdp_mid ?? null,\n sdpMLineIndex: msg.sdp_mline_index ?? null,\n usernameFragment: msg.username_fragment ?? null,\n };\n if (this.hasAnswer) {\n this.pc.addIceCandidate(init).catch(() => {\n /* ignore stale candidates */\n });\n } else {\n this.pendingRemoteCandidates.push(init);\n }\n break;\n }\n\n case \"connection_state\": {\n // The server echoes the peer connection state; the onconnectionstatechange\n // handler above already covers this, but callers can also react via\n // onConnectionStateChange.\n break;\n }\n\n case \"error\": {\n this.opts.callbacks?.onError?.(new Error(msg.error));\n break;\n }\n }\n }\n\n /** Waits for ICE gathering to reach the \"complete\" state. */\n private gatherComplete(): Promise<void> {\n return new Promise((resolve) => {\n const pc = this.pc;\n if (!pc) {\n resolve();\n return;\n }\n if (pc.iceGatheringState === \"complete\") {\n resolve();\n return;\n }\n const handler = () => {\n if (pc.iceGatheringState === \"complete\") {\n pc.removeEventListener(\"icegatheringstatechange\", handler);\n resolve();\n }\n };\n pc.addEventListener(\"icegatheringstatechange\", handler);\n });\n }\n}\n","/**\n * Options for {@link captureCamera}.\n *\n * These are merged shallowly over the library defaults: any field you provide\n * replaces the default for that field entirely (e.g. passing `video` overrides\n * the default video constraints rather than merging into them). Omit a field to\n * keep its default.\n */\nexport interface CaptureCameraOptions {\n /**\n * Video constraints, or `true`/`false`. Defaults to a modest resolution and\n * frame rate (see {@link captureCamera}). Set `false` to disable video.\n */\n video?: MediaTrackConstraints | boolean;\n /**\n * Audio constraints, or `true`/`false`. Defaults to `false` — Argus is a\n * video-frame streaming system, so audio is off unless you ask for it.\n */\n audio?: MediaTrackConstraints | boolean;\n /**\n * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the browser's permission\n * prompt and transient-activation check are anchored to that window rather\n * than the one holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\n}\n\n/**\n * Options for {@link captureScreen}.\n *\n * These are merged shallowly over the library defaults: any field you provide\n * replaces the default for that field entirely. Omit a field to keep its\n * default.\n */\nexport interface CaptureScreenOptions {\n /**\n * Video constraints, or `true`. Defaults to a capped width and a low frame\n * rate (see {@link captureScreen}).\n */\n video?: MediaTrackConstraints | boolean;\n /**\n * Audio constraints, or `true`/`false`. Defaults to `false` — screen shares\n * rarely need audio for a video-frame streaming system.\n */\n audio?: MediaTrackConstraints | boolean;\n /**\n * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the screen picker and its\n * transient-activation check are anchored to that window rather than the one\n * holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\n}\n\n/**\n * Captures the user's camera via `navigator.mediaDevices.getUserMedia`,\n * applying sensible defaults for a video-frame streaming system.\n *\n * Defaults:\n * - `video`: `{ width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }`\n * — a modest resolution that keeps upload bandwidth reasonable. Cameras are\n * rarely the 4k bandwidth problem that screen capture is, so this is an\n * `ideal` (a hint) rather than a hard cap.\n * - `audio`: `false` — this is a video-frame streaming system.\n *\n * Any option you pass replaces the corresponding default outright (shallow\n * merge), so pass a full `video` constraints object if you want to tweak it.\n *\n * @example\n * ```ts\n * const stream = await captureCamera();\n * await publisher.start(stream);\n * ```\n *\n * @example Front camera with audio:\n * ```ts\n * const stream = await captureCamera({\n * video: { facingMode: \"user\" },\n * audio: true,\n * });\n * ```\n */\nexport async function captureCamera(\n opts: CaptureCameraOptions = {},\n): Promise<MediaStream> {\n const constraints: MediaStreamConstraints = {\n video: opts.video ?? {\n width: { ideal: 1280 },\n height: { ideal: 720 },\n frameRate: { ideal: 30 },\n },\n audio: opts.audio ?? false,\n };\n const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return mediaDevices.getUserMedia(constraints);\n}\n\n/**\n * Captures a screen / window / tab via\n * `navigator.mediaDevices.getDisplayMedia`, applying sensible defaults tuned to\n * avoid the HiDPI/Retina bandwidth trap.\n *\n * Defaults:\n * - `video`: `{ width: { max: 1920 }, frameRate: { ideal: 5, max: 10 } }`.\n * The `width` is a **`max`, not an `ideal`**: on a 2x-Retina display the\n * browser would otherwise capture at native resolution (often 3456px+ /\n * effectively 4k), wasting upload bandwidth and downstream decode cost for no\n * visible benefit. Capping the max roughly halves a 2x-Retina share while\n * leaving smaller displays untouched. Screen content is mostly static, so the\n * low frame rate saves further bandwidth.\n * - `audio`: `false`.\n *\n * IMPORTANT — do NOT add `resizeMode: \"none\"` here. That value forbids the\n * browser from downscaling the source, which turns the `width: { max: 1920 }`\n * cap into a no-op on exactly the Retina displays it targets. By omitting\n * `resizeMode` we let the user agent scale to satisfy the constraint (its\n * default behaviour), which is the entire point of this helper. It is tempting\n * to add `resizeMode: \"none\"` back for \"sharpness\" — don't.\n *\n * Any option you pass replaces the corresponding default outright (shallow\n * merge), so pass a full `video` constraints object if you want to tweak it.\n *\n * @example\n * ```ts\n * const stream = await captureScreen();\n * await publisher.start(stream);\n * ```\n */\nexport async function captureScreen(\n opts: CaptureScreenOptions = {},\n): Promise<MediaStream> {\n const constraints: MediaStreamConstraints = {\n video: opts.video ?? {\n width: { max: 1920 },\n frameRate: { ideal: 5, max: 10 },\n },\n audio: opts.audio ?? false,\n };\n const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return mediaDevices.getDisplayMedia(constraints);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -27,7 +27,6 @@ type SignalMessage = {
|
|
|
27
27
|
type: "proceed";
|
|
28
28
|
} | {
|
|
29
29
|
type: "ready";
|
|
30
|
-
turn_url?: string;
|
|
31
30
|
turn_urls?: string[];
|
|
32
31
|
turn_username?: string;
|
|
33
32
|
turn_credential?: string;
|
|
@@ -48,8 +47,6 @@ interface PublisherCallbacks {
|
|
|
48
47
|
* TURN and read-token info delivered in the gateway `ready` message.
|
|
49
48
|
*/
|
|
50
49
|
interface GatewayReadyInfo {
|
|
51
|
-
/** Legacy single (UDP) TURN URL. Prefer `turn_urls` when present. */
|
|
52
|
-
turn_url?: string;
|
|
53
50
|
/** Ordered TURN URLs (UDP first, then TCP) so relay can fall back to TCP on UDP-blocking networks. */
|
|
54
51
|
turn_urls?: string[];
|
|
55
52
|
turn_username?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -27,7 +27,6 @@ type SignalMessage = {
|
|
|
27
27
|
type: "proceed";
|
|
28
28
|
} | {
|
|
29
29
|
type: "ready";
|
|
30
|
-
turn_url?: string;
|
|
31
30
|
turn_urls?: string[];
|
|
32
31
|
turn_username?: string;
|
|
33
32
|
turn_credential?: string;
|
|
@@ -48,8 +47,6 @@ interface PublisherCallbacks {
|
|
|
48
47
|
* TURN and read-token info delivered in the gateway `ready` message.
|
|
49
48
|
*/
|
|
50
49
|
interface GatewayReadyInfo {
|
|
51
|
-
/** Legacy single (UDP) TURN URL. Prefer `turn_urls` when present. */
|
|
52
|
-
turn_url?: string;
|
|
53
50
|
/** Ordered TURN URLs (UDP first, then TCP) so relay can fall back to TCP on UDP-blocking networks. */
|
|
54
51
|
turn_urls?: string[];
|
|
55
52
|
turn_username?: string;
|
package/dist/index.js
CHANGED
|
@@ -73,10 +73,9 @@ var Publisher = class {
|
|
|
73
73
|
this.readToken = readyInfo.read_token;
|
|
74
74
|
}
|
|
75
75
|
const iceServers = [...this.opts.iceServers ?? []];
|
|
76
|
-
|
|
77
|
-
if (turnUrls.length > 0) {
|
|
76
|
+
if (readyInfo.turn_urls && readyInfo.turn_urls.length > 0) {
|
|
78
77
|
iceServers.push({
|
|
79
|
-
urls:
|
|
78
|
+
urls: readyInfo.turn_urls,
|
|
80
79
|
username: readyInfo.turn_username,
|
|
81
80
|
credential: readyInfo.turn_credential
|
|
82
81
|
});
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/signaling.ts","../src/publisher.ts","../src/capture.ts"],"sourcesContent":["import type { SignalMessage } from \"./types\";\n\n/**\n * Wraps an open WebSocket to the Argus gateway and handles the JSON message\n * envelope: incoming text frames are parsed into {@link SignalMessage}s and\n * outgoing messages are serialised.\n *\n * This is an internal helper. The {@link Publisher} opens the socket itself\n * (racing all candidate gateways) and hands the winner here, so this class only\n * ever adopts an already-open socket rather than dialing one.\n *\n * @internal\n */\nexport class SignalingChannel {\n private ws: WebSocket;\n\n /** Fired for every incoming JSON message. */\n onMessage: ((msg: SignalMessage) => void) | null = null;\n /** Fired when the underlying WebSocket closes. */\n onClose: (() => void) | null = null;\n /** Fired when an error occurs on the WebSocket. */\n onError: ((err: Error) => void) | null = null;\n\n private constructor(ws: WebSocket) {\n this.ws = ws;\n }\n\n /**\n * Adopts an already-open WebSocket (e.g. the winner of a gateway race),\n * routing its events through the channel's callbacks. Any handlers previously\n * attached to the socket are replaced.\n */\n static wrap(ws: WebSocket): SignalingChannel {\n const ch = new SignalingChannel(ws);\n ws.onmessage = (ev: MessageEvent) => {\n const msg = parseSignal(ev.data);\n if (msg) ch.onMessage?.(msg);\n };\n ws.onerror = () => ch.onError?.(new Error(\"WebSocket error\"));\n ws.onclose = () => ch.onClose?.();\n return ch;\n }\n\n /** Sends a JSON message if the socket is open; a no-op otherwise. */\n send(msg: SignalMessage): void {\n if (this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n }\n\n /** Closes the underlying WebSocket. */\n close(): void {\n this.ws.close();\n }\n}\n\n/** Parses a WebSocket text frame into a SignalMessage, or null if malformed. */\nfunction parseSignal(data: unknown): SignalMessage | null {\n try {\n return JSON.parse(data as string) as SignalMessage;\n } catch {\n return null;\n }\n}\n","import { SignalingChannel } from \"./signaling\";\nimport type { GatewayReadyInfo, PublisherOptions, SignalMessage } from \"./types\";\n\n/**\n * Publisher streams a browser {@link MediaStream} to an Argus media server over\n * WebRTC. Given the `gateway_urls` and `token` from a join-token response, it\n * races the candidate gateways to the fastest one, completes the two-phase\n * signaling handshake, and manages the peer connection — offer/answer exchange,\n * ICE candidate trickling, and track (re)negotiation.\n *\n * After {@link Publisher.start} resolves, {@link Publisher.frameReadToken} holds\n * the token your application server needs to fetch frames for this stream.\n *\n * @example Publish the default camera:\n * ```ts\n * const pub = new Publisher({\n * gatewayURLs: joinResp.gateway_urls,\n * token: joinResp.token,\n * callbacks: { onConnected: () => console.log(\"live!\") },\n * });\n *\n * const stream = await navigator.mediaDevices.getUserMedia({ video: true });\n * await pub.start(stream);\n * ```\n */\nexport class Publisher {\n private opts: PublisherOptions;\n private sig: SignalingChannel | null = null;\n private pc: RTCPeerConnection | null = null;\n private hasAnswer = false;\n private pendingRemoteCandidates: RTCIceCandidateInit[] = [];\n private localStream: MediaStream | null = null;\n private readToken: string | null = null;\n\n constructor(opts: PublisherOptions) {\n this.opts = opts;\n }\n\n /** The read token received from the gateway after connecting, for fetching frames. */\n get frameReadToken(): string | null { return this.readToken; }\n\n /**\n * Starts the publisher: races all gateways to find the fastest, completes\n * the two-phase handshake, creates the peer connection, and sends the SDP\n * offer. Resolves when the offer has been sent (not when ICE completes —\n * use onConnected for that).\n */\n async start(stream: MediaStream): Promise<void> {\n this.localStream = stream;\n\n // Race all gateways; returns winning WebSocket + TURN/read-token info\n const { ws, readyInfo } = await this.raceGateways();\n\n // Store read token for caller\n if (readyInfo.read_token) {\n this.readToken = readyInfo.read_token;\n }\n\n // Build ICE servers: extra servers from opts + TURN from gateway. Prefer\n // the multi-transport turn_urls (UDP + TCP) so relay can fall back to TCP\n // on UDP-blocking networks; fall back to the legacy single turn_url. A\n // single credential is valid for every transport.\n const iceServers: RTCIceServer[] = [...(this.opts.iceServers ?? [])];\n const turnUrls =\n readyInfo.turn_urls && readyInfo.turn_urls.length > 0\n ? readyInfo.turn_urls\n : readyInfo.turn_url\n ? [readyInfo.turn_url]\n : [];\n if (turnUrls.length > 0) {\n iceServers.push({\n urls: turnUrls,\n username: readyInfo.turn_username,\n credential: readyInfo.turn_credential,\n });\n }\n\n this.pc = new RTCPeerConnection({ iceServers });\n\n this.pc.onicecandidate = (ev) => {\n if (!ev.candidate || !this.sig) return;\n const c = ev.candidate;\n this.sig.send({\n type: \"ice_candidate\",\n candidate: c.candidate,\n sdp_mid: c.sdpMid ?? undefined,\n sdp_mline_index: c.sdpMLineIndex ?? undefined,\n username_fragment: c.usernameFragment ?? undefined,\n });\n };\n\n this.pc.onconnectionstatechange = () => {\n const state = this.pc?.connectionState;\n if (state) this.opts.callbacks?.onConnectionStateChange?.(state);\n if (state === \"connected\") this.opts.callbacks?.onConnected?.();\n };\n\n for (const track of stream.getTracks()) {\n this.pc.addTrack(track, stream);\n }\n\n const offer = await this.pc.createOffer();\n await this.pc.setLocalDescription(offer);\n await this.gatherComplete();\n\n // Wrap winning WebSocket in SignalingChannel\n this.sig = SignalingChannel.wrap(ws);\n this.sig.onMessage = (msg) => this.handleSignal(msg);\n this.sig.onClose = () => this.opts.callbacks?.onError?.(new Error(\"signaling closed\"));\n this.sig.onError = (err) => this.opts.callbacks?.onError?.(err);\n\n const local = this.pc.localDescription;\n if (!local) throw new Error(\"local description missing after gather\");\n this.sig.send({ type: \"offer\", sdp: local.sdp, sdp_type: \"offer\" });\n }\n\n /** Replaces the currently published stream with a new one. */\n async replaceStream(stream: MediaStream): Promise<void> {\n if (!this.pc) throw new Error(\"publisher not started\");\n\n // Remove old tracks.\n const senders = this.pc.getSenders();\n for (const sender of senders) {\n if (sender.track) {\n this.pc.removeTrack(sender);\n }\n }\n\n // Add new tracks.\n for (const track of stream.getTracks()) {\n this.pc.addTrack(track, stream);\n }\n this.localStream = stream;\n\n // Renegotiate.\n const offer = await this.pc.createOffer();\n await this.pc.setLocalDescription(offer);\n await this.gatherComplete();\n\n const local = this.pc.localDescription;\n if (!local) throw new Error(\"local description missing\");\n this.sig?.send({\n type: \"offer\",\n sdp: local.sdp,\n sdp_type: \"offer\",\n });\n }\n\n /** Stops publishing and tears down the peer connection. */\n stop(): void {\n this.sig?.close();\n this.sig = null;\n\n this.localStream?.getTracks().forEach((t) => t.stop());\n this.localStream = null;\n\n this.pc?.close();\n this.pc = null;\n this.hasAnswer = false;\n this.pendingRemoteCandidates = [];\n this.readToken = null;\n }\n\n /** Returns the current RTCPeerConnection, or null if not started. */\n get peerConnection(): RTCPeerConnection | null {\n return this.pc;\n }\n\n /** Returns true if the peer connection is in the \"connected\" state. */\n get isConnected(): boolean {\n return this.pc?.connectionState === \"connected\";\n }\n\n // -------------------------------------------------------------------------\n // Private helpers\n // -------------------------------------------------------------------------\n\n private raceGateways(): Promise<{ ws: WebSocket; readyInfo: GatewayReadyInfo }> {\n return new Promise((resolve, reject) => {\n const { gatewayURLs, token } = this.opts;\n if (gatewayURLs.length === 0) {\n reject(new Error(\"no gateway URLs provided\"));\n return;\n }\n\n const sockets: WebSocket[] = [];\n let settled = false;\n\n const closeAll = (except?: WebSocket) => {\n for (const s of sockets) {\n if (s !== except) {\n s.onmessage = null;\n s.onerror = null;\n s.onclose = null;\n s.close();\n }\n }\n };\n\n const checkAllFailed = () => {\n if (settled) return;\n if (sockets.every(s => s.readyState === WebSocket.CLOSED || s.readyState === WebSocket.CLOSING)) {\n settled = true;\n reject(new Error(\"all gateways failed to connect\"));\n }\n };\n\n for (const gatewayURL of gatewayURLs) {\n const u = new URL(gatewayURL);\n u.searchParams.set(\"token\", token);\n const ws = new WebSocket(u.toString());\n sockets.push(ws);\n\n let accepted = false;\n\n ws.onmessage = (ev: MessageEvent) => {\n if (settled) return;\n try {\n const msg = JSON.parse(ev.data as string);\n if (!accepted && msg.type === \"accepted\") {\n accepted = true;\n ws.send(JSON.stringify({ type: \"proceed\" }));\n } else if (accepted && msg.type === \"ready\") {\n settled = true;\n closeAll(ws);\n resolve({ ws, readyInfo: msg as GatewayReadyInfo });\n }\n } catch {\n /* ignore malformed */\n }\n };\n\n ws.onerror = () => checkAllFailed();\n ws.onclose = () => checkAllFailed();\n }\n });\n }\n\n private handleSignal(msg: SignalMessage): void {\n switch (msg.type) {\n case \"answer\": {\n if (!this.pc) return;\n this.pc\n .setRemoteDescription(\n new RTCSessionDescription({ type: \"answer\", sdp: msg.sdp }),\n )\n .then(() => {\n this.hasAnswer = true;\n // Flush any ICE candidates that arrived before the answer.\n for (const init of this.pendingRemoteCandidates) {\n this.pc?.addIceCandidate(init).catch(() => {\n /* ignore stale candidates */\n });\n }\n this.pendingRemoteCandidates = [];\n })\n .catch((err) => {\n this.opts.callbacks?.onError?.(\n new Error(`failed to set remote description: ${err}`),\n );\n });\n break;\n }\n\n case \"ice_candidate\": {\n if (!this.pc) return;\n const init: RTCIceCandidateInit = {\n candidate: msg.candidate,\n sdpMid: msg.sdp_mid ?? null,\n sdpMLineIndex: msg.sdp_mline_index ?? null,\n usernameFragment: msg.username_fragment ?? null,\n };\n if (this.hasAnswer) {\n this.pc.addIceCandidate(init).catch(() => {\n /* ignore stale candidates */\n });\n } else {\n this.pendingRemoteCandidates.push(init);\n }\n break;\n }\n\n case \"connection_state\": {\n // The server echoes the peer connection state; the onconnectionstatechange\n // handler above already covers this, but callers can also react via\n // onConnectionStateChange.\n break;\n }\n\n case \"error\": {\n this.opts.callbacks?.onError?.(new Error(msg.error));\n break;\n }\n }\n }\n\n /** Waits for ICE gathering to reach the \"complete\" state. */\n private gatherComplete(): Promise<void> {\n return new Promise((resolve) => {\n const pc = this.pc;\n if (!pc) {\n resolve();\n return;\n }\n if (pc.iceGatheringState === \"complete\") {\n resolve();\n return;\n }\n const handler = () => {\n if (pc.iceGatheringState === \"complete\") {\n pc.removeEventListener(\"icegatheringstatechange\", handler);\n resolve();\n }\n };\n pc.addEventListener(\"icegatheringstatechange\", handler);\n });\n }\n}\n","/**\n * Options for {@link captureCamera}.\n *\n * These are merged shallowly over the library defaults: any field you provide\n * replaces the default for that field entirely (e.g. passing `video` overrides\n * the default video constraints rather than merging into them). Omit a field to\n * keep its default.\n */\nexport interface CaptureCameraOptions {\n /**\n * Video constraints, or `true`/`false`. Defaults to a modest resolution and\n * frame rate (see {@link captureCamera}). Set `false` to disable video.\n */\n video?: MediaTrackConstraints | boolean;\n /**\n * Audio constraints, or `true`/`false`. Defaults to `false` — Argus is a\n * video-frame streaming system, so audio is off unless you ask for it.\n */\n audio?: MediaTrackConstraints | boolean;\n /**\n * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the browser's permission\n * prompt and transient-activation check are anchored to that window rather\n * than the one holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\n}\n\n/**\n * Options for {@link captureScreen}.\n *\n * These are merged shallowly over the library defaults: any field you provide\n * replaces the default for that field entirely. Omit a field to keep its\n * default.\n */\nexport interface CaptureScreenOptions {\n /**\n * Video constraints, or `true`. Defaults to a capped width and a low frame\n * rate (see {@link captureScreen}).\n */\n video?: MediaTrackConstraints | boolean;\n /**\n * Audio constraints, or `true`/`false`. Defaults to `false` — screen shares\n * rarely need audio for a video-frame streaming system.\n */\n audio?: MediaTrackConstraints | boolean;\n /**\n * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the screen picker and its\n * transient-activation check are anchored to that window rather than the one\n * holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\n}\n\n/**\n * Captures the user's camera via `navigator.mediaDevices.getUserMedia`,\n * applying sensible defaults for a video-frame streaming system.\n *\n * Defaults:\n * - `video`: `{ width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }`\n * — a modest resolution that keeps upload bandwidth reasonable. Cameras are\n * rarely the 4k bandwidth problem that screen capture is, so this is an\n * `ideal` (a hint) rather than a hard cap.\n * - `audio`: `false` — this is a video-frame streaming system.\n *\n * Any option you pass replaces the corresponding default outright (shallow\n * merge), so pass a full `video` constraints object if you want to tweak it.\n *\n * @example\n * ```ts\n * const stream = await captureCamera();\n * await publisher.start(stream);\n * ```\n *\n * @example Front camera with audio:\n * ```ts\n * const stream = await captureCamera({\n * video: { facingMode: \"user\" },\n * audio: true,\n * });\n * ```\n */\nexport async function captureCamera(\n opts: CaptureCameraOptions = {},\n): Promise<MediaStream> {\n const constraints: MediaStreamConstraints = {\n video: opts.video ?? {\n width: { ideal: 1280 },\n height: { ideal: 720 },\n frameRate: { ideal: 30 },\n },\n audio: opts.audio ?? false,\n };\n const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return mediaDevices.getUserMedia(constraints);\n}\n\n/**\n * Captures a screen / window / tab via\n * `navigator.mediaDevices.getDisplayMedia`, applying sensible defaults tuned to\n * avoid the HiDPI/Retina bandwidth trap.\n *\n * Defaults:\n * - `video`: `{ width: { max: 1920 }, frameRate: { ideal: 5, max: 10 } }`.\n * The `width` is a **`max`, not an `ideal`**: on a 2x-Retina display the\n * browser would otherwise capture at native resolution (often 3456px+ /\n * effectively 4k), wasting upload bandwidth and downstream decode cost for no\n * visible benefit. Capping the max roughly halves a 2x-Retina share while\n * leaving smaller displays untouched. Screen content is mostly static, so the\n * low frame rate saves further bandwidth.\n * - `audio`: `false`.\n *\n * IMPORTANT — do NOT add `resizeMode: \"none\"` here. That value forbids the\n * browser from downscaling the source, which turns the `width: { max: 1920 }`\n * cap into a no-op on exactly the Retina displays it targets. By omitting\n * `resizeMode` we let the user agent scale to satisfy the constraint (its\n * default behaviour), which is the entire point of this helper. It is tempting\n * to add `resizeMode: \"none\"` back for \"sharpness\" — don't.\n *\n * Any option you pass replaces the corresponding default outright (shallow\n * merge), so pass a full `video` constraints object if you want to tweak it.\n *\n * @example\n * ```ts\n * const stream = await captureScreen();\n * await publisher.start(stream);\n * ```\n */\nexport async function captureScreen(\n opts: CaptureScreenOptions = {},\n): Promise<MediaStream> {\n const constraints: MediaStreamConstraints = {\n video: opts.video ?? {\n width: { max: 1920 },\n frameRate: { ideal: 5, max: 10 },\n },\n audio: opts.audio ?? false,\n };\n const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return mediaDevices.getDisplayMedia(constraints);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/signaling.ts","../src/publisher.ts","../src/capture.ts"],"sourcesContent":["import type { SignalMessage } from \"./types\";\n\n/**\n * Wraps an open WebSocket to the Argus gateway and handles the JSON message\n * envelope: incoming text frames are parsed into {@link SignalMessage}s and\n * outgoing messages are serialised.\n *\n * This is an internal helper. The {@link Publisher} opens the socket itself\n * (racing all candidate gateways) and hands the winner here, so this class only\n * ever adopts an already-open socket rather than dialing one.\n *\n * @internal\n */\nexport class SignalingChannel {\n private ws: WebSocket;\n\n /** Fired for every incoming JSON message. */\n onMessage: ((msg: SignalMessage) => void) | null = null;\n /** Fired when the underlying WebSocket closes. */\n onClose: (() => void) | null = null;\n /** Fired when an error occurs on the WebSocket. */\n onError: ((err: Error) => void) | null = null;\n\n private constructor(ws: WebSocket) {\n this.ws = ws;\n }\n\n /**\n * Adopts an already-open WebSocket (e.g. the winner of a gateway race),\n * routing its events through the channel's callbacks. Any handlers previously\n * attached to the socket are replaced.\n */\n static wrap(ws: WebSocket): SignalingChannel {\n const ch = new SignalingChannel(ws);\n ws.onmessage = (ev: MessageEvent) => {\n const msg = parseSignal(ev.data);\n if (msg) ch.onMessage?.(msg);\n };\n ws.onerror = () => ch.onError?.(new Error(\"WebSocket error\"));\n ws.onclose = () => ch.onClose?.();\n return ch;\n }\n\n /** Sends a JSON message if the socket is open; a no-op otherwise. */\n send(msg: SignalMessage): void {\n if (this.ws.readyState === WebSocket.OPEN) {\n this.ws.send(JSON.stringify(msg));\n }\n }\n\n /** Closes the underlying WebSocket. */\n close(): void {\n this.ws.close();\n }\n}\n\n/** Parses a WebSocket text frame into a SignalMessage, or null if malformed. */\nfunction parseSignal(data: unknown): SignalMessage | null {\n try {\n return JSON.parse(data as string) as SignalMessage;\n } catch {\n return null;\n }\n}\n","import { SignalingChannel } from \"./signaling\";\nimport type { GatewayReadyInfo, PublisherOptions, SignalMessage } from \"./types\";\n\n/**\n * Publisher streams a browser {@link MediaStream} to an Argus media server over\n * WebRTC. Given the `gateway_urls` and `token` from a join-token response, it\n * races the candidate gateways to the fastest one, completes the two-phase\n * signaling handshake, and manages the peer connection — offer/answer exchange,\n * ICE candidate trickling, and track (re)negotiation.\n *\n * After {@link Publisher.start} resolves, {@link Publisher.frameReadToken} holds\n * the token your application server needs to fetch frames for this stream.\n *\n * @example Publish the default camera:\n * ```ts\n * const pub = new Publisher({\n * gatewayURLs: joinResp.gateway_urls,\n * token: joinResp.token,\n * callbacks: { onConnected: () => console.log(\"live!\") },\n * });\n *\n * const stream = await navigator.mediaDevices.getUserMedia({ video: true });\n * await pub.start(stream);\n * ```\n */\nexport class Publisher {\n private opts: PublisherOptions;\n private sig: SignalingChannel | null = null;\n private pc: RTCPeerConnection | null = null;\n private hasAnswer = false;\n private pendingRemoteCandidates: RTCIceCandidateInit[] = [];\n private localStream: MediaStream | null = null;\n private readToken: string | null = null;\n\n constructor(opts: PublisherOptions) {\n this.opts = opts;\n }\n\n /** The read token received from the gateway after connecting, for fetching frames. */\n get frameReadToken(): string | null { return this.readToken; }\n\n /**\n * Starts the publisher: races all gateways to find the fastest, completes\n * the two-phase handshake, creates the peer connection, and sends the SDP\n * offer. Resolves when the offer has been sent (not when ICE completes —\n * use onConnected for that).\n */\n async start(stream: MediaStream): Promise<void> {\n this.localStream = stream;\n\n // Race all gateways; returns winning WebSocket + TURN/read-token info\n const { ws, readyInfo } = await this.raceGateways();\n\n // Store read token for caller\n if (readyInfo.read_token) {\n this.readToken = readyInfo.read_token;\n }\n\n // Build ICE servers: extra servers from opts + TURN from gateway. The\n // gateway supplies multi-transport turn_urls (UDP + TCP) so relay can fall\n // back to TCP on UDP-blocking networks. A single credential is valid for\n // every transport.\n const iceServers: RTCIceServer[] = [...(this.opts.iceServers ?? [])];\n if (readyInfo.turn_urls && readyInfo.turn_urls.length > 0) {\n iceServers.push({\n urls: readyInfo.turn_urls,\n username: readyInfo.turn_username,\n credential: readyInfo.turn_credential,\n });\n }\n\n this.pc = new RTCPeerConnection({ iceServers });\n\n this.pc.onicecandidate = (ev) => {\n if (!ev.candidate || !this.sig) return;\n const c = ev.candidate;\n this.sig.send({\n type: \"ice_candidate\",\n candidate: c.candidate,\n sdp_mid: c.sdpMid ?? undefined,\n sdp_mline_index: c.sdpMLineIndex ?? undefined,\n username_fragment: c.usernameFragment ?? undefined,\n });\n };\n\n this.pc.onconnectionstatechange = () => {\n const state = this.pc?.connectionState;\n if (state) this.opts.callbacks?.onConnectionStateChange?.(state);\n if (state === \"connected\") this.opts.callbacks?.onConnected?.();\n };\n\n for (const track of stream.getTracks()) {\n this.pc.addTrack(track, stream);\n }\n\n const offer = await this.pc.createOffer();\n await this.pc.setLocalDescription(offer);\n await this.gatherComplete();\n\n // Wrap winning WebSocket in SignalingChannel\n this.sig = SignalingChannel.wrap(ws);\n this.sig.onMessage = (msg) => this.handleSignal(msg);\n this.sig.onClose = () => this.opts.callbacks?.onError?.(new Error(\"signaling closed\"));\n this.sig.onError = (err) => this.opts.callbacks?.onError?.(err);\n\n const local = this.pc.localDescription;\n if (!local) throw new Error(\"local description missing after gather\");\n this.sig.send({ type: \"offer\", sdp: local.sdp, sdp_type: \"offer\" });\n }\n\n /** Replaces the currently published stream with a new one. */\n async replaceStream(stream: MediaStream): Promise<void> {\n if (!this.pc) throw new Error(\"publisher not started\");\n\n // Remove old tracks.\n const senders = this.pc.getSenders();\n for (const sender of senders) {\n if (sender.track) {\n this.pc.removeTrack(sender);\n }\n }\n\n // Add new tracks.\n for (const track of stream.getTracks()) {\n this.pc.addTrack(track, stream);\n }\n this.localStream = stream;\n\n // Renegotiate.\n const offer = await this.pc.createOffer();\n await this.pc.setLocalDescription(offer);\n await this.gatherComplete();\n\n const local = this.pc.localDescription;\n if (!local) throw new Error(\"local description missing\");\n this.sig?.send({\n type: \"offer\",\n sdp: local.sdp,\n sdp_type: \"offer\",\n });\n }\n\n /** Stops publishing and tears down the peer connection. */\n stop(): void {\n this.sig?.close();\n this.sig = null;\n\n this.localStream?.getTracks().forEach((t) => t.stop());\n this.localStream = null;\n\n this.pc?.close();\n this.pc = null;\n this.hasAnswer = false;\n this.pendingRemoteCandidates = [];\n this.readToken = null;\n }\n\n /** Returns the current RTCPeerConnection, or null if not started. */\n get peerConnection(): RTCPeerConnection | null {\n return this.pc;\n }\n\n /** Returns true if the peer connection is in the \"connected\" state. */\n get isConnected(): boolean {\n return this.pc?.connectionState === \"connected\";\n }\n\n // -------------------------------------------------------------------------\n // Private helpers\n // -------------------------------------------------------------------------\n\n private raceGateways(): Promise<{ ws: WebSocket; readyInfo: GatewayReadyInfo }> {\n return new Promise((resolve, reject) => {\n const { gatewayURLs, token } = this.opts;\n if (gatewayURLs.length === 0) {\n reject(new Error(\"no gateway URLs provided\"));\n return;\n }\n\n const sockets: WebSocket[] = [];\n let settled = false;\n\n const closeAll = (except?: WebSocket) => {\n for (const s of sockets) {\n if (s !== except) {\n s.onmessage = null;\n s.onerror = null;\n s.onclose = null;\n s.close();\n }\n }\n };\n\n const checkAllFailed = () => {\n if (settled) return;\n if (sockets.every(s => s.readyState === WebSocket.CLOSED || s.readyState === WebSocket.CLOSING)) {\n settled = true;\n reject(new Error(\"all gateways failed to connect\"));\n }\n };\n\n for (const gatewayURL of gatewayURLs) {\n const u = new URL(gatewayURL);\n u.searchParams.set(\"token\", token);\n const ws = new WebSocket(u.toString());\n sockets.push(ws);\n\n let accepted = false;\n\n ws.onmessage = (ev: MessageEvent) => {\n if (settled) return;\n try {\n const msg = JSON.parse(ev.data as string);\n if (!accepted && msg.type === \"accepted\") {\n accepted = true;\n ws.send(JSON.stringify({ type: \"proceed\" }));\n } else if (accepted && msg.type === \"ready\") {\n settled = true;\n closeAll(ws);\n resolve({ ws, readyInfo: msg as GatewayReadyInfo });\n }\n } catch {\n /* ignore malformed */\n }\n };\n\n ws.onerror = () => checkAllFailed();\n ws.onclose = () => checkAllFailed();\n }\n });\n }\n\n private handleSignal(msg: SignalMessage): void {\n switch (msg.type) {\n case \"answer\": {\n if (!this.pc) return;\n this.pc\n .setRemoteDescription(\n new RTCSessionDescription({ type: \"answer\", sdp: msg.sdp }),\n )\n .then(() => {\n this.hasAnswer = true;\n // Flush any ICE candidates that arrived before the answer.\n for (const init of this.pendingRemoteCandidates) {\n this.pc?.addIceCandidate(init).catch(() => {\n /* ignore stale candidates */\n });\n }\n this.pendingRemoteCandidates = [];\n })\n .catch((err) => {\n this.opts.callbacks?.onError?.(\n new Error(`failed to set remote description: ${err}`),\n );\n });\n break;\n }\n\n case \"ice_candidate\": {\n if (!this.pc) return;\n const init: RTCIceCandidateInit = {\n candidate: msg.candidate,\n sdpMid: msg.sdp_mid ?? null,\n sdpMLineIndex: msg.sdp_mline_index ?? null,\n usernameFragment: msg.username_fragment ?? null,\n };\n if (this.hasAnswer) {\n this.pc.addIceCandidate(init).catch(() => {\n /* ignore stale candidates */\n });\n } else {\n this.pendingRemoteCandidates.push(init);\n }\n break;\n }\n\n case \"connection_state\": {\n // The server echoes the peer connection state; the onconnectionstatechange\n // handler above already covers this, but callers can also react via\n // onConnectionStateChange.\n break;\n }\n\n case \"error\": {\n this.opts.callbacks?.onError?.(new Error(msg.error));\n break;\n }\n }\n }\n\n /** Waits for ICE gathering to reach the \"complete\" state. */\n private gatherComplete(): Promise<void> {\n return new Promise((resolve) => {\n const pc = this.pc;\n if (!pc) {\n resolve();\n return;\n }\n if (pc.iceGatheringState === \"complete\") {\n resolve();\n return;\n }\n const handler = () => {\n if (pc.iceGatheringState === \"complete\") {\n pc.removeEventListener(\"icegatheringstatechange\", handler);\n resolve();\n }\n };\n pc.addEventListener(\"icegatheringstatechange\", handler);\n });\n }\n}\n","/**\n * Options for {@link captureCamera}.\n *\n * These are merged shallowly over the library defaults: any field you provide\n * replaces the default for that field entirely (e.g. passing `video` overrides\n * the default video constraints rather than merging into them). Omit a field to\n * keep its default.\n */\nexport interface CaptureCameraOptions {\n /**\n * Video constraints, or `true`/`false`. Defaults to a modest resolution and\n * frame rate (see {@link captureCamera}). Set `false` to disable video.\n */\n video?: MediaTrackConstraints | boolean;\n /**\n * Audio constraints, or `true`/`false`. Defaults to `false` — Argus is a\n * video-frame streaming system, so audio is off unless you ask for it.\n */\n audio?: MediaTrackConstraints | boolean;\n /**\n * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the browser's permission\n * prompt and transient-activation check are anchored to that window rather\n * than the one holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\n}\n\n/**\n * Options for {@link captureScreen}.\n *\n * These are merged shallowly over the library defaults: any field you provide\n * replaces the default for that field entirely. Omit a field to keep its\n * default.\n */\nexport interface CaptureScreenOptions {\n /**\n * Video constraints, or `true`. Defaults to a capped width and a low frame\n * rate (see {@link captureScreen}).\n */\n video?: MediaTrackConstraints | boolean;\n /**\n * Audio constraints, or `true`/`false`. Defaults to `false` — screen shares\n * rarely need audio for a video-frame streaming system.\n */\n audio?: MediaTrackConstraints | boolean;\n /**\n * The `MediaDevices` instance to capture from. Defaults to the global\n * `navigator.mediaDevices`. Pass another window's `navigator.mediaDevices`\n * (e.g. a Document Picture-in-Picture window's) so the screen picker and its\n * transient-activation check are anchored to that window rather than the one\n * holding the global `navigator`.\n */\n mediaDevices?: MediaDevices;\n}\n\n/**\n * Captures the user's camera via `navigator.mediaDevices.getUserMedia`,\n * applying sensible defaults for a video-frame streaming system.\n *\n * Defaults:\n * - `video`: `{ width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30 } }`\n * — a modest resolution that keeps upload bandwidth reasonable. Cameras are\n * rarely the 4k bandwidth problem that screen capture is, so this is an\n * `ideal` (a hint) rather than a hard cap.\n * - `audio`: `false` — this is a video-frame streaming system.\n *\n * Any option you pass replaces the corresponding default outright (shallow\n * merge), so pass a full `video` constraints object if you want to tweak it.\n *\n * @example\n * ```ts\n * const stream = await captureCamera();\n * await publisher.start(stream);\n * ```\n *\n * @example Front camera with audio:\n * ```ts\n * const stream = await captureCamera({\n * video: { facingMode: \"user\" },\n * audio: true,\n * });\n * ```\n */\nexport async function captureCamera(\n opts: CaptureCameraOptions = {},\n): Promise<MediaStream> {\n const constraints: MediaStreamConstraints = {\n video: opts.video ?? {\n width: { ideal: 1280 },\n height: { ideal: 720 },\n frameRate: { ideal: 30 },\n },\n audio: opts.audio ?? false,\n };\n const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return mediaDevices.getUserMedia(constraints);\n}\n\n/**\n * Captures a screen / window / tab via\n * `navigator.mediaDevices.getDisplayMedia`, applying sensible defaults tuned to\n * avoid the HiDPI/Retina bandwidth trap.\n *\n * Defaults:\n * - `video`: `{ width: { max: 1920 }, frameRate: { ideal: 5, max: 10 } }`.\n * The `width` is a **`max`, not an `ideal`**: on a 2x-Retina display the\n * browser would otherwise capture at native resolution (often 3456px+ /\n * effectively 4k), wasting upload bandwidth and downstream decode cost for no\n * visible benefit. Capping the max roughly halves a 2x-Retina share while\n * leaving smaller displays untouched. Screen content is mostly static, so the\n * low frame rate saves further bandwidth.\n * - `audio`: `false`.\n *\n * IMPORTANT — do NOT add `resizeMode: \"none\"` here. That value forbids the\n * browser from downscaling the source, which turns the `width: { max: 1920 }`\n * cap into a no-op on exactly the Retina displays it targets. By omitting\n * `resizeMode` we let the user agent scale to satisfy the constraint (its\n * default behaviour), which is the entire point of this helper. It is tempting\n * to add `resizeMode: \"none\"` back for \"sharpness\" — don't.\n *\n * Any option you pass replaces the corresponding default outright (shallow\n * merge), so pass a full `video` constraints object if you want to tweak it.\n *\n * @example\n * ```ts\n * const stream = await captureScreen();\n * await publisher.start(stream);\n * ```\n */\nexport async function captureScreen(\n opts: CaptureScreenOptions = {},\n): Promise<MediaStream> {\n const constraints: MediaStreamConstraints = {\n video: opts.video ?? {\n width: { max: 1920 },\n frameRate: { ideal: 5, max: 10 },\n },\n audio: opts.audio ?? false,\n };\n const mediaDevices = opts.mediaDevices ?? navigator.mediaDevices;\n return mediaDevices.getDisplayMedia(constraints);\n}\n"],"mappings":"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","names":[]}
|