utd_live_room_kit 3.3.0 copy "utd_live_room_kit: ^3.3.0" to clipboard
utd_live_room_kit: ^3.3.0 copied to clipboard

Real-time video live room for Flutter: host camera plus up to three guest tiles, go-live requests, host media control, chat, and minimize/PiP.

Changelog #

3.3.0 #

Broadcasters on the loudspeaker echoed — asking for the media audio profile was also switching every echo canceller off.

UTDAudioMode.enableMediaMode() calls UtdmClient.initialize(mediaAudioProfile: true) for one reason: keep the OS out of the telephony profile so viewers are never "in a call" — other apps keep the microphone (a WhatsApp voice note works with the room open) and audio routes like music rather than to the earpiece. In the SDK that flag also disabled the native hardware echo canceller and noise suppressor, swapped the capture source to a raw microphone, and forced WebRTC's own software AEC/NS/AGC off — so a broadcaster was publishing a completely unprocessed microphone into the room.

Requires utd_media_client 2.9.0, where routing and processing are separated. Nothing in this kit's own routing or session recipes changes: viewers keep the media profile exactly as before.

  • A dropped audio profile is no longer invisible. The media engine is built once per process: if anything touches WebRTC before this kit, the profile is silently discarded and the whole session behaves as a phone call, with nothing able to repair it. UTDAudioMode.engineAudioProfileIsMedia now exposes whether it was actually applied, UTDAudioMode.onAudioProfileResolved reports it to the host app's analytics, and a failure is logged at SEVERE in release builds instead of only in a debug console.

3.2.1 #

Every request now says which kit sent it.

The kits identified themselves with nothing — Accept, Content-Type, X-App-Id, X-App-Key and no more — so the engine could not tell a device on the current release from one on a build from June.

That gap blocks a specific decision. The publishable app_key mint is the impersonation path we are retiring, and it can only be closed per project, after that project's app has actually moved to a server-signed token. Closing it on an app that still mints with the key stops that app instantly. Without a version on the wire, deciding which projects have moved is a guess — and a wrong guess costs a paying customer their app.

Every request from both HTTP clients (the token host and the engine host) now carries:

X-UTD-Kit: utd_live_room_kit/3.2.1

It is a label, never a credential: it names the library, and nothing about the app, the user, or the project — those already have their own headers. That is what makes it safe to keep in a server log, which matters here: the header that used to answer this question, X-App-Key, had to be dropped from our access logs because it was leaking live credentials.

Sent unconditionally on both clients on purpose. A device that only ever mints and a device that only ever acts in-room must both be countable; a header present on some calls and not others would undercount exactly the apps we most need to see. And a request arriving without it is itself the finding — that is an old kit.

Nothing else changed. Both authentication paths keep working exactly as before: appKey is still accepted and still required by the widget, and tokenProvider is still the optional server-signed alternative. No app needs to change anything to upgrade.

kitVersion is pinned to pubspec.yaml by a test that reads the pubspec and fails when the two disagree, so the version on the wire can never quietly drift from the version that was published.

3.2.0 #

The kit now understands "no", and it stops knocking.

Two gaps, one root: the kit read the engine's refusals as prose instead of as a contract, and it had no way to say "this answer is final".

A suspended project is no longer knocked on forever #

The engine answers a suspended project with

{ "message": "...", "code": "project_suspended", "retryable": false }

and this kit read neither field. A 403 was classified by searching its message for the word "ban" — "Project is suspended" contains none, so the refusal became a generic "not available", the redial schedule kept its 30-second ceiling, and the client went on asking: 120 requests an hour per device, forever. One suspended project produced 5,489 token requests.

Refusals are now classified on code and retryable:

  • project_suspended / client_suspendedUTDProjectSuspendedException (a subtype of UTDServiceNotAvailableException, so code that already handles "not available" keeps working untouched). isClientSuspension tells the two apart.
  • user_bannedUTDBannedException, even when the message never says "ban".
  • Any retryable: false, on any status and with a code this kit has never seen, is final too.
  • 429 and 5xx are unchanged: they back off and retry, because backing off IS the answer to a rate limit.

code is read before the status code, so an engine that moves suspensions off 403 needs no new kit.

An engine that sends neither field behaves exactly as it did. The old text match still runs, but only when there is no code at all — so an app on a not-yet-updated engine sees the same exceptions as before, and a not-yet-updated app on the new engine still catches everything through UTDServiceNotAvailableException.

Final means stopped, not slowed #

UTDRedialPacer could only ever slow down. A ceiling is the right answer to a network that is down and the wrong one to an engine that has already given its final answer, so the pacer can now be halted: halt() / isHalted / clearHalt(), cleared automatically by a dial that connects.

The stop lives in the room manager, at the one place a session is opened — so an app that calls connect() in its own loop is stopped by the same line that stops the kit's own recovery. UTDRoomManager.dialingStoppedBy exposes the refusal, resumeDialing() clears it (for the app that knows the bill was paid). Recovery stops on the first final refusal instead of spending its remaining attempts on an answer already in hand.

The user is told the truth, and the developer gets the details #

UTDRoomController.onDialingStopped fires once with the refusal, so the app can leave the room instead of holding a spinner over a room that is never coming.

The engine's suspension message is written for the developer — it explains that settling the invoice restores service immediately. It is never shown to an end user: the built-in connect-error view shows the new UTDRoomStrings.serviceSuspended ("This room is temporarily unavailable. Please try again later.", localised) with no Retry button. Showing an account's billing state as if it were a personal block is exactly what the old contains('ban') match did.

The user token renews itself before the engine starts refusing #

The renewal machinery shipped in the previous release but nothing armed it on the kit's own minting path: only an app that adopted its own token ever scheduled anything. An app_key room therefore ran until the engine began refusing — 715 refusals for one user in a day, with zero renewal attempts among them.

  • generateToken() now arms the ahead-of-time renewal for the token it just minted, in every mode.
  • A widget-level tokenProvider is forwarded to the controller, so a server-signed app can be re-minted for by its own backend instead of falling through to "no mint source".
  • A failed renewal backs off (5s, 10s, 20s, 40s, 60s) and stops at five, rather than either giving up on the first failure or hammering. A refusal the engine called final is not retried at all.

3.1.0 #

Staying on the stream is now the kit's job, not your app's.

Four days of production logs say the same thing in four different ways: when a session is interrupted, the kit hands the problem to the host app, and the host app does the only thing it can - try again, immediately, forever. Every item below is one of those loops moved inside the kit, where it can be paced, measured and stopped. The last one closes a gap this kit had and the audio kit did not: it reported no quality telemetry at all.

Reconnects back off instead of hammering - and stop exhausting TURN #

One user produced 100 session attempts in 45 seconds. The engine grants a user twelve TURN allocations; every attempt builds a new PeerConnection and claims one. The quota was gone in the first few seconds, and every attempt after that was refused before it could reach the room - the retry loop was the outage. This kit's own retry delay was a flat 500ms, which is the shape of that incident.

Re-establishing now follows a schedule: 1s, 2s, 4s ... capped at 30s, with plus/minus 20% jitter so a fleet knocked offline by one SFU restart does not come back in lockstep and re-create the outage. The schedule is enforced where the dialing happens (UTDRedialPacer inside the room manager), so an app that calls connect() in its own loop is paced too. Dialing a different room is a new intent, not a retry - a viewer switching streams never waits.

  • New: UTDReconnectPolicy (the schedule) and UTDRedialPacer (the enforcement).
  • Removed: UTDConstants.retryDelay. The delay is no longer a single number; UTDConstants.reconnectInitialDelay / reconnectMaxDelay / reconnectJitterFraction describe the schedule instead. If you referenced the old constant, this is the one line you need to change.

An expired user token is renewed once, and the refused call is replayed #

The engine answers an expired user token with 401 {"code":"token_expired"} and WWW-Authenticate: Bearer error="invalid_token", error_description="expired". The kit read neither and kept sending the dead token: 715 refusals for a single user in one day, with every stage, role and moderation call in that window failing silently while the stream looked fine.

Now: the refusal is recognised (body and header), the token is re-minted once, and the refused request is replayed with the fresh bearer. Concurrent failures share one mint rather than each triggering their own. A 401 that is not an expiry - a revoked token, the wrong project - is passed through untouched, because re-minting for those is a loop, not a fix.

The kit also renews ahead of time, at 80% of user_token_expires_in, which matters most here: a broadcast runs far longer than a token's life.

  • If your backend mints tokens: set controller.tokenProvider so the kit has a way to get a fresh one. Without it, renewal is not possible and refusals surface exactly as they did before.
  • If you mint through controller.generateToken(): nothing to do - the last request is replayed.
  • UTDTokenResponse.userTokenExpiresIn carries the lifetime through (numbers or numeric strings).

Returning from the background resumes the session instead of evicting it #

The engine allows one session per identity. Re-joining the room you are already in therefore kicks your own session out - DUPLICATE_IDENTITY, 374 times in one day on a single project, each one a dropped stream for the user it happened to.

connect() now recognises a join for the room already in hand: the existing session gets up to 8 seconds to come back (keeping its PeerConnection and its TURN allocation), and only if it does not is a fresh session dialed. A join for a different room tears the old one down exactly as before. A session that ends for good is detected immediately - nobody waits out the timeout for a session that is already gone.

Quality telemetry, with latency_ms on every report #

This kit sent no quality reports at all, so a live stream was the one product surface whose quality dashboard was empty - "the stream is bad" arrived with no numbers attached. It now posts the same periodic snapshot the audio kit does: bitrate, packet loss, jitter and latency, once every 15 seconds, over the per-user bearer (and not at all without one, rather than into a guaranteed 401).

Latency is read from the transport's selected ICE candidate pair, so a viewer - who publishes nothing, and is most of the audience - reports it too. Unknown latency is sent as null, never a fabricated zero.

A re-established session asks the engine where the room is #

A room lives on a media node the engine assigns, and the engine can move it. A client that reconnected to its remembered node was observed landing on a different node than the one hosting its room - connected, and alone.

UTDRoomController.rejoin() mints a fresh token for the room on every attempt and dials the url that answer carries. It resumes first (above), re-mints second, and is bounded by maxAttempts (defaulting to autoRejoinMaxAttempts).

// The kit re-establishes the session by itself; you decide when to ask.
final back = await controller.rejoin();

The kit gets the user back into the room by itself #

Everything above still needed an app to notice the drop and ask. Now the kit does it: when a session ends without the app asking - a network change, an SFU restart, a carrier handoff - it resumes the existing session if it can, and otherwise asks the engine where the room is and re-establishes on the same schedule (1s, 2s, 4s ... capped at 30s, with jitter). One attempt at a time, never two, however many drop events or rejoin() calls arrive at once.

After autoRejoinMaxAttempts (10 by default) it stops and calls onRejoinFailed - one verdict, not one per attempt. That is your cue to show an error and leave the room; the kit will not try again on its own until the next connect().

Recovery stops immediately - including an attempt that is mid-backoff - when:

  • your app calls leave(), or disposes the controller;
  • the user was banned or signed in on another device (coming back would fight a decision made about that user);
  • you set autoRejoinEnabled = false.
controller
  ..autoRejoinMaxAttempts = 10           // default
  ..onRejoinFailed = () => showError();  // the kit has stopped trying

// controller.autoRejoinEnabled = false; // your app owns recovery instead
// controller.isRejoining                // true while it is trying
// controller.rejoinPolicy               // the backoff schedule, if you must tune it

If your backend signs tokens, set controller.tokenProvider - recovery mints a fresh token on every attempt and cannot work without a way to get one. With no token source the kit does not dial blindly; it calls onRejoinFailed.

3.0.0 #

What a broadcast costs the user — in their data plan, their battery and their phone's heat — is now the thing this kit is tuned for.

This kit shipped defaults meant for desktop broadband: 720p at 30 fps, published with a software codec. Measured end to end that is 2.31 Mbps of uplink — about 1 GB an hour out of a host's phone, and ~765 MB an hour for a viewer watching full-screen. Compared against the competing SDK our customers' users could be on instead, which publishes 360p / 600 kbps / 15 fps for host, co-host and audience alike, we were sending roughly four times the data and asking for twice the encoder work — for the same product, to an audience whose handsets are weaker than any device we had tested on and whose data is metered.

The codec — the single largest change #

Camera tracks now publish H.264 instead of the engine's VP8 default, with VP8 kept as the backup codec for anything that cannot take H.264.

Almost no Android phone has a hardware VP8 encoder — libwebrtc only enables one for Intel parts — so VP8 meant libvpx encoding on the CPU, and decoding on the CPU for every viewer too. Every Android device made in the last decade has hardware H.264 in both directions. On the phones this product actually runs on, this is the largest battery and thermal item in the pipeline, and it applies to senders and receivers alike.

Simulcast still works: the engine's Android factory wraps the hardware encoder in a simulcast adapter written specifically to handle H.264.

UTDVideoQuality.auto — and it is now the default #

The kit picks the tier from the device at connect: low on a phone with four cores or fewer, sd on everything else. It never resolves above sd — moving up is a decision the app makes explicitly, not one a core count makes for the user. UTDDeviceClass.override lets a host app that knows the handset better than a core count does say so.

This adds no dependency: the signal is Platform.numberOfProcessors from dart:io.

The ladder was retuned #

tier was now
low 640×360 · 450 kbps · 20 fps 640×360 · 500 kbps · 20 fps
sd 960×540 · 800 kbps · 25 fps 960×540 · 900 kbps · 20 fps
hd 1280×720 · 1.7 Mbps · 30 fps 1280×720 · 1.4 Mbps · 24 fps
fullHd 1920×1080 · 3 Mbps · 30 fps 1920×1080 · 2.5 Mbps · 24 fps

No tier asks for more than 24 fps. Frame rate costs encoder time linearly and buys very little on a talking head, so it is the cheapest thing to give up on a weak phone.

The capture rate is now capped as well, not just the encoding. Without that the camera kept producing 30 fps and every surplus frame was colour-converted, run through the effects processor, and handed to an encoder that discarded it — work paid for twice on the devices that could least afford it.

A battle publishes the same camera into two rooms at once, and the cross-room copy carried no publish options at all — it fell back to the engine's own defaults, which at the old HD default meant roughly 4.6 Mbps out of a single phone. It now goes out one tier below the home room, which costs nothing visually because the host occupies half the screen during a battle. Its microphone follows the same speech preset as the home room instead of falling back to a music preset.

Audio publishes as speech #

Unset, the engine falls back to a 48 kbps music preset. A live room carries a talking voice, which opus carries at 24 kbps with no audible loss. DTX is set explicitly rather than inherited, so a default moving underneath us fails a test instead of quietly doubling everyone's data.

What did not change, and is worth knowing #

Simulcast and adaptive stream stay exactly as they were, and they are the reason this kit is structurally cheaper than the alternative regardless of the numbers above: a 104×150 guest tile pulls the 180p layer here, where in a kit without simulcast the same thumbnail decodes the full stream. Every tier still publishes a small layer.

Migration #

Nothing to change. If you were relying on the 720p default, pass UTDLiveRoomConfig(videoQuality: UTDVideoQuality.hd) explicitly — and read the table above, because hd is now 24 fps.

2.4.0 #

Crowded streams cost what a small one costs. Every participant event — an arrival, a departure, an avatar change — rebuilt the whole room: jsonDecode over every participant's metadata, plus a new object and a copied attribute map for each. One viewer arriving paid for all of them. Filling a room to 300 cost 1+2+...+300 = 45,150 operations, quadratic in room size and paid in bursts exactly while the audience was arriving. The log line itself read participants.length, so every event paid it twice.

A participant index now absorbs each event into a single entry. Measured:

fill a room of 300     45,150 -> 300 decodes, 45,150 -> 300 builds
one avatar change           300 -> 0 decodes
  • The viewer list is served from the index, so a read costs nothing unless something changed, and a burst of arrivals notifies listeners once instead of once per arrival.
  • Roles are decoded once, when they change — not for the whole room on every event, and not again on every read.
  • 98 hot-path log calls no longer run in release. debugPrint is not stripped from release builds; utdLog takes its message as a closure, so the string is never built outside debug.

Public API unchanged.

  • Video Effects could never activate on a live stream. The engine resolves the per-platform entitlement from the os field on the token request and fails closed without it — and this kit never sent one. The parameter existed on generateToken, but nothing filled it and UTDLiveRoom did not pass it, so every live token arrived with no platform and the signed videoEffects claim came back false for every customer, however they had paid. (The audio kit has always reported it, which is why only live was affected.) The controller now resolves the platform itself when the caller does not supply one — no new dependency, dart:io answers the only question the entitlement asks. An app that already passes os still wins.

    A server-side change (2026-09-05) already restores the Android + iOS package on the kits in the field; this is what makes a single-platform licence resolvable at all.

2.3.0 #

Makes duplicate delivery diagnosable, and closes a stale-listener hole.

  • dataFrameStream — the same messages as dataStream, plus the transport metadata the decoded payload cannot carry: the per-send id and the sender's identity. dataStream delivers the decoded payload alone, so an app had no way to tell ONE message delivered twice from TWO separate sends of identical content — which is the entire difference between a transport fault and a sender sending twice. dataStream is unchanged; nothing is injected into the payload map an integration already parses.

  • A handler from a released Room can no longer reach the app. EventsListener.dispose() is async and the teardown does not await it: it cancels its twelve subscriptions one after another, so the data handler (eighth) stops only after seven awaits — and a connect retry builds the next Room immediately, with the previous teardown bounded to two seconds and left to finish in the background. Every handler now checks the Room generation it was created in, so the window is closed by construction rather than by microtask timing.

  • utd_media_client floor raised to ^2.8.5, which carries two correctness fixes this kit depends on: a replaced subscriber data channel no longer keeps delivering (the same duplicate shape this release makes diagnosable), and uplink audio quality is actually measured — loss and round-trip time were read from the wrong stats report and every speaker was reported as a flawless uplink.

2.2.0 #

Guest invitations become an ask, and the stage clears when people leave. No breaking changes.

  • Inviting a guest no longer forces them live. inviteToSpeak used to promote instantly: the viewer's publish permission flipped and the kit turned their camera and mic on, with no dialog and no way to refuse. It now sends a server-authoritative invitation — the target stays audience until they accept. Accept/decline, a built-in dialog (override with onInvitationUI), and onInvitationFailed for an accept that arrives too late. Set UTDLiveRoomConfig.invitationTimeout (or expiresIn per invite) to give the invitation a window; the engine enforces it and notifies both sides when it closes. addToStage still promotes immediately, for approving a raise-hand where a second confirmation makes no sense.
  • A guest's tile disappears when they leave. leave() only disconnected; the stage state was never updated, so every other client kept rendering the departed guest. A guest now steps off the stage before disconnecting, and when the host ends the broadcast every guest is removed first — so the next live starts empty instead of showing the previous session's guests. The engine also cleans up on an unexpected disconnect (crash, network loss).

2.1.1 #

Hardening — no API changes.

  • The media audio profile is claimed at controller construction — the native engine is built once per process by the first thing that touches it, and a host's camera preview could race the connect-time claim. See the new "Audio engine setup" README section: apps should also call UtdmClient.initialize(mediaAudioProfile: true) first thing in main().
  • pub.flutter-io.cn now lists the supported platforms (Android, iOS) explicitly.

2.1.0 #

Media audio profile — the stream no longer sounds like a phone call. No breaking changes.

  • A live stream now runs in the platform's MEDIA audio profile, not the telephony one. Before this, the OS treated a live stream as a phone call: Android sat in MODE_IN_COMMUNICATION (call volume, other apps blocked from the mic, "already in a call"), and on iOS the kit's Bluetooth routing set WebRTC's call profile — playAndRecord + voiceChat — on every join, viewers included: earpiece routing at call volume, and a session that needs microphone permission a viewer never granted, so it could fail to activate and play nothing at all.
  • Now: a viewer gets a pure playback session (media volume, speaker/BT routing, no "in call" state, never touches the mic); a broadcaster records under videoChat (speaker-routed) instead of the telephony voiceChat; Android runs MODE_NORMAL + the music stream with WebRTC's software echo-cancellation/noise-suppression/AGC kept on the mic. Bluetooth routing still works on both platforms — on Android via a re-applied media config with forced device routing, on iOS implicitly from the media session.
  • The flutter_webrtc speakerphone helper (which arms the iOS call profile) is funnelled through one platform-guarded call site, with tests pinning the session recipe per track state and validating every recipe against what AVAudioSession actually accepts per category.
  • utd_media_client floor raised to ^2.8.1 (the mediaAudioProfile engine flag).

2.0.2 #

Documentation fix — no code changes.

  • adminIdsResolver doc now describes what actually happens: resolved identities feed the HOST client's promotion targets, and the owner promotes them to admin via the owner-only role endpoint as they appear in the room. The doc previously described a client self-upgrade mechanism (upgradeSelfRole) that never existed in this kit — clients never self-assert admin.

2.0.1 #

Battery/network fixes — no breaking changes.

  • Background video pause (viewers). When the app goes to the background, the kit now disables every remote video publication (the SFU stops forwarding video; nothing is decoded behind a dark screen) and re-enables it on resume. Audio keeps playing, publishing (host/guest camera) is unaffected, and Android OS Picture-in-Picture keeps its video. Opt out via the new UTDLiveRoomConfig.pauseVideoInBackground (default true).
  • Mini overlay speaking ring is event-driven — reacts to activeSpeakers directly instead of a 1s polling timer.
  • activeSpeakers is now single-sourced from the engine's active-speakers event; the state backstop poll no longer duplicates it and runs every 2s (was 300ms), only for mute/camera state.

2.0.0 #

BREAKING — new media engine generation #

  • The kit now runs on utd_media_client (the UTD media engine client) instead of the previous third-party RTC client. Public media types are re-exported under the new names. Apps on 1.x keep working unchanged — 1.x stays on the old engine path; upgrade to 2.x deliberately, not via pub upgrade.

Added — official server-signed token support #

  • UTDRoomController.adoptTokenResponse(UTDTokenResponse) — adopts a token minted outside the controller (your backend calling POST /api/v1/token with X-App-Secret): applies the per-user bearer to every in-room API client (stage/ban/role/participant) exactly like generateToken does. Idempotent.
  • UTDLiveRoom.tokenProvider — new optional widget parameter (same shape as the audio-room kit's): supply a token from your backend and the widget adopts + connects with it, falling back to its own generateToken when the provider is unset, throws, or resolves null.

1.6.0 #

  • Developer-controlled video publish quality. New UTDVideoQuality enum (low 360p / sd 540p / hd 720p / fullHd 1080p) exposed as UTDLiveRoomConfig.videoQuality and UTDRoomController.setVideoQuality (call before connect). The tier drives BOTH the camera capture resolution and the publish encoding (defaultVideoPublishOptions.videoEncoding + simulcast layers) for every local camera path: host self-preview → Go Live, guest go-live, setCameraEnabled, and reconnect re-publish. Default is HD (720p) — unchanged capture behavior, but the publish bitrate is now pinned to the tier (~1.7 Mbps for HD) instead of the engine's derived default.

1.5.1 #

  • Distinguish a not-activated service from a ban on the token endpoint. A non-ban 403 (e.g. Type 'live_stream' is not enabled for this project) now throws the new UTDServiceNotAvailableException instead of UTDBannedException.
  • The built-in connect-error view shows a distinct "not available" message and hides Retry for that refusal (retrying can't help). Adds UTDRoomStrings.serviceNotAvailable (EN + AR).

1.5.0 #

  • Video Effects entitlement (trusted, token-signed). The kit now decodes a server-signed videoEffects entitlement from the join token (the engine resolves it per-platform against the requesting client's os at mint and stamps a boolean into the token's metadata claim) and threads it into the video-processor factory. Effects therefore run only when the customer has activated + paid for the current platform; unentitled sessions get a passthrough processor. The signature is read client-side WITHOUT verifying it (the SFU verifies the token on join); the processor is the authoritative gate.
    • Breaking: the processor factory now receives the entitlement — UTDLiveRoomConfig.buildVideoProcessor and UTDRoomController.setVideoProcessorFactory changed from TrackProcessor Function()? to TrackProcessor Function(bool entitled)?. Forward the flag to your processor, e.g. (entitled) => VideoEffectsProcessor.create(entitled: entitled).
    • New UTDRoomController.videoEffectsEntitled getter (valid after connect) so UI can surface an "activate to unlock" hint.
    • Backward-compatible / fail-open: a token with no videoEffects claim (older engine) keeps effects working; only an explicit false disables them.

1.4.0 #

  • Seat grid → unbounded stage. A live room is no longer a fixed 4-tile seat grid (host on seat 0 + up to 3 guest tiles). live_stream is now ALWAYS the engine's unbounded seatless stage: the media room is uncapped (maxParticipants: 0) so viewers are unlimited, and this package decides how many co-host tiles to surface (still 4 by default — the cap is now purely a UI choice, not an engine limit). Every non-owner joins as audience; the host promotes co-publishers post-join.

  • Type-first token. generateToken now sends type: 'live_stream' to POST /api/v1/token and no longer sends service/kind or seat_count/seat_mode/host_seat — those are ignored for live_stream. The engine still accepts the legacy service(rooms)+kind(live) fields, so an un-migrated app keeps working; this version opts into the canonical type. The same app_id/app_key works for every product type the project has enabled — type is a per-request field, not a credential. A request for a type the project hasn't enabled returns 403 "Type 'live_stream' is not enabled for this project".

  • Publishing decoupled from moderation. Roles are server-authoritative and the engine clamps a non-owner's claimed role to audience (a client can no longer self-grant publish by claiming role: 'host'):

    • host — the verified room owner (publishes and moderates).
    • guest — a host-invited co-publisher (publishes only).
    • admin — an owner-promoted moderator that moderates only and is never on camera (decoupled — promoting to admin no longer grants a tile).
    • audience — default (neither).
  • New stage_api (UTDStageApi, exported via stage_api.dart) for the live_stream stage endpoints (all live_stream-only; the engine returns 400 on a seated/non-live_stream room and 403 if live_stream isn't enabled):

    • getStageGET /api/v1/rooms/:name/stage{ members: [{ identity, name, role }] } (publishers = host + guests).
    • addToStagePOST /api/v1/rooms/:name/stage/add { target_identity } (host/admin → grants publish, sets role guest).
    • removeFromStagePOST /api/v1/rooms/:name/stage/remove { target_identity } (host/admin → back to audience).
    • leaveStagePOST /api/v1/rooms/:name/stage/leave (self step-down).
    • requestStagePOST /api/v1/rooms/:name/stage/request (viewer raise-hand; the engine notifies host/admins via a _stage_request data message — no server-side queue).

    The actor is resolved server-side from the per-user bearer; identity rides the body as a dual-mode fallback. Stage state arrives over the data channel as _stage_update (roster) and via the _stage room-metadata key for late joiners; _stage_request is the raise-hand ping.

  • Moderator promotion reuses the existing role endpoint — PUT /api/v1/rooms/:name/participants/:identity/role { role: 'admin' }, owner-only. On a stage room this grants moderation but not publish; the engine refuses (409) to add an admin to the stage to keep the two capabilities disjoint (demote first).

  • Removed seat_api and speaker_api along with the invite/request-to-go- live invitation handshake (/seats/*, /speakers/* including speakers/invite + invitations/:id/accept|decline). The live room is the stage now; seats/speakers remain in utd_audio_room_kit (and on the engine for audio_room + the legacy live kit) but are gone from this package. Breaking for integrators driving seats/speakers directly: switch to UTDStageApi.

  • Minimum engine version: requires an engine build with the type-first token path and the live_stream stage endpoints (enabled_types + /stage/*). Older engines that only understand service/kind will reject the type field — stay on 1.3.0 against those until the engine is upgraded.

1.3.0 #

  • No-backend credentials (recommended): pass UTDLiveRoom(appKey: ...) / UTDRoomController.initApi(appKey: ...) — the project's publishable app key. The kit mints tokens directly from the engine (X-App-Key on POST /api/v1/token), and the engine signs the returned per-user user_token with the project server_secret server-side, so the secret never ships in the app and no integrator backend is required. The kit applies that user_token as the Authorization: Bearer for all in-room/moderation calls (persisted across initApi re-inits, so it survives restore-from-minimize).
  • Removed tokenProvider and its UTDTokenRequest / UTDTokenBundle / UTDTokenProvider types (added in 1.2.0). The no-backend appKey flow above replaces it. Breaking for integrators who adopted tokenProvider: migrate to appKey.
  • Removed serverSecret from UTDLiveRoom and UTDRoomController.initApi (deprecated in 1.2.0). Shipping the project secret in an app let anyone extract it and mint tokens for any identity/room. Breaking: appKey is now the only credential and is required on UTDLiveRoom. The legacy X-App-Secret header path is gone (UTDApiClient no longer takes appSecret).
  • A leaked app_key cannot forge bearers offline or call the server-to-server API, and rotates independently via the engine regenerate-credentials admin endpoint.

1.2.0 #

  • Secure credential mode: new tokenProvider callback mints tokens via the integrator's own backend (which holds the project secret and authenticates the real user) instead of embedding serverSecret in the app. The kit never sees the secret; the returned per-user user_token becomes the Authorization: Bearer for all in-room/moderation REST calls.
  • Deprecate serverSecret on UTDLiveRoom and UTDRoomController.initApi (now optional). Shipping it in an app lets anyone extract it and mint tokens for any identity/room. Existing callers keep working in legacy/dual mode.
  • Add the UTDTokenRequest, UTDTokenBundle, and UTDTokenProvider types (exported via token_provider.dart); UTDTokenResponse gains userToken.
  • The secure-mode per-user bearer is persisted on the controller and re-applied whenever the API clients are rebuilt (e.g. restore-from-minimize re-inits without re-minting a token), so in-room/moderation calls stay authenticated.
  • generateToken validates the tokenProvider bundle and throws UTDTokenException on an empty token/url instead of failing later in connect.

1.1.0 #

  • Single-active-session enforcement: send a stable per-install device_id (persisted via shared_preferences, auto-resolved in generateToken) and handle the _kicked (signed_in_elsewhere) data event through the existing exit funnel with a distinct "signed in on another device" notice and dialog.
  • Add UTDRateLimitedException for 429 responses from the token endpoint.
  • Split the API into separate token (udt-stream.com) and engine (engine.udt-stream.com, grey-cloud) clients; configure via the new engineBaseUrl parameter on initApi.
  • Security: stop mirroring user attributes into participant metadata (server-owned, spoofing vector); chat text/sender name are treated as untrusted and rendered plain-text only.

1.0.0 #

  • Initial standalone release. Extracted from the Tempo-Live monorepo into its own package repository.
  • Real-time video live room (host camera + up to 3 guest video tiles) built on the same seat state machine as utd_audio_room_kit.
  • Camera tiles, invite / request-to-go-live, host force-control of guest media, real-time chat over the data channel, tiered reconnection, and minimize / Android OS Picture-in-Picture.
  • Pairs with utd_video_effects_kit via UTDLiveRoomConfig.buildVideoProcessor (a video track processor) for real-time filters / beauty effects.
0
likes
140
points
883
downloads

Documentation

API reference

Publisher

verified publisherutdsoftware.com

Weekly Downloads

Real-time video live room for Flutter: host camera plus up to three guest tiles, go-live requests, host media control, chat, and minimize/PiP.

Homepage

Topics

#video #live-streaming #webrtc #video-call

License

MIT (license)

Dependencies

audio_session, dartz, dio, equatable, floating, flutter, flutter_bloc, flutter_screenutil, flutter_webrtc, get_it, permission_handler, pretty_dio_logger, shared_preferences, utd_media_client

More

Packages that depend on utd_live_room_kit