utd_stream_sdk 0.2.0 copy "utd_stream_sdk: ^0.2.0" to clipboard
utd_stream_sdk: ^0.2.0 copied to clipboard

Headless LiveKit-based audio/video SDK for Flutter (audio call, video call, live stream, audio room) as logic-only sessions with no UI — build your own interface.

UTD Stream SDK (headless) #

LiveKit-based audio/video for Flutter with no UI. You get the logic — sessions, reactive state, and control methods — and build your own interface. One set of credentials (app_id + app_key) works for every product type your project enabled; the type is chosen per session, never per credential.

The SDK is server-authoritative: publish capability, seats, roles, and bans are all granted by the engine. The SDK mirrors that state and never elevates its own permissions, and no project secret ever ships in the app.

The four types #

Type What it is Session
audio_call 1:1 audio UTDCallSession
video_call 1:1 audio + video UTDCallSession
live_stream one host, unlimited viewers, engine-uncapped co-publishers UTDLiveStreamSession
audio_room seat-based speakers + listeners UTDAudioRoomSession

Install #

dependencies:
  utd_stream_sdk: ^0.2.0

Platform setup #

Declare the media permissions the SDK needs (see example/ for a working setup):

Platform Required
iOS NSMicrophoneUsageDescription, NSCameraUsageDescription in Info.plist
Android RECORD_AUDIO, CAMERA, INTERNET, MODIFY_AUDIO_SETTINGS, BLUETOOTH_CONNECT (API 31+)
macOS com.apple.security.device.audio-input / .camera / .network.client entitlements + the usage strings
Web none (the browser prompts)

Quick start #

final client = UTDStreamClient(appId: '1234567890', appKey: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6');

// ── Audio room ──
final room = await client.joinAudioRoom(identity: 'u1', roomName: 'lobby', asHost: true, seatCount: 8);
await room.takeSeat(0);            // engine grants publish; the SDK waits for the grant, then unmutes
room.seats.addListener(rebuildUI); // reactive UTDSeatGrid

// ── Live stream ──
final live = await client.joinLiveStream(identity: 'host1', roomName: 'show', asHost: true);
await live.goLive();               // host publishes camera + mic
await live.inviteToStage('u2');    // promote a viewer to co-publish — no engine count limit
// viewers join as `audience` (subscribe only) and can raise a hand: await live.requestStage();

// ── 1:1 call ──
await client.authenticate(identity: 'u1');         // mint a bearer (calls have no room to mint first)
final call = client.callSession(UTDStreamType.videoCall);
final created = await call.start(calleeIdentity: 'u2');
// callee side: client.authenticate(identity:'u2'); await call.accept(created.callId);

State: two ways to consume it #

Every session exposes the same state reactively (ValueListenables + a raw events stream) and imperatively (one assignable event handler). Use whichever fits — or both.

// Reactive
ValueListenableBuilder(valueListenable: room.connectionState, builder: (_, state, _) => ...);
room.seats.addListener(rebuildUI);

// Imperative (zego-style assignable callbacks, per session)
room.setEventHandler(UTDStreamEventHandler(
  onConnectionStateChanged: (s) => ...,
  onSeatUpdate: (grid) => ...,
  onSpeakerEvent: (e) => ...,
  onForceExit: (reason) => leaveScreen(reason),
  onError: (e) => report(e),
));

Listenables available on every session: connectionState (UTDConnectionState), connected, canPublish, micEnabled, cameraEnabled, speakerOn, screenShareEnabled, remoteParticipants, activeSpeakers. Audio rooms add seats (UTDSeatGrid), chatLocked, roles; live streams add stage; calls add status and callId.

Connection lifecycle & reconnection #

UTDConnectionState is disconnected → connecting → connected → reconnecting → forceExit. LiveKit auto-reconnects; the SDK runs a tiered re-sync (short outage → light seat re-sync, longer → full re-sync, past the window → force-exit) and re-reads room state from metadata + REST so your UI is never left stale. A failed/aborted connect can never orphan a live audio session.

A force-exit funnel delivers server-driven removals exactly once — ban, single-active-session takeover (signed_in_elsewhere), kick, or an unrecoverable reconnect — via session.forceExit / onForceExit.

Device control & audio routing #

await room.setMicrophone(true);          // gated on the server publish grant
await room.setSpeakerPreferBluetooth();  // prefer a BT headset, else loudspeaker
await room.applyBluetoothAudioRouting(); // Android fix: call after connect + publish
await room.switchCamera();
await room.setScreenShare(true);
room.muteAllRemoteAudio(true);

live_stream: roles & the stage #

live_stream is the seatless, unbounded broadcast (seat_count / seat_mode / host_seat are ignored; the engine creates the room uncapped). Your app decides how many co-publisher tiles to surface from UTDLiveStreamSession.stage, which updates reactively from _stage_update.

Role Publishes? Moderates? How you get it
host yes yes the verified room owner (asHost: true)
guest yes no a host-invited co-publisher (inviteToStage)
admin no yes a host-promoted moderator — never on camera
audience no no the default for everyone else

Roles are server-authoritative: a non-owner's claimed role is clamped to audience, so a viewer can't self-grant publish or moderation. Moderator promotion is owner-only via session.moderation.changeRole(id, UTDRole.admin).

audio_room: seats #

seats is a reactive UTDSeatGrid kept fresh from _seat_update + room metadata, with a REST re-sync on reconnect (guarded so a slow fetch can't clobber a fresher push). Speakers:

await room.takeSeat(1);                 // free mode
await room.requestToSpeak();            // request mode → host approves
await room.approveSpeaker(requestId);
await room.inviteToSpeak('u9', seatIndex: 2);
// host seat ops: moveSeat, kickSeat, lockSeat/unlockSeat, muteSeat/unmuteSeat

Moderation #

Host/admin operations are server-verified (session.moderation):

await room.moderation.ban('u9', reason: 'spam', durationSeconds: 3600);
await room.moderation.kick('u9');
await room.moderation.changeRole('u9', UTDRole.admin);
await room.moderation.lockComments();
await room.moderation.forceMute('u9');

Realtime: presence, incoming calls & chat (WebSocket) #

Presence, incoming-call push, and DM/group chat run over the engine's signalling WebSocket. Its token is minted with the project server secret, so — to keep the no-secret-on-client guarantee — your backend mints it and the SDK consumes it:

final signal = client.signalClient(
  credentialsProvider: () async {
    final res = await myBackend.mintSignalToken(); // your server calls POST /api/v1/signal/token
    return UTDSignalCredentials(token: res.token, url: res.url);
  },
);
await signal.connect();

signal.incomingCalls.listen((inv) => showIncomingCall(inv));      // the callee finally gets the call_id
signal.presenceChanges.listen((p) => updateDot(p.identity, p.status));
signal.messages.listen((m) => appendMessage(m));

await signal.subscribePresence(['u2', 'u3']);
await signal.sendMessage(toIdentity: 'u2', content: 'hi');
final invite = await signal.inviteCall(calleeIdentity: 'u2', type: UTDStreamType.videoCall);

The provider is re-invoked on every reconnect, so expired tokens refresh transparently.

Error handling #

All failures surface as UTDStreamException, with subtypes you can branch on: UTDBannedException (403), UTDRateLimitedException (429, with isTakeoverCooldown), UTDTokenException (malformed mint). Each carries a stable UTDErrorCode.

Architecture #

  • UTDStreamClient — entry point; holds credentials, mints tokens, produces sessions + the signal client.
  • UTDApi / UTDApiClient — engine REST (mint host uses X-App-Key; privileged ops use the server-minted Authorization: Bearer). No secret ever ships in the app.
  • UTDRoom — headless LiveKit Room wrapper: lifecycle, reconnection, the decoded data plane, publish gating, audio routing.
  • UTDDataRouter — decodes the engine's data-channel messages into typed state.
  • UTDSignalClient — the realtime WebSocket plane (presence / calls / chat).
  • session/* — one control surface per product type; UTDModeration for host ops.

License #

MIT — see LICENSE.

0
likes
130
points
0
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Headless LiveKit-based audio/video SDK for Flutter (audio call, video call, live stream, audio room) as logic-only sessions with no UI — build your own interface.

Topics

#livekit #webrtc #voice-chat #video-call #live-streaming

License

MIT (license)

Dependencies

device_info_plus, dio, flutter, flutter_webrtc, livekit_client, package_info_plus, shared_preferences, web_socket_channel

More

Packages that depend on utd_stream_sdk