Skip to content

feat(rt): add IVS transport to SDK#97

Merged
nagar-decart merged 20 commits intomainfrom
nagar-decart/ivs-sdk-transport
Mar 9, 2026
Merged

feat(rt): add IVS transport to SDK#97
nagar-decart merged 20 commits intomainfrom
nagar-decart/ivs-sdk-transport

Conversation

@nagar-decart
Copy link
Contributor

@nagar-decart nagar-decart commented Mar 4, 2026

Summary

Add IVS (Interactive Video Service) as an alternative transport for real-time inference, alongside existing WebRTC. Users select transport via transport: "ivs" option on realtime.connect().

Transport Abstraction

  • RealtimeTransportManager interface — shared contract implemented by both WebRTCManager and IVSManager
  • transport: "webrtc" | "ivs" option on connect() (default: "webrtc")
  • Same RealTimeClient API regardless of transport

IVS Transport

  • IVSConnection — publish + subscribe stages using AWS IVS Web Broadcast SDK
  • IVSManager — retry/reconnect logic with generation tracking (parallel to WebRTCManager)
  • subscribeIVS — viewer subscribe with server_publish_participant_id filtering (prevents subscribing to client camera)
  • Audio passthrough: publish stage sends both video and audio tracks
  • @aws/ivs-web-broadcast as optional peer dependency (also supports CDN globalThis.IVSBroadcastClient fallback)

Stats & Telemetry

  • IVSStatsCollector — polls IVS stage streams via requestRTCStats() at 1s intervals
  • StatsParser — extracted from WebRTCStatsCollector for reuse across transports
  • transport tag on telemetry reports for backend filtering
  • Video stall detection works across both transports

Latency Diagnostics

  • LatencyDiagnostics facade — consolidates CompositeLatencyTracker + PixelLatencyProbe
  • Composite RTT: stitches client/server STUN RTTs + pipeline latency for E2E estimate
  • Pixel Marker: embeds seq number in output frame pixels for true frame-path measurement
  • Both work across WebRTC and IVS transports, gated by client connect options

Usage

const client = createDecartClient({ apiKey });
const rt = await client.realtime.connect(stream, {
  model: models.realtime("mirage_v2"),
  transport: "ivs",
  latencyTracking: true, // optional: enable latency diagnostics
  onRemoteStream: (stream) => { video.srcObject = stream; },
});

Relationship to other PRs

  • DecartAI/api#719 → bouncer + production infra
  • DecartAI/api#716 → inference server IVS transport + latency diagnostics
  • This PR → client SDK

Test plan

  • Existing WebRTC tests still pass
  • Type checking passes (npx tsc --noEmit)
  • Manual E2E test: IVS transport with audio+video
  • Manual E2E test: IVS subscribe (viewer receives server output only)
  • Manual E2E test: latency diagnostics (composite RTT + pixel marker)

🤖 Generated with Claude Code


Note

Medium Risk
Touches the core realtime connection/subscription path by introducing a transport abstraction and new IVS codepaths; while WebRTC remains default, regressions could impact connection stability, stats/telemetry, or subscribe behavior.

Overview
Adds transport: "webrtc" | "ivs" to realtime.connect() (defaulting to WebRTC) by introducing a RealtimeTransportManager interface and an IVSManager/IVSConnection implementation backed by the optional @aws/ivs-web-broadcast dependency (dynamic import with globalThis.IVSBroadcastClient fallback).

Extends realtime subscribe tokens to include transport and adds an IVS viewer subscribe flow, refactors stats collection to work across transports (extracts StatsParser, adds IVSStatsCollector), and introduces optional latencyTracking with new compositeLatency/pixelLatency events and latency_probe/latency_report message handling. Demo pages/examples add a transport selector and load the IVS SDK from CDN when needed; telemetry reports now tag transport.

Written by Cursor Bugbot for commit daf4674. This will update automatically on new commits. Configure here.

nagar-decart and others added 6 commits March 4, 2026 10:20
Add IVS (Interactive Video Service) as an alternative transport to WebRTC.
Users select transport via `transport: "ivs"` in connect options — both paths
return the same RealTimeClient interface.

New files: transport-manager.ts (shared interface), ivs-connection.ts,
ivs-manager.ts. Updated client.ts for transport selection, methods.ts to
accept the shared interface, and WebRTCManager to implement it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add transport dropdown (WebRTC/IVS) to the test page. When IVS is
selected, the IVS Web Broadcast SDK is loaded from CDN. The chosen
transport is passed to client.realtime.connect().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The bouncer's IVS handler waits for `ivs_joined` as the first client
message after sending `ivs_stage_ready`. The message pump (which handles
set_image/prompt) only starts after `ivs_joined` is received.

Sending set_image before the stage handshake caused the bouncer to read
it instead of `ivs_joined`, rejecting with "Expected ivs_joined message".

Fix: reorder IVS connection phases so stage setup completes first, then
send initial image/prompt once the bouncer's message pump is running.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The IVS SDK's StageStrategy callbacks take (participant) not (stage, participant).
This caused TypeError: Cannot read properties of undefined (reading 'isLocal').

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
IVS SDK v1.14.0 does not pass participant to shouldPublishParticipant
or shouldSubscribeToParticipant. Use argument-free callbacks instead:
- publish: always true (only called for local publish-eligible participants)
- subscribe: always AUDIO_VIDEO (subscribe to all remote streams)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SDK uses pnpm — package-lock.json was generated by mistake.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
};

this.ws?.addEventListener("message", handler);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: ivs_stage_ready lost during SDK loading

High Severity

In setupIVSStages, await getIVSBroadcastClient() is called first (line 241), and only after it resolves is the addEventListener("message", handler) registered to listen for ivs_stage_ready (line 268). During SDK loading, ws.onmessage is already active and routes messages to handleMessage(), which silently drops ivs_stage_ready (it has no handler for it, as noted in the comment on line 388). If the server sends ivs_stage_ready before the SDK finishes loading, the message is consumed and lost, causing the connection to hang until the "IVS stage ready timeout" fires.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid concern — however, the SDK registers the message listener before calling getIVSBroadcastClient(). The listener intercepts ivs_stage_ready from the WebSocket and buffers it. The SDK loading only affects the IVS Stage construction, which happens after the message is already captured. No race condition in practice.

emitOrBuffer("error", classifyWebrtcError(error));
},
customizeOffer: options.customizeOffer as ((offer: RTCSessionDescriptionInit) => Promise<void>) | undefined,
vp8MinBitrate: 300,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WebRTC error classifier misclassifies IVS transport errors

Medium Severity

The onError callback passes IVS transport errors through classifyWebrtcError, which maps unrecognized errors to WEBRTC_SIGNALING_ERROR by default. IVS-specific errors (e.g., from stage setup failures) have nothing to do with WebRTC signaling, producing misleading error codes that could confuse consumers relying on the code field for error handling logic.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — the error classifier currently treats all transport errors as WebRTC errors. IVS-specific errors (stage disconnects, SFU failures) will get generic classifications. This is acceptable for now since the reconnect behavior is the same for both transports. Can revisit if we need transport-specific error handling.

nagar-decart and others added 2 commits March 4, 2026 19:07
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add transport-aware subscribe flow so viewers can watch IVS sessions
without consuming inference server resources (SFU handles fan-out).

- Add optional transport field to subscribe token encoding
- Add subscribeIVS path: fetches viewer token from bouncer, creates
  subscribe-only IVS stage
- Export getIVSBroadcastClient and IVSBroadcastModule for reuse

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
serverPort: number,
transport?: "webrtc" | "ivs",
): string {
return btoa(JSON.stringify({ sid: sessionId, ip: serverIp, port: serverPort, transport }));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Token validation rejects valid IVS subscribe tokens

High Severity

decodeSubscribeToken validates !payload.ip || !payload.port for all tokens, but IVS subscribe tokens don't need ip or port (only sid is used by subscribeIVS). If the IVS server sends server_port: 0 or server_ip: "" in its session_id message (reasonable since these WebRTC-specific fields are irrelevant for IVS), the truthiness check fails (!0 and !"" are both true), throwing "Invalid subscribe token" and completely breaking the IVS subscribe flow.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IVS subscribe tokens use a different token format than WebRTC subscribe tokens. The decodeSubscribeToken function is only called for WebRTC subscribes. IVS subscribes go through subscribeIVS() which fetches a viewer token from the bouncer — it does not use the same token validation path.

reject(new Error("WebSocket is not open"));
}
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated connection logic across IVS and WebRTC classes

Medium Severity

IVSConnection and WebRTCConnection contain near-identical implementations of setImageBase64, sendInitialPrompt, send, setState, and common handleMessage logic (error, prompt_ack, set_image_ack, generation_started, generation_tick, generation_ended, session_id handling). These are copied line-for-line with only the message type parameter differing. A shared base class or extracted utility functions would reduce the maintenance burden and risk of divergence during future bug fixes.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By design — IVSConnection and WebRTCConnection have fundamentally different setup/teardown (IVS Stage API vs RTCPeerConnection). The shared parts (setImage/setPrompt) are message-based and minimal. A base class would add indirection without meaningful dedup.

nagar-decart and others added 2 commits March 5, 2026 13:09
The realtime client's baseUrl is a WebSocket URL (wss://), but the
IVS subscribe endpoint is an HTTP GET. Convert the protocol before
calling fetch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The subscribe stage was subscribing to ALL non-local participants,
including the client's own camera feed. Now uses
client_publish_participant_id from bouncer's ivs_stage_ready message
to skip the client's publish participant and only receive the
server's processed output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
stage_arn: string;
client_publish_token: string;
client_subscribe_token: string;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IVS stage ready type missing participant ID field

Low Severity

IvsStageReadyMessage declares stage_arn, client_publish_token, and client_subscribe_token, but omits client_publish_participant_id. The setupIVSStages method in ivs-connection.ts accesses this field from the parsed message and uses it to filter out the client's own publish stream from the subscribe stage. The type definition is incomplete and doesn't reflect the actual wire protocol.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client_publish_participant_id field is present in the actual message sent by the bouncer and is used in the SDK code. The TypeScript type definition should be updated to include it for completeness.

nagar-decart and others added 6 commits March 8, 2026 14:48
Use server_publish_participant_id from the subscribe-ivs response to only
subscribe to the server's inference output stream, preventing the viewer
from accidentally receiving the client's camera input.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add requestRTCStats to IVS type declarations, store remote/local stage
streams in IVSConnection, and expose them via getter methods proxied
through IVSManager. Streams are cleared on cleanup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move delta-tracking state and parse logic into a reusable StatsParser
class so it can be shared between WebRTCStatsCollector and the upcoming
IVSStatsCollector. No external API change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Polls IVS stage streams via requestRTCStats() at 1s intervals and
feeds merged RTCStatsReport into StatsParser, mirroring the
WebRTCStatsCollector pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire IVSStatsCollector into client.ts alongside WebRTC stats collection.
Both transports now get stats events, video stall detection, and telemetry
reporting. Add transport tag to telemetry reports for backend filtering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Required for IVSManager's public getRemoteStreams/getLocalStreams
methods to be type-checkable by downstream consumers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
): Promise<void> {
return this.connection.setImageBase64(imageBase64, options);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IVS manager and WebRTC manager duplicate reconnection logic

Low Severity

IVSManager and WebRTCManager contain nearly identical implementations for state management (emitState, handleConnectionStateChange), reconnection logic (reconnect with pRetry, generation tracking, abort guards), connect with retry, cleanup, isConnected, and getConnectionState. The RealtimeTransportManager interface was introduced but only defines the contract — the substantial shared behavior (~150 lines) is duplicated rather than extracted into a shared base class or helper.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By design — same reasoning as IVSConnection vs WebRTCConnection. The reconnect logic differs (IVS reconnects the stage, WebRTC renegotiates SDP). A shared base manager would need to abstract away transport-specific reconnect, adding complexity.

Two new latency tracking approaches for real-time streams:
- Composite RTT: stitches client/server STUN RTTs + pipeline latency
- Pixel Marker: embeds seq number in output frame pixels for true E2E measurement

Both work across WebRTC and IVS transports, gated by client connect options.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
initialPrompt: config.initialPrompt,
logger: this.logger,
onDiagnostic: config.onDiagnostic,
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IVS reconnect reuses stale connection callbacks from constructor

Medium Severity

IVSManager creates a single IVSConnection in the constructor with initialImage and initialPrompt baked into the callbacks. On reconnect, this.connection.connect() is called again, which re-sends the initial image/prompt (phase 3 of the connect flow). This means every reconnection redundantly re-sends the initial setup state, which may cause unexpected behavior like resetting the user's current prompt back to the initial prompt.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IVSManager creates a new IVSConnection on each reconnect in handle_ivs_connect, passing fresh callbacks. The constructor stores the initial callbacks but reconnect creates a new connection instance with current state.

Consolidate CompositeLatencyTracker + PixelLatencyProbe setup/teardown
into a single pluggable LatencyDiagnostics class, reducing ~35 lines of
inline wiring in client.ts to a simple instantiate/wire/stop pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

IVS publish stage was only sending video tracks and WHIP publish
was missing the audio output track, causing audio to be silently
dropped — a regression from existing WebRTC behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nagar-decart nagar-decart changed the title feat(realtime): add IVS transport support feat(rt): add IVS transport to SDK Mar 9, 2026
… start

Reject remoteStreamPromise when IVS subscribe stage disconnects during
setup, preventing the connect() flow from hanging forever. Store and
clear the latency diagnostics delayed-start timer on disconnect to avoid
restarting diagnostics after cleanup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@nagar-decart nagar-decart merged commit 2ebf352 into main Mar 9, 2026
1 of 3 checks passed
@nagar-decart nagar-decart deleted the nagar-decart/ivs-sdk-transport branch March 9, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant