From 2a7de26996f999f0d6ca046db1f0287fa15a7890 Mon Sep 17 00:00:00 2001 From: golgetahir Date: Mon, 16 Feb 2026 17:41:14 +0300 Subject: [PATCH 1/2] Implement auto managed compatibility with sdk v2 --- packages/webrtc-sdk/src/client/base-client.ts | 2 + packages/webrtc-sdk/src/core/events.ts | 8 + packages/webrtc-sdk/src/core/types.ts | 6 +- packages/webrtc-sdk/src/core/webrtc-client.ts | 2 + .../webrtc-sdk/src/core/websocket-adaptor.ts | 154 +++++++++++++++++- 5 files changed, 165 insertions(+), 7 deletions(-) diff --git a/packages/webrtc-sdk/src/client/base-client.ts b/packages/webrtc-sdk/src/client/base-client.ts index e1591bf3..5f427ff5 100644 --- a/packages/webrtc-sdk/src/client/base-client.ts +++ b/packages/webrtc-sdk/src/client/base-client.ts @@ -141,6 +141,8 @@ export abstract class BaseClient extends Emitter { this.ws = new WebSocketAdaptor({ websocketURL: opts.websocketURL, httpEndpointUrl: opts.httpEndpointUrl, + httpEndpointAccessToken: opts.httpEndpointAccessToken, + endpointTimeoutMs: opts.endpointTimeoutMs, webrtcadaptor: { notifyEventListeners: (info: string, obj?: unknown) => this.handleTransportEvent(info, obj), diff --git a/packages/webrtc-sdk/src/core/events.ts b/packages/webrtc-sdk/src/core/events.ts index 956dab5c..bf95a0df 100644 --- a/packages/webrtc-sdk/src/core/events.ts +++ b/packages/webrtc-sdk/src/core/events.ts @@ -9,6 +9,14 @@ export interface EventMap { initialized: void; closed: unknown; server_will_stop: unknown; + /** Emitted when auto-managed endpoint resolution begins (Lambda call) */ + endpoint_resolving: void; + /** Emitted when the Lambda returns instance info successfully */ + endpoint_resolved: { websocketUrl: string; httpUrl: string }; + /** Emitted while polling for the auto-managed instance to become ready */ + instance_waiting: { attempt: number; elapsedMs: number }; + /** Emitted when endpoint resolution or instance readiness times out */ + endpoint_timeout: { elapsedMs: number; maxMs: number }; /** Emitted when new local tracks are attached or replaced; used internally to refresh senders */ publish_started: { streamId: string }; publish_finished: { streamId: string }; diff --git a/packages/webrtc-sdk/src/core/types.ts b/packages/webrtc-sdk/src/core/types.ts index a1b4815b..3db05d36 100644 --- a/packages/webrtc-sdk/src/core/types.ts +++ b/packages/webrtc-sdk/src/core/types.ts @@ -11,8 +11,12 @@ export type Role = "publisher" | "viewer"; export interface BaseClientOptions { /** WebSocket signaling URL (e.g. wss://host:5443/App/websocket) */ websocketURL?: string; - /** HTTP REST endpoint of Ant Media (used as fallback by signaling layer) */ + /** HTTP endpoint URL (e.g. Lambda URL) that returns the websocket URL for auto-managed instances */ httpEndpointUrl?: string; + /** Access token sent as a query parameter when resolving the HTTP endpoint */ + httpEndpointAccessToken?: string; + /** Maximum time (ms) to wait for auto-managed endpoint resolution and instance readiness (default: 120000) */ + endpointTimeoutMs?: number; /** If true, initializes in play-only mode and skips getUserMedia */ isPlayMode?: boolean; /** If true, creates data-only sessions without capturing audio/video locally */ diff --git a/packages/webrtc-sdk/src/core/webrtc-client.ts b/packages/webrtc-sdk/src/core/webrtc-client.ts index 208d6795..69ddc530 100644 --- a/packages/webrtc-sdk/src/core/webrtc-client.ts +++ b/packages/webrtc-sdk/src/core/webrtc-client.ts @@ -172,6 +172,8 @@ export class WebRTCClient extends Emitter { this.ws = new WebSocketAdaptor({ websocketURL: opts.websocketURL, httpEndpointUrl: opts.httpEndpointUrl, + httpEndpointAccessToken: opts.httpEndpointAccessToken, + endpointTimeoutMs: opts.endpointTimeoutMs, webrtcadaptor: { notifyEventListeners: (info: string, obj?: unknown) => this.notify(info as keyof EventMap, obj as never), diff --git a/packages/webrtc-sdk/src/core/websocket-adaptor.ts b/packages/webrtc-sdk/src/core/websocket-adaptor.ts index c84ac4d3..2ed22d07 100644 --- a/packages/webrtc-sdk/src/core/websocket-adaptor.ts +++ b/packages/webrtc-sdk/src/core/websocket-adaptor.ts @@ -18,14 +18,34 @@ export interface IWebSocketAdaptor { */ export interface WebSocketAdaptorOptions { websocketURL?: string; + /** HTTP endpoint URL (e.g. Lambda URL) that returns the websocket URL for auto-managed instances */ httpEndpointUrl?: string; + /** Access token sent as a query parameter when resolving the HTTP endpoint */ + httpEndpointAccessToken?: string; + /** Maximum time (ms) to wait for endpoint resolution + instance readiness (default: 120000) */ + endpointTimeoutMs?: number; + /** Interval (ms) between retry attempts when polling the endpoint or instance (default: 3000) */ + endpointRetryMs?: number; webrtcadaptor: { notifyEventListeners: (info: string, obj?: unknown) => void }; debug?: boolean | LogLevel; } +/** Response shape returned by the auto-managed HTTP endpoint (Lambda). */ +interface EndpointResponse { + fqdn?: string; + websocket_url: string; + http_url: string; +} + /** * Thin wrapper around WebSocket that adapts Ant Media's signaling protocol * and emits typed events to the adaptor. + * + * Supports two connection modes: + * 1. **Direct WebSocket** — when `websocketURL` is provided, connects immediately. + * 2. **Auto-managed endpoint** — when `httpEndpointUrl` is provided, resolves the + * actual WebSocket URL via the HTTP endpoint (e.g. AWS Lambda), polls until the + * instance is ready, then connects. */ export class WebSocketAdaptor extends Emitter implements IWebSocketAdaptor { private ws?: WebSocket; @@ -34,6 +54,7 @@ export class WebSocketAdaptor extends Emitter implements IWebSocketAda private opts: WebSocketAdaptorOptions; private log: Logger; private pingTimer: ReturnType | null = null; + private endpointAborted = false; /** * Create a new WebSocket adaptor. @@ -44,11 +65,127 @@ export class WebSocketAdaptor extends Emitter implements IWebSocketAda this.log = new Logger( typeof opts.debug === "string" ? opts.debug : opts.debug ? "debug" : "info" ); - if (opts.websocketURL || opts.httpEndpointUrl) { - this.init(); + if (opts.websocketURL) { + this.initWebSocket(opts.websocketURL); + } else if (opts.httpEndpointUrl) { + void this.resolveEndpointAndConnect(); + } + } + + /** + * Resolves the actual WebSocket URL from the HTTP endpoint (Lambda) and waits + * for the backend instance to become ready before opening the WebSocket. + */ + private async resolveEndpointAndConnect(): Promise { + const maxWaitMs = this.opts.endpointTimeoutMs ?? 120_000; + const retryMs = this.opts.endpointRetryMs ?? 3000; + const startedAt = Date.now(); + let attempt = 0; + + this.connecting = true; + this.connected = false; + + const endpointUrl = this.buildEndpointUrl(); + + while (!this.endpointAborted) { + const elapsedMs = Date.now() - startedAt; + if (elapsedMs >= maxWaitMs) { + this.connecting = false; + this.log.error("endpoint resolution timed out after %dms", elapsedMs); + this.opts.webrtcadaptor.notifyEventListeners("endpoint_timeout", { + elapsedMs, + maxMs: maxWaitMs, + }); + this.opts.webrtcadaptor.notifyEventListeners("error", { + error: "EndpointTimeout", + message: `Auto-managed endpoint did not become ready within ${maxWaitMs}ms`, + }); + return; + } + + attempt++; + + // Phase 1: Resolve endpoint + let data: EndpointResponse | null = null; + try { + this.opts.webrtcadaptor.notifyEventListeners("endpoint_resolving"); + this.log.info("resolving endpoint (attempt %d): %s", attempt, endpointUrl); + const response = await fetch(endpointUrl, { method: "GET" }); + if (!response.ok) { + throw new Error(`HTTP ${response.status} ${response.statusText}`); + } + data = (await response.json()) as EndpointResponse; + this.log.info( + "endpoint resolved -> fqdn: %s, websocket_url: %s, http_url: %s", + data.fqdn ?? "N/A", + data.websocket_url, + data.http_url + ); + this.opts.webrtcadaptor.notifyEventListeners("endpoint_resolved", { + websocketUrl: data.websocket_url, + httpUrl: data.http_url, + }); + } catch (e) { + this.log.warn( + "endpoint resolution failed (attempt %d), retrying in %dms", + attempt, + retryMs + ); + this.log.debug("endpoint error:", e); + this.opts.webrtcadaptor.notifyEventListeners("instance_waiting", { + attempt, + elapsedMs: Date.now() - startedAt, + }); + await this.delay(retryMs); + continue; + } + + // Phase 2: Wait for instance readiness + if (this.endpointAborted) return; + try { + this.opts.webrtcadaptor.notifyEventListeners("instance_waiting", { + attempt, + elapsedMs: Date.now() - startedAt, + }); + this.log.info("checking instance readiness: %s", data.http_url); + const healthResp = await fetch(data.http_url, { method: "HEAD" }); + if (healthResp.status >= 200 && healthResp.status < 400) { + this.log.info("instance is ready, opening WebSocket"); + this.initWebSocket(data.websocket_url); + return; + } + this.log.warn("instance not ready (HTTP %d), retrying in %dms", healthResp.status, retryMs); + } catch (e) { + this.log.warn( + "instance health check failed (attempt %d), retrying in %dms", + attempt, + retryMs + ); + this.log.debug("health check error:", e); + } + + await this.delay(retryMs); + } + } + + /** + * Builds the endpoint URL with query parameters (source=sdk, optional accessToken). + */ + private buildEndpointUrl(): string { + const url = new URL(this.opts.httpEndpointUrl!); + url.searchParams.set("source", "sdk"); + if (this.opts.httpEndpointAccessToken) { + url.searchParams.set("accessToken", this.opts.httpEndpointAccessToken); } + return url.toString(); + } + + private delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); } + // ─── WebSocket connection ──────────────────────────────────────────── + private startPing(): void { this.clearPing(); this.pingTimer = setInterval(() => { @@ -63,12 +200,14 @@ export class WebSocketAdaptor extends Emitter implements IWebSocketAda } } - private init(): void { - if (!this.opts.websocketURL) return; + /** + * Opens a WebSocket connection to the given URL and wires up event handlers. + */ + private initWebSocket(websocketURL: string): void { this.connecting = true; this.connected = false; - this.log.info("connecting to websocket %s", this.opts.websocketURL); - const ws = new WebSocket(this.opts.websocketURL); + this.log.info("connecting to websocket %s", websocketURL); + const ws = new WebSocket(websocketURL); this.ws = ws; ws.onopen = () => { @@ -111,6 +250,8 @@ export class WebSocketAdaptor extends Emitter implements IWebSocketAda }; } + // ─── Public API ────────────────────────────────────────────────────── + isConnected(): boolean { return this.connected; } @@ -136,6 +277,7 @@ export class WebSocketAdaptor extends Emitter implements IWebSocketAda } close(): void { + this.endpointAborted = true; this.clearPing(); this.ws?.close(); } From f8f24eacb0328945896a2decf0ad54bde6fc1ac4 Mon Sep 17 00:00:00 2001 From: golgetahir Date: Wed, 18 Feb 2026 13:04:06 +0300 Subject: [PATCH 2/2] Add conference auto managed example --- .../examples/conference-v2-auto-managed.html | 811 ++++++++++++++++++ packages/webrtc-sdk/src/client/base-client.ts | 1 + 2 files changed, 812 insertions(+) create mode 100644 packages/webrtc-sdk/examples/conference-v2-auto-managed.html diff --git a/packages/webrtc-sdk/examples/conference-v2-auto-managed.html b/packages/webrtc-sdk/examples/conference-v2-auto-managed.html new file mode 100644 index 00000000..ef681913 --- /dev/null +++ b/packages/webrtc-sdk/examples/conference-v2-auto-managed.html @@ -0,0 +1,811 @@ + + + + + + Ant Media WebRTC Conference – Auto-Managed (SDK v2) + + + + + + + + +
+
+
+

WebRTC Multitrack Conference – Auto-Managed (SDK v2)

+ +
+
+ +
+ + +
+ +
+
+ + +
+
+ +
+
+
+ +
+
+ +
+
+ + + +
+ + + +
+ +
+
+
+ + + + +
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+ + +
+ + + + diff --git a/packages/webrtc-sdk/src/client/base-client.ts b/packages/webrtc-sdk/src/client/base-client.ts index 5f427ff5..129da565 100644 --- a/packages/webrtc-sdk/src/client/base-client.ts +++ b/packages/webrtc-sdk/src/client/base-client.ts @@ -898,6 +898,7 @@ export abstract class BaseClient extends Emitter { } this.onInitialized(); + this.emit("initialized", undefined as never); return; } else if (info === "start") { const { streamId } = obj as { streamId: string };