utd_audio_room_kit 1.11.0
utd_audio_room_kit: ^1.11.0 copied to clipboard
Real-time live audio room for Flutter: seat management, real-time chat, mic/speaker controls, speak requests, moderation, and minimize/PiP.
Changelog #
1.11.0 #
Feature — activate the dormant Android OS-level Picture-in-Picture layer for the audio room (previously defined but never wired up). Mirrors the live kit.
UTDAudioRoomnow calls_controller.pip.armIfEnabled()on both the fresh-connect and the reuse (already-connected) branches, so pressing Home while in a connected room auto-enters system PiP (Android 12+/API 31+).armIfEnabled()self-guards (Android-only,enableOSPip, device support), so it is a harmless no-op on iOS and unsupported devices.build()now swaps its body to the compactUTDPipView(room cover + active-speaker soundwave) viaValueListenableBuilder(pip.isInPip)when the OS reports the app is in PiP; expands back to the full room otherwise.- No new public API; opt in per room via
UTDMinimizeConfig(enableOSPip: true). The host app forwards the OS PiP enter/exit transition to the controller viapip.setInPip(...)(already available).
1.10.4 #
Fix — OTHER users in the room saw a peer's OLD avatar/frame/name after that peer changed them mid-session (1.10.3 fixed only the changer's own seat).
- Root cause: repainting a peer's seat relied on the SFU fanning the changer's
setAttributesout to present participants as aParticipantAttributesChangedevent. That fan-out is as unreliable as the sender echo 1.10.3 worked around (this deployment does not reliably deliver it), so peers kept the stale_seatssnapshot until they rejoined. updateSelfAttributesnow ALSO broadcasts the changed cosmetics over the reliable data channel as a_cosmetic_updatemessage ({type, identity, attrs}) — the same transport chat/gifts/seat-updates use, whosecanPublishDatagrant is a universal invariant. Each recipient applies it to the sender's seat viaapplyCosmeticOverride.setAttributesis still called so LiveKit's attribute sync covers late joiners (who never receive the one-shot data message).applyLocalCosmeticOverride(identity, attrs)→ generalized toapplyCosmeticOverride(identity, attrs), and the override store is now per-identity (Map<identity, Map<key,value>>) so it holds overrides for any participant, not just the local user. Empty values are honored (an unequip, e.g.fr:'', authoritatively clears a stale seat value). The override wins over both the live attribute map and_seatsin_withLiveAttributes. A departed participant's override is dropped on participant-left; the whole map clears on dispose.
1.10.3 #
Fix — the local user's OWN seat did not repaint on a mid-session cosmetic change.
LocalParticipant.setAttributes(livekit_client) only sends the change to the SFU and awaits an ack; it does not updatelocalParticipant.attributes, and the server does not necessarily echo a participant update back to the sender. So afterupdateSelfAttributes,_withLiveAttributesre-read the still-stale local attribute map and the user's own seat kept the old avatar/frame until they rejoined (a rejoin reads the fresh backend_seats).- New
UTDSeatController.applyLocalCosmeticOverride(identity, attrs), called fromupdateSelfAttributes, records the freshly published values in a_localCosmeticOverridesmap and repaints the local seat immediately. The map is overlaid on the local participant in_liveParticipantInfo, so a subsequent_seatsreapply cannot revert the seat to the stale published values. The eventual RTC echo (if any) is then a no-op. Cleared on dispose.
1.10.2 #
Live cosmetics update — additive and backward compatible.
Mid-session avatar / frame changes #
- New
UTDRoomController.updateSelfAttributes(Map<String, String>)re-publishes the local participant's cosmetic attributes (avatar, frame, frame geometry, color name) after join. LiveKit fans the change to everyone in the room and to late joiners, so a user who changes their photo or equips a new frame updates on their seat — for themselves and others — without leaving and re-entering. _withLiveAttributesnow treats cosmetic keys as live-authoritative: a non-empty live value wins over the (possibly stale)_seatssnapshot, instead of only filling blanks. Non-cosmetic keys keep the previous backend-wins / fill-blanks-only behavior. Cosmetics are never server-moderated (the backend only reads them), so preferring the occupant's own latest published value is correct.- The cosmetic key set is not hardcoded — it is derived at runtime from the
exact keys the app publishes as the local participant's attributes (the
userInRoomAttributesmap at connect, plus everyupdateSelfAttributesdelta), via the newUTDSeatController.registerCosmeticKeys. The app publishes only cosmetics as participant attributes, so the set can never drift from the app's own cosmetic definition, and new cosmetic keys need no kit change. Until the app registers any keys, the merge falls back to blank-fill-only (the original behavior).
1.10.1 #
Bugfix.
Speaking ring #
UTDSpeakingRingno longer shrinks the avatar while the occupant is speaking. The pulsing ring previously insetting the child by its animating border width, so the seat image visibly shrank and pulsed smaller on every open mic. The ring now paints just OUTSIDE the avatar (BorderSide.strokeAlignOutside) and the avatar keeps a constant size. The defaultUTDSeatWidgetoccupied-seatStackis nowClip.noneso the outward ring/glow isn't clipped.
1.10.0 #
Dev-tunable seat sizing, snappier minimized overlays, and a polished pre-connect state. All additive and backward compatible — apps that pass nothing are unaffected.
Seat layout #
- New
UTDSeatLayout(avatarRatio,rowPadding,rowSpacing,seatScale), passed viaUTDAudioRoomConfig.seatLayout. The mounted room publishes it to a shared source so every seat-size consumer (grid, skeleton,computeSeatSizecall sites, and the app-side metrics) reads the same values and can't drift. Defaults reproduce the original look; the 52–120px safety clamp still applies as a hard rail.
Minimized overlay & PiP #
- Speaking state is now event-driven off
activeSpeakersinstead of a 1s pollingTimer— the ring reacts instantly and nothing ticks while the room is silent. - Wave animations run only while someone is speaking, so a silent minimized session holds zero per-frame animation work.
- Overlay bubble snaps to the nearest edge on release (with flick support), clamps clear of system chrome so it can't tuck behind the notch/home indicator, lifts on grab, and adds haptic feedback on drag/restore/close/mic actions.
Connecting state #
- The static pre-connect placeholder is now a gently breathing skeleton driven by a
single shared
AnimationController, wrapped in aRepaintBoundary, torn down the instant the real room paints, and honoring reduce-motion.
1.9.0 #
Room renders at token receipt — occupied seats with names and avatars appear roughly one token round trip after the tap, instead of waiting for the full RTC connection (WSS + ICE/DTLS). On a prefetched token the room is effectively instant.
Rendering #
- The room body (seats, chat, controls) now paints as soon as the token response arrives
(
_tokenReady). The grey skeleton only gates the ban check (token POST 403), not the entire RTC establishment. A connect failure drops back to the skeleton/error view. - Seats are seeded pre-connect via
UTDRoomController.primeSeats: newer engines embed aseatssnapshot in the token response (zero extra round trips); against older engines the kit firesGET /seatsin parallel with the RTC dial. - Seat mutations (take/move) are gated on connection state so a tap during the pre-connect window cannot create ghost occupants.
Avatars #
UTDDefaultAvatarnow usescached_network_image(disk cache) — avatars download once per install, not once per session. Keyed by occupant so a seat that changes hands shows the new person's initials, not the old photo.MediaQuery.devicePixelRatioOfreplaces the fullMediaQuery.ofto avoid rebuilding every avatar on each keyboard-inset animation frame.
Seat updates #
- Seat mutation responses from newer engines carry the synced seat
stateinline; the kit applies it immediately (applyMutationResponse, generation- and room-guarded) so the actor sees changes without waiting for the_seat_updatebroadcast round trip. - Blank occupant name/avatar self-heals from the occupant's live RTC participant attributes
(
_withLiveAttributes+ aparticipantAttributesChangedrepair listener). The per-key merge never un-fills existing attributes. - The preservation guard treats an all-empty-string attribute map (server enrichment failure) as effectively empty, so it no longer blocks the live-attribute fill.
Host join #
audioSetup()(mic capture, Bluetooth routing, setAttributes) now runs unawaited for all joins, including hosts withturnOnMicrophoneWhenJoining. The ordering is internal to the closure; the mic permission dialog now appears over the rendered room instead of behind a skeleton. The_opEpoch/cancelPendingPublishmachinery handles aleave()during setup.- Same-instance reconnect (
connect()re-dial) now drains pending mic publishes before disconnecting, closing theaddTransceivertrack-is-null window. - Device-info collection is bounded at 150 ms; a timeout sends nulls (the engine treats
all device fields as optional).
deviceIdstays fully awaited (single-session enforcement).
Reliability #
- Inter-attempt teardown in the retry loop is bounded at 2 s (was unbounded — the SDK's disconnect can block ~10 s per attempt on a half-dead link).
connectTimeoutraised 15 s → 25 s (deadlock backstop aligned with the SDK's 3 × 7 s internal phase budget). The reconnection handler's force-exit is suspended during a kit-driven connect so it cannot fire mid-retry.
Jank cluster #
- Payload
debugPrints (data messages, seat metadata) gated tokDebugMode. - The seat grid binds
participantRolesNotifier(cached, updated on participant/role changes) instead of theparticipantRolesgetter that jsonDecodes every participant's metadata on every rebuild. MediaQuery.sizeOf/devicePixelRatioOfreplaceMediaQuery.ofin the seat grid, skeleton, seat widget, and avatar — eliminates full-subtree rebuilds on keyboard frames.UTDSpeakingRingkeeps a structure-stable widget tree (AnimatedBuilder → SizedBox → DecoratedBox → Padding → child always), so the avatar subtree is never torn down and re-inflated on speaking transitions.
README #
- New "Fast first join" section documenting
warmUp()and the tap-time token prefetch pattern for apps that want the first entrance to be as fast as every later one.
1.8.0 #
Faster room entry — every serial cost on the join path was removed, overlapped, or cached:
- HTTP transport is now process-shared and survives room exit → re-entry, so rejoins reuse the
warm TLS connection to the engine instead of paying a fresh DNS+TCP+TLS handshake per join
(~60–200ms on mobile).
UTDApiClient.dispose()no longer closes the shared transport. - The token host's TLS connection and the device-id/device-info caches are warmed
fire-and-forget at
initApi()time, off the join's critical path. - New
UTDRoomController.warmUp()(static): apps can call it when a room entry becomes likely (e.g. the lobby screen opens) to pre-open the token-host connection, heat the device caches, and pre-warm DNS/TLS to the RTC edge — making the subsequent join near-handshake-free. - The RTC edge URL from each successful join is remembered (memory + SharedPreferences) and the next join pre-warms DNS/TLS to it in parallel with its token request, so the WSS dial resumes a TLS session instead of running a cold handshake (~30–150ms).
- Device-id and device-info lookups now run concurrently (and package/device platform channels inside the collector too), with in-flight memoization — a cold first join pays one platform hop instead of three.
- On a room switch, the previous room's teardown starts as soon as the new join begins and overlaps the token round trip instead of serializing ahead of the RTC dial (~50–300ms on switches). Note: the old room now starts closing even if the new join later fails.
- Flaky-network joins fail over faster: the SDK's signal phases are bounded at 7s (was 10s default, racing the kit's own 15s outer timeout), and the retry delay dropped 500ms → 200ms.
1.7.0 #
- Internalized the real-time transport layer so it is no longer part of the public API. The
low-level engine handle (
UTDRoomManager) is no longer exported, andUTDRoomController.roomManageris now private. Consumers use the neutral surface —UTDAudioRoom,UTDRoomController, and the seat/media/chat controllers — none of which expose transport-specific types.UTDConnectionStatemoved to its own file but is still exported unchanged. - Breaking (advanced API only): code that reached
controller.roomManageror the raw engine handle must migrate to the controller's public getters (e.g.localIdentity,connectionState,isConnected). The standard widget/controller usage is unaffected.
1.6.1 #
- Distinguish a not-activated service from a ban on the token endpoint. A non-ban
403(e.g.Type 'audio_room' is not enabled for this project) now throws the newUTDServiceNotAvailableExceptioninstead ofUTDBannedException. - The built-in connect-error view shows a distinct "not available" message and hides Retry
for that refusal (retrying can't help). When the host supplies no
onConnectError,UTDAudioRoomnow falls back to this error view instead of an endless connecting skeleton. AddsUTDRoomStrings.serviceNotAvailable(EN + AR).
1.6.0 #
- Auto-collect device facts for the dashboard's per-participant view.
UTDRoomController.generateTokennow populatesdevice_model/os/os_version/app_versionautomatically (viadevice_info_plus+package_info_plus) when the host app doesn't pass them. Explicit arguments still win, collection is cached per process, and it never throws — on an unsupported platform or a plugin failure each field degrades tonullrather than blocking token issuance. Values are capped to the engine's column limits (device_model 100, os 50, os_version/app_version 20). UTDRoomController.generateToken/UTDTokenApi.generateToken: add an optionalimageUrl, sent asimageonPOST /api/v1/tokenand shown as the participant's profile image in the dashboard.- New direct dependencies:
device_info_plus: ^12.0.0andpackage_info_plus: ^8.0.0(the former was already in the tree transitively via the real-time engine). - Minimum SDK raised to Dart
>=3.7.0/ Flutter>=3.29.0, required bydevice_info_plus ^12.0.0. Hosts on older toolchains should stay on1.5.0.
1.5.0 #
- Type-first token request: the kit now sends
type: 'audio_room'onPOST /api/v1/tokeninstead of the legacyservice: 'rooms'. The engine is type-first — oneappId/appKeyserves every product type enabled on the project, and the type is a per-request field, not a credential. The project must haveaudio_roomin its enabled types. Seat behavior is unchanged —audio_roomkeeps the full seat model (take/leave/switch/lock/unlock/kick/mute/swap, apply-to-speak, seat grid); only the token field changed. UTDRoomController.generateToken/UTDTokenApi.generateToken: the requiredserviceparameter is removed and replaced by an optionaltype(default'audio_room'). Drop-in users of theUTDAudioRoomwidget are unaffected — it no longer passesserviceinternally. Direct callers ofgenerateTokenshould dropservice: 'rooms'; the default already targetsaudio_room.- Non-breaking on the engine side: the deployed engine still accepts the legacy
service+kindfields and derives the canonical type, so older builds of this kit keep working against the same engine while apps migrate to this version at their own pace.
1.4.0 #
- No-backend credentials (recommended): pass
UTDAudioRoom(appKey: ...)/UTDRoomController.initApi(appKey: ...)— the project's publishable app key. The kit mints tokens directly from the engine (X-App-KeyonPOST /api/v1/token), and the engine signs the returned per-useruser_tokenwith the projectserver_secretserver-side, so the secret never ships in the app and no integrator backend is required. The kit applies thatuser_tokenas theAuthorization: Bearerfor all in-room/moderation calls (persisted acrossinitApire-inits, so it survives restore-from-minimize). - Removed
tokenProviderand itsUTDTokenProvider/UTDTokenRequest/UTDTokenBundletypes plus theUTDRoomController.usesTokenProvidergetter (added in 1.3.0). The no-backendappKeyflow replaces it — there is no longer a built-in path for integrators who run their own token backend. - Removed
serverSecretentirely (deprecated in 1.3.0).UTDAudioRoom.serverSecret,UTDRoomController.initApi(serverSecret:)/ itsserverSecretgetter, and theUTDApiClient(appSecret:)param /X-App-Secretheader are all gone.appKeyis now the sole, required credential. Migrate anyserverSecret:callsites toappKey:. - A leaked
app_keycannot forge bearers offline or call the server-to-server API, and rotates independently via the engineregenerate-credentialsadmin endpoint.
1.3.0 #
- Secure credentials via
tokenProvider(recommended): a newUTDAudioRoom(tokenProvider: ...)/UTDRoomController.initApi(tokenProvider: ...)callback lets the integrator mint tokens from their own backend, so the projectserverSecretnever ships in the app. The kit calls the provider with aUTDTokenRequest(identity, room, service, room owner, device id) and consumes the returnedUTDTokenBundle(UTDTokenBundle.fromEngineJsonparses the enginePOST /api/v1/tokenresponse verbatim). The per-useruser_tokenfrom the bundle is applied as theAuthorization: Bearerfor all in-room/moderation REST calls, so actions are authenticated as the server-verified user. New exported API:UTDTokenProvider,UTDTokenRequest,UTDTokenBundle, plusUTDRoomController.usesTokenProvider. The role is intentionally not sent from the client in this mode — the integrator backend is the authority on the user's role. serverSecretis now deprecated and optional.UTDAudioRoom.serverSecret/initApi(serverSecret:)still work in legacy/dual mode (the kit keeps sendingX-App-Secret), but embedding the secret in a shipped app lets anyone extract it and mint tokens for any identity/room — migrate totokenProvider.UTDAudioRoomnow asserts that exactly one oftokenProvider(recommended) orserverSecret(legacy) is provided.UTDTokenResponsegains auserTokenfield (engineuser_token; empty on legacy responses) so the per-user bearer is surfaced through the normal token flow as well.- README rewritten around the secure
tokenProviderflow, with a backend-proxy example and an explicit "do not ship the secret" warning; the params table marksappIdas a safe public identifier andserverSecretas deprecated/legacy.
1.2.0 #
- Comment lock: host/admin can now lock room chat so only host/admin may post.
New
UTDCommentApi(exported) wrapping the engine endpointsPOST /api/v1/rooms/{room}/comments/{lock,unlock}, plusUTDRoomController.lockComments()/unlockComments()/setCommentsLocked(), thecommentsLockednotifier and thecanICommentgetter. The lock is driven by the server (the_chat_lockbroadcast + thechat_lockedroom-metadata field), never set optimistically, so it stays consistent across devices and is restored for late joiners on reconnect. Enforcement lives in both the send path (a hidden composer can't be bypassed) and the receive path (chat from non-privileged senders is dropped while locked, as a backstop against a tampered client). The default controls bar gains a host/admin lock toggle and swaps the audience chat button for a lock indicator while locked. - Admin role announcements: a centered, dimmed room-chat system line is posted
when a user gains or loses the
adminrole (" UTDRoomController.stringslets the host app's localizedUTDRoomStringsback controller-emitted system lines; wired automatically fromUTDAudioRoomConfig. New strings (English + Arabic defaults) for the comment-lock UI and the admin/lock announcements; existing directUTDRoomStringscallers are unaffected (the new fields default to English).
1.1.0 #
- Split the API base URL by operation: token generation now uses the edge host
https://udt-stream.comwhile all in-room operations (seats, speakers, bans, roles) use the grey-cloud engine hosthttps://engine.udt-stream.com.UTDApiClient.defaultBaseUrlis now the engine host; the newUTDApiClient.defaultTokenBaseUrlis the token host.initApigained atokenBaseUrlparameter (defaulted) — existing callers need no change. - Security (M2): the client no longer self-writes cosmetic fields (avatar, frame,
color name) into RTC participant metadata. Cosmetics are published only
as participant attributes; the server remains the sole owner of metadata
(
role,_device, …). This removes a client-trust surface where a peer could spoofrole/VIP in broadcast metadata. No public API change — cosmetics still flow throughuserAttributes/setAttributes. Part of a coordinated rollout: the backend may then gatecanUpdateOwnMetadatato privileged roles only.
1.0.1 #
- Update the default API base URL to
https://api.udt-stream.com.
1.0.0 #
- Initial standalone release. Extracted from the Tempo-Live monorepo into its own package repository.
- Real-time audio room: a drop-in prebuilt live-audio-room solution.
- Seat management (take, leave, switch, lock, unlock, kick, mute, swap), built-in seat actions and moderation sheet, apply-to-speak request queue, member list with host actions, mic/speaker controls (Bluetooth-preferring routing).
- Real-time chat over the data channel (batching + dedup), tiered reconnection, minimize / Android OS Picture-in-Picture, 8 layout modes.