Skip to main content

voxera_audio_room_kit 2.2.2 copy "voxera_audio_room_kit: ^2.2.2" to clipboard
voxera_audio_room_kit: ^2.2.2 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 #

2.2.2 #

  • The package homepage points to voxerastream.com. Error links open voxerastream.com/docs/errors (voxera_room_core 1.1.1).
  • Requires voxera_media_client ^1.6.1 and voxera_room_core ^1.1.1.

2.2.1 #

  • The kit reports its real version to the engine (the X-Voxera-Kit header and the vx_kit attribute). 2.1.0 and 2.2.0 reported 2.0.0, so the engine could hold back features meant for newer kits.

2.2.0 #

  • VoxeraAudioRoomConfig.bannedDialog: the app's own dialog for a ban, a timed kick, a 403 on rejoin or a sign-in on another device, shown in place of the built-in one. The kit still leaves the room and pops its route first.

2.1.0 #

  • Every failure has a code from the Voxera error table (voxera_room_core 1.1, doc/errors.md there). A failed connect or microphone start prints one line such as [Voxera] E42901 RATE_LIMITED: Rate limit exceeded (retry in 30s) · https://voxera.app/docs/errors/42901 · req 3f2a… and is delivered on VoxeraErrors.stream.
  • Token errors are read from the engine's status_code, so a ban and a service that is not enabled are told apart by code rather than by message text. Engines that send no code still work as before. VoxeraBannedException, VoxeraRateLimitedException, VoxeraServiceNotAvailableException and VoxeraTokenException are now VoxeraErrors: each has code, requestId and docsUrl. A suspended project (41001), a revoked session (41002), an expired credit lease (41003) and the concurrent-session cap (42902) are VoxeraServiceNotAvailableExceptions.
  • onConnectError receives the VoxeraError (the same object for the token refusals above, a converted one for media and network errors, with the original in cause).
  • The built-in "Action failed" messages and the connect-error view end with the code, for example "Action failed. Please try again. (40902)". VoxeraErrors.showCodesInUi = false hides it. A failed in-room request is only noted: it is printed when the kit shows a failure, so the ones the kit treats as done (leaving a seat you are already off) stay quiet.

2.0.0 #

  • Built on the new voxera_room_core 1.0.0, which now holds the code both room kits had as two copies: connection state, reconnect, session resume, connect budget, MOS, mute strategy, token response and refresh, the token and ban exceptions, VoxeraJoinFailure, data frames, messaging, room music, the mini overlay, minimize, PiP, prefetch and VoxeraNoiseCancellation. The kit re-exports it, so imports don't change; an app that uses both kits now gets one type for each of these instead of two.
  • VoxeraRoomController implements VoxeraRoomSession, the interface both kits share (music, data frames, noise cancellation, minimize, PiP, leave, ...). VoxeraRoomSession.active is the room the user is in, from either kit.
  • One mini overlay for both kits. VoxeraMiniOverlayMachine records which room is minimized (minimizedSession), and controller.minimize.isMinimizing is now true only for that room, not for any controller while some room is minimized. Another room's leave, close or dismiss no longer hides its floating window. A room of the other kit stays in its window until this room connects. VoxeraMiniOverlayHost(builder:) shows whichever room is minimized and replaces per-kit overlay builders in the app.
  • Fix: closing a room screen while a different room was minimized left that screen's controller undisposed.
  • Breaking: VoxeraMinimizeController is generic (the controller's minimize is a VoxeraMinimizeController<VoxeraMinimizeConfig>), VoxeraMinimizeData.controller is a VoxeraRoomSession, VoxeraMinimizeConfig implements VoxeraMinimizeOptions, and VoxeraRoomPrefetch is a typedef of VoxeraSessionPrefetch<VoxeraRoomController>. Code that only uses these through the controller is unaffected.
  • Requires voxera_room_core ^1.0.0.

1.8.0 #

  • VoxeraAudioRoomConfig.backgroundMic and VoxeraRoomController.setBackgroundMic(...): VoxeraBackgroundMic.pauseWhileBackground stops an open mic from sending while the app is in the background and turns it back on when the app returns (an admin mute that landed meanwhile still holds); room playback continues. keepSending leaves it on. A closed mic gets the listener audio route either way. Null, the default, keeps today's behaviour: the kit does not touch the mic. The steps are serialized, and a newer transition or leave() cancels a stale one. A PiP window keeps the mic: one paused just before PiP showed comes back. Moved from the host app, with its tests; a quick background, foreground, background sequence no longer leaves the mic paused for good. micPausedForBackground reports the state.
  • OS Picture-in-Picture state now comes from voxera_media_client 1.5 (VoxeraPictureInPicture.changes) while PiP is armed, so the app no longer needs a MainActivity.onPictureInPictureModeChanged override and a channel calling pip.setInPip. setInPip still works for apps that keep that wiring; a repeated state is ignored.
  • Requires voxera_media_client ^1.5.0.

1.7.0 #

  • Credentials and hosts can be set once with VoxeraKit.configure(appId:, appKey:, tokenBaseUrl:, engineBaseUrl:) from voxera_media_client 1.4. VoxeraAudioRoom's appId, appKey, tokenBaseUrl and engineBaseUrl are now optional and nullable; initApi and warmUp take the same values as nullable. Each value comes from the argument, then VoxeraKit.configure, then the built-in default. Code that read these widget fields as non-null String needs a ! or a default.
  • New VoxeraAudioRoom.startPrefetch(...) mints the join token when the user taps a room and returns a VoxeraRoomPrefetch. Pass it to the widget's new prefetch: argument and the widget takes its controller and token. The request is the one the widget sends itself, so an app no longer copies it. The prefetch's token never throws (null means the widget mints its own); one no room takes is released with dispose(), which waits for the mint to settle.

1.6.0 #

  • VoxeraRoomController.initApi takes engineBaseUrl for the in-room host. baseUrl still works as its deprecated name (engineBaseUrl wins when both are passed). The live kit's initApi used baseUrl for the token host; both kits now take tokenBaseUrl and engineBaseUrl with the same meaning.
  • New VoxeraRoomController.isLocalHost, as in the live kit: true when this device is the room owner. Known once the room widget bound the room or a token was minted.
  • New VoxeraJoinFailure.classify(error) for onConnectError: rateLimited (with the engine's retryAfter), unavailable, banned or transient, plus isRetryable. The audio and live kits ship the same class, so an app no longer matches the exception types itself.
  • Removed the dependencies flutter_bloc, get_it, dartz and flutter_screenutil. No kit code imported them; they only limited the versions a host app could use.
  • device_info_plus accepts 12 and 13, package_info_plus 8 to 10. Their new majors changed build tooling only (AGP, Kotlin, win32, iOS 13), so a host app on them no longer needs a dependency_overrides entry.

1.5.1 #

  • Fix: VoxeraRoomController.music.start points the media client at the current controller's participant on every start. 1.5.0 kept the first controller's resolver (??=), so after the room was left and joined again an aux song failed against the disposed participant and the app fell back to the room-music track.

1.5.0 #

  • New VoxeraAudioRoomConfig.roomMusicMode (VoxeraRoomMusicMode.track by default, the released room-music track) and VoxeraRoomController.music (VoxeraRoomMusic): start, pause, resume, seek, stop, publish volume, playout volume, position, duration, onFinished, for both modes.
  • VoxeraRoomMusicMode.aux mixes the song into the microphone after echo cancellation (voxera_media_client RoomMusicAux, see its doc/room-music-aux-mix.md). While a song plays the mic button is the voice gain: self mute keeps the track live and the song going, keeps the Android call mode and the capture (no detach); the track is muted the normal way once the song stops. A song started with the mic off makes the uplink live with the voice at gain 0. A host seat mute silences voice and song; the seat unmute brings the song back with the mic off. Listeners cannot mute the music alone in this mode.
  • VoxeraAudioRoomConfig.auxMusicBitrate (48000 by default): the microphone's Opus bitrate while an aux song plays.
  • VoxeraAudioRoomConfig.seatUnmuteRestoresMusic (default false), track mode: a seat unmute unmutes the room-music track again, and its mute no longer flips the mic icon.
  • Mute icons refresh when a participant's vx_mic attribute changes.
  • Requires voxera_media_client 1.3.0 (RoomMusicAux).

1.4.0 #

  • New VoxeraAudioRoomConfig.earlyReconnectingState (default false), also VoxeraRoomController.setEarlyReconnectingState. With true, connectionState goes to reconnecting at the media SDK's first reconnect attempt (RoomAttemptReconnectEvent) and back to connected on RoomReconnectedEvent. With false, as before, it changes only for a full reconnect (RoomReconnectingEvent). Measured on a phone (Android 15) and the emulator: after a network cut the SDK started reconnecting 4-7 s later and the server dropped the user at about 20 s, but the kit said connected for 85-157 s after the cut.
  • The setting also turns on the media client's RoomOptions.resilientReconnect: a microphone that cannot be published again after a full reconnect (the server freed the seat) is dropped instead of leaving the kit stuck in reconnecting, and a network that comes back without a connectivity callback is seen within 2 s instead of after up to 70 s.
  • With true, the seat controller's 60 s force exit (onForceExit) counts from the cut, because it starts at reconnecting: a cut of more than about 60 s ends in a force exit, a shorter one in a seat resync when the room is back. While reconnecting the kit does not start a microphone publish.
  • Requires voxera_media_client 1.2.0 (RoomOptions.resilientReconnect).
  • New VoxeraAudioRoomConfig.noiseCancellation (VoxeraNoiseCancellation.off by default, standard or voiceFocus): AI noise cancellation of the local microphone on the device (Android, VoxeraAudioProfile.speech only; voxera_media_client AudioCaptureOptions.noiseCancellation). standard removes noise and keeps every voice; voiceFocus also lowers voices far from the phone (a TV) by 20 dB while the user is quiet. VoxeraRoomController.setNoiseCancellation switches it while the microphone is live, for a user-facing toggle; VoxeraRoomController.isNoiseCancellationSupported() tells whether to show it. The media diagnostics include its stats only while it is on. Needs voxera_media_client 1.2.0, which adds its native library to every app (about 2.4 MB per ABI, about 7 MB in a fat APK) even with noise cancellation off; an app can drop it with packaging { jniLibs { excludes += ["**/libvoxera_nc.so"] } }.

1.3.0 #

  • New VoxeraAudioRoomConfig.silenceGate (VoxeraSilenceGate.off by default, or standard): an open but silent microphone stops streaming room noise. A seated speaker with the mic open in a normal room sent ~49 packets/s and ~23 kbps of payload (about 22 MB/h up, and ~20 MB/h down for every other person in the room) even with nobody talking; with the gate those pauses go out as Opus DTX (about one packet per 400 ms). Applies to the speech profile only, Android only for now; off leaves the microphone path unchanged. VoxeraRoomController.setSilenceGate, VoxeraMediaController.silenceGateStats() and the audio diagnostics show the gate's open share, threshold and noise floor. Requires voxera_media_client with AudioCaptureOptions.silenceGate.
  • New VoxeraAudioRoomConfig.adaptiveRed (VoxeraAdaptiveRed.off by default, or standard): Opus RED for the local microphone, chosen at each publish from the packet loss this device measured. The quality reporter's readings are pooled over the last 2 minutes (lost / expected packets): this microphone's uplink once it was published in the session, before that the least lossy received stream (the others carry their senders' uplink loss). On at 2% or more, off below 0.5%; in between, or with fewer than about 1000 packets in the window (a mostly silent DTX mic), the previous choice holds. The choice and the loss are stored per media host (when they change, at most once a minute, and on leave; up to 24 h; a clock moved back invalidates them), so the next session's first publish starts from the path's last choice. RED (one redundant frame per packet) lets every listener recover a single lost packet. Measured on a phone (loss at the listener 2.9-8.5%): concealed audio while talking 2.80 / 5.65 / 7.17% -> 0.75 / 0.43 / 1.31%, concealment events per minute 80.5 / 154.2 / 189.7 -> 6.5 / 8.2 / 26.0; it sounded clearer with fewer cuts. It roughly doubles the payload of a talking stream (23.8 -> 49.4, 23.8 -> 48.9 and 20.7 -> 45.4 kbps; phone upload 22.5 -> 34 MB/h) for the speaker and every RED-capable listener, costs almost nothing in silence, and the packet count stays the same. A mic that is already published keeps its RED (no republish of a live mic). Speech profile only; an explicit red wins. VoxeraRoomController.setAdaptiveRed, VoxeraRoomController.redDecision (read-only VoxeraRedDecision), VoxeraMediaController.micRed (the RED of the mic as published) and the audio diagnostics show the choice. Without the quality reporter (no quality API or no user token) a debug warning is logged and the stored choice, or RED off, is used. Requires voxera_media_client with setMicrophoneEnabled(audioPublishOptions:).
  • Room messaging over the engine WebSocket (engine Phase 2.1). When the token response carries room_messaging (the project turned it on in the engine), chat (sendChatMessage), app messages (sendRoomMessage, e.g. gifts) and likes go through the engine's room socket instead of the data channel. The engine delivers app messages first, then host/admin chat, then chat, shows at most 20 chat lines per second per room, sends one frame per room per tick and sums likes into one counter. Incoming messages still arrive on dataStream / dataFrameStream with the same payloads; dataFrameStream carries the sender the engine verified.
  • Without room_messaging (older engines, projects that did not turn it on) nothing changes: no socket is opened and everything stays on the data channel.
  • The socket reconnects on its own; while it is down chat and app messages use the data channel. The engine also copies every room message to the data channel for kits that do not know room messaging yet, and the kit keeps one copy of a message that arrives both ways. A device never receives its own message back.
  • New: VoxeraRoomController.roomMessagingActive, likeCount, likeStream and sendLike(). sendRoomMessage now returns Future<bool>: false when the engine refused the message (too fast, room busy). VoxeraTokenResponse.roomMessaging carries the engine field.
  • A speaker whose connection dropped gets the mic back after the kit rejoins: the kit remembers the mic was on, waits (up to 10s) for the engine to grant publish again for the held seat, then reopens it. An admin mute is never overridden.
  • Every engine request now carries X-Voxera-Kit: <package>/<version> (for example voxera_audio_room_kit/1.2.3), and the kit adds a vx_kit participant attribute with the same value to the attribute update it already makes after connect (no update of its own). The engine stores it with the participant and reports the share of users per kit version, so features that need a newer kit (the audio Mixer of engine Phase 2.4) can wait until enough users run one. Engines that do not know the header ignore it; nothing else in the requests changes.
  • New: kVoxeraKitName, kVoxeraKitVersion, kVoxeraKitId (<package>/<version>), kVoxeraKitHeader and kVoxeraKitAttribute. An app whose backend mints the token (tokenProvider) can forward kVoxeraKitId as the X-Voxera-Kit header on that call.

1.2.3 #

  • A host/admin unmute no longer leaves the speaker locked until they re-enter the room. The seat snapshot's is_muted and the _force_mute message carry the same decision and can arrive in either order; a refresh that still read the old "muted" snapshot after the unmute message set the admin-mute flag again, and nothing cleared it (the live-track backstop cannot fire while the mic is held off). The kit now acts only on a CHANGE of the local seat's is_muted: muted -> unmuted lifts the lock, and a stale muted snapshot is ignored.
  • An unmute also forgets a mute carried by the join token, so a reconnect into the same room no longer mutes the user again.
  • VoxeraRoomController.adminUnmutedStream emits once per unmute (the engine and the moderator's device both send the message; only the one that clears the flag counts). It belongs to the controller, so it still fires while the room is minimized.
  • VoxeraAudioRoomConfig.showAdminMuteNotices (default true) turns the room's own unmute / muted-refusal snackbars off for apps that show their own.

1.2.2 #

  • VoxeraRoomController.dataFrameStream exposes the room manager's data frames: each message with its per-send id and the identity of the participant who sent it (null for an engine message). The identity comes from the transport, so an app can check it against the user a payload claims to come from. dataStream is unchanged.

1.2.1 #

  • Require voxera_media_client ^1.0.12. From that version the media SDK waits for the first media path (ICE + DTLS) with Timeouts.peerConnection instead of the connection timeout.
  • A first join gives the media path at least 15s (VoxeraConstants.rtcPeerConnectionTimeout), or the join's phase budget when that is larger. A network that blocks UDP has to reach the TURN relay over TLS, which regularly takes longer than the signaling phases; cutting it short failed the join, dialed again, and spent more of the user's TURN allocations. Reconnects keep the 10s media floor from 1.2.0. The backstop covers the longer phase (connectTimeout 25s -> 33s for the floor budget).
  • The redial pacer is process-wide. Apps create a new controller for every room entry, so a per-manager pacer was reset by the leave-and-enter-again loop it exists to slow down.
  • VoxeraRateLimitedException.retryAfter carries the wait the engine asked for (Retry-After or RateLimit-Reset), so apps can retry when the window opens instead of immediately.
  • VoxeraDeviceId resolves through the media client's process-wide VoxeraInstallId, which it shares with the live-room kit. Two kits looking the id up at once on a fresh install could otherwise keep two different ids.

1.2.0 #

  • Join timeout follows the network. Each connect phase used to get a fixed 7s; a slow join that was still making progress was cut and dialed again, which the server saw as a second session for the same person. The budget now starts from what this device needed before on the same media host (2.5x, at least 7s), grows on each retry, and stops at 20s. On a network that is fully down the first join now gives up after about 40s instead of about 24s (7s + 11.2s + 17.9s with the 1s and 2s pauses, against 3 x 7s before).
  • The larger join budget applies to first joins only. After a join the media SDK's own reconnect timeouts stay at 7s: the SDK waits out that timeout when a restarted node refuses its resume, so a larger value would only lengthen that wait.
  • A token the media node refuses (401/403) is no longer dialed two more times. The failure goes straight back to the caller, and the automatic rejoin mints a new token.
  • While the media SDK retries a dropped session, the kit fetches a fresh token at the 3rd attempt (and the 6th), after a random 0-2s wait so that a room dropped by a node restart does not send every token request in the same second, and hands its URL and token to the remaining attempts. A node that restarted with a new key, or a room the engine moved to another node, is reached in seconds instead of after the SDK's ten attempts (about a minute), without leaving the room.
  • initApi warms the token host (HEAD /warmz) and the last media host by default (autoWarmUp, default true). A token request that starts while that warm-up is in flight waits for it, at most 1.5s, and reuses its connection.
  • After a reconnect the seats are read once from GET /seats, in addition to the room metadata. _seat_update messages sent while the session was down are lost, and after a media node restart the room metadata starts empty. VoxeraSyncManager.startPeriodicSync no longer polls (deprecated): seats are pushed, not polled.
  • A healthy listener posts a quality report every 5 minutes instead of every minute. Publishers keep 20s, and a degraded session still reports after 10s.

1.1.3 #

  • Require voxera_media_client ^1.0.11. The kit turns stats polling off, which on earlier clients also turned off the Android silent-microphone recorder restart.

1.1.2 #

  • Fix a token request on every quality report after an hour in a room. A call refused for an expired user token now keeps the renewed token on its client, where before only the replayed call carried it.
  • initApi called again with the same settings keeps the existing API clients. The room screen calls it when it returns from minimize, which left the quality reporter and the seat controller on a replaced client that no token renewal reached.
  • RemoteReceiveLedger tracks publications disabled by a room mute or background pause, ensuring personal participant mutes survive room mute/unmute and background resume.
  • Default VoxeraAudioRoomConfig.muteStrategy to VoxeraMuteStrategy.server to reduce downlink bandwidth when speaker output is off.
  • VoxeraAudioProfile.speech defaults speech redundancy (RED) to false (one Opus copy).
  • Quality reporter uses sender RTT directly from stats and only queries transport RTT as fallback.
  • Expose currentMos and audioQuality notifiers on VoxeraRoomController, synchronized with the quality reporter and cleaned up on room leave and controller disposal.
  • Pass live mute state to seat widgets and optimize seat grid rebuilds when custom builders are used.
  • Use CachedNetworkImage for room cover art in Picture-in-Picture and minimized overlays.

1.1.1 #

  • VoxeraAudioRoomConfig.red overrides speech redundancy. Null keeps it on. False publishes one Opus copy.
  • Pause remote audio while the app is backgrounded, and apply that pause again after a reconnect, along with a room mute.
  • pauseUplinkKeepPlayback stops an open mic from sending in the background and leaves the communication audio session up so the room can still be heard.
  • Drop a video track that subscribes inside an audio room, and disable voice from someone who is no longer on a seat.
  • Depend on voxera_media_client ^1.0.8.

1.1.0 #

  • A room mode can set reservesHostSeat: false so seat 0 is an ordinary seat: no host styling, and the owner is offered "leave" rather than "step down". Defaults to true, so existing layouts are unchanged.
  • setupSeats takes an optional reserveHostSeat. It is sent only when set, so an older engine keeps reserving seat 0.
  • autoSeatHost now seats the owner on the host seat after connecting, a mode change, or a successful setup, and only while the current mode reserves a host seat. It does not run on an ordinary seat update, so an owner who steps down stays down.
  • Turning the microphone off when a seat snapshot no longer includes the local user, including when a smaller layout removes their seat.
  • Depend on voxera_media_client ^1.0.7.

1.0.7 #

  • Detach a muted microphone from its sender after 10 seconds, and right away on a background auto-mute, so a muted seat stops recording and running echo cancellation, noise suppression and AGC. Unmute reattaches the same track (LocalTrack.detachCaptureWhileMuted).
  • Turn off the media client's per-track stats poll (Track.statsMonitorEnabled). Nothing in the kit reads it, and it cost a native getStats() every 2 seconds for each speaker's track.
  • Fix minimize and PiP overlay navigation when unnamed dialogs or bottom sheets are open, preventing unintended bubble overlay over the active room and properly preserving room routes for restore.
  • Expose VoxeraRoomController.roomManager getter for participant and room state access.
  • Depend on voxera_media_client ^1.0.6.

1.0.6 #

  • Silence remote audio tracks that subscribe or activate after playback is muted, and prevent engine start() enables from overriding room mute.
  • Synchronize speaker state before disabling tracks to prevent mid-toggle audio leakage on route transitions.

1.0.5 #

  • Preserve speaker output preference when switching audio route away from loudspeaker on iOS.
  • Depend on voxera_media_client 1.0.5.

1.0.4 #

  • Add ordered background-listening and foreground-publishing audio transitions so host apps do not race microphone changes with native WebRTC routing.
  • Depend on the channel-safe voxera_media_client 1.0.4 and flutter_webrtc 1.6.2.

1.0.3 #

  • Publish the seat avatar as soon as the room connects, before microphone setup, and send it on the join token so a user who sits early is not left without a photo.

1.0.2 #

  • Sample WebRTC audio stats on report cadence: publishers sample every 20s, listeners every 60s, and degraded sessions every 10s. Avoid redundant seat grid rebuilds on unchanged audio levels.
  • Depend on voxera_media_client 1.0.3.
  • Update package license holder to Voxera Software.

1.0.1 #

  • The host writes the room admin roster with assignAdmin as soon as the admin list is known, and retries a failed grant so the first entry is not left without admin powers.
  • Depend on voxera_media_client 1.0.2.

3.7.0 #

Staying in the room 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.

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.

Re-establishing now follows a schedule: 1s, 2s, 4s … capped at 30s, with ±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 (VoxeraRedialPacer 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 user switching rooms never waits.

  • New: VoxeraReconnectPolicy (the schedule) and VoxeraRedialPacer (the enforcement).
  • Removed: VoxeraConstants.retryDelay. The delay is no longer a single number; VoxeraConstants.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 seat, role and moderation call in that window failing silently while the room 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, so a long visit never reaches the point where the engine starts refusing.

  • 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.
  • VoxeraTokenResponse.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 an audio cut 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.

Every quality report carries latency_ms #

Latency came from sender stats, which only exist while publishing — so listeners, most of any room, reported none: 3,711 reports a day on one project with the field empty, and latency is the first number anyone asks for when a room feels slow.

RTT is now read from the transport's selected ICE candidate pair, which exists whether or not the user publishes, with the sender's own RTT kept as a fallback and an RTCP-reported round trip behind that. Unknown latency is still 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.

VoxeraRoomController.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.6.1 #

Quality telemetry stops polling an endpoint that rejects every request.

Found in production logs: 995 rejected quality reports in a single day — 29% of all quality traffic — every one from a Flutter client sending no Authorization header at all.

The privileged in-room routes authenticate with a per-user bearer, and that bearer only lands when the host app hands its token response to adoptTokenResponse (apps using this kit's own generateToken get it automatically). An app that mints tokens on its own backend and skips that call had no bearer — so the reporter posted into a 401 every ten seconds, for the whole session, forever.

Two costs, both silent:

  • The telemetry was empty. Packet loss, jitter and RTT only exist client-side; if the report never lands, nobody has those numbers — which is exactly the data an integrator would go looking for when a room sounds bad.
  • Users paid for it. A request every ten seconds out of a phone's battery and metered data, with a guaranteed-zero result.

The reporter now checks for a bearer before starting, and logs one clear line naming the call to make instead of failing quietly on a timer. VoxeraApiClient.hasUserToken is exposed so an integrator can assert the same thing in their own tests.

If you mint tokens on your own backend, check your integration — the bearer gates every privileged route, not just this one: kick, ban, role changes and seat management go through the same door.

3.6.0 #

An hour in an audio room now costs a listener about half what it did.

This is the room type most users are in, on metered data, for hours at a time — and two things were costing them for nothing. Neither was visible from the code, because both were engine defaults this kit simply never set.

Audio publishes as speech, not music #

Unset, the engine falls back to its 48 kbps music preset. That is what this kit shipped — for a room that carries nothing but people talking. Opus carries a voice at 24 kbps mono with no audible loss.

In a room with eight speakers this is the difference between roughly 200 MB and 100 MB an hour of a listener's mobile data.

DTX (stop sending during silence, which a seat room mostly is) is on by default and is now set explicitly, so a default moving underneath us fails a test instead of quietly doubling everyone's data.

Remote video is never subscribed #

autoSubscribe defaults to true and cannot filter by kind, so this kit subscribed to any video track anyone published into an audio room — allocating a transceiver and a decoder for a picture it has no widget to render. Video subscriptions are now dropped the moment they appear.

This helps the sender too: the SFU stops forwarding a layer nobody is subscribed to, so refusing the subscription also stops the upload.

Turning autoSubscribe off wholesale would have taken the audio with it — which is the entire room. A test pins that distinction so nobody reaches for the blunt fix later.

The camera trap is closed #

VoxeraMediaController exposes setCameraEnabled / toggleCamera and any host app can call them, but this kit set no capture or publish options. A camera turned on inside an audio room would have published the engine default — 720p at 30 fps with a full simulcast ladder, about 2.3 Mbps. It is now capped at 180p / 15 fps / 120 kbps with simulcast off, and uses hardware H.264 rather than software VP8.

No UI in this kit turns a camera on. This is a ceiling, not a feature.

Migration #

Nothing to change. If your app deliberately published or rendered video inside an audio room, it will no longer receive remote video here — use the live room kit for that.

3.5.0 #

Crowded rooms cost what a small room costs. Every participant event — a join, a leave, an avatar change — used to rebuild the whole room: jsonDecode over every participant's metadata, a new object and a copied attribute map for each. One person walking in paid for all 300. Filling a room to 300 cost 1+2+…+300 ≈ 45,150 operations, quadratic in room size and paid in bursts exactly while the room filled. The log line itself called participants.length, so every event paid it twice.

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

fill a room of 300     45,150 → 300 decodes, 45,150 → 300 builds
one avatar change           300 → 0 decodes
mute state         ~1,000 reads/sec → 22 on a mic event
  • Mic state is event-driven. The 300ms sweep of every participant is gone; the index rides TrackMuted/TrackUnmuted (already being received) and tracks only the identities whose mute icon is actually rendered — the local user and the seat occupants. A 3s recovery pass over those same identities covers a dropped event, and it does not run while disconnected or backgrounded.
  • Seat enrichment is a lookup. It scanned the full remote list once per occupied seat — 22 × 300 ≈ 6,600 comparisons per seat rebuild.
  • The seat grid stops repainting for changes that do not affect it. The roles map keeps its identity unless a role really changed, so an avatar change or a mic toggle no longer repaints 22 tiles.
  • Bursts collapse. 50 people arriving at once notify listeners once, not 50 times.
  • 169 hot-path log calls no longer run in release. debugPrint is not stripped from release builds; voxeraLog takes the message as a closure, so the string is never even built outside debug.

Public API unchanged.

3.4.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.

  • voxera_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.

3.3.0 #

Invitation windows + stale-snapshot guard. No breaking changes.

  • Speaker invitations can expire. An ignored invitation used to stay open forever. VoxeraAudioRoomConfig.invitationTimeout (or expiresIn on a single inviteToSpeak) sets how long it stays acceptable — a product decision, so the app owns it; the engine clamps it to 10s..10min, refuses a late accept, and tells BOTH sides when the window closes. The built-in dialog closes itself at the deadline instead of offering a button that can only fail, and onSpeakerInvitationExpired reports it. Default stays null = no expiry, so upgrading changes nothing until the app opts in.
  • A room no longer flips to the default layout right after rendering. Seat snapshots do not arrive in order — the token response renders first, then the SDK serves the _seats metadata it read at connect, which is older. Every snapshot now carries a monotonic revision and anything older than what is already applied is dropped. Snapshots from an engine that sends no revision still apply.

3.2.0 #

Batch seat locking + invitation-flow repair. No breaking changes.

  • lockAllSeats() / unlockAllSeats() (controller + VoxeraSeatApi) — host/admin locks or reopens every seat in ONE atomic server call with one broadcast. The engine owns the skip rules: an occupied seat keeps its speaker, an already-locked seat is not re-locked, the reserved host seat is never touched; unlock-all leaves open seats alone. Returns the number of seats actually changed.
  • Re-inviting to the mic works. A pending invitation used to answer 409 and permanently block the host from re-inviting anyone who missed the dialog (app backgrounded, message dropped) until that user left the room. The engine now SUPERSEDES the pending invitation — same id, new seat/inviter, the dialog message is re-sent.
  • An invitation accept that cannot seat you is no longer silent. The old flow swallowed every failure, blind-fell-back to takeSeat, and opened the mic unconditionally. Now: the mic opens only once a seat is actually held, an expired invitation (404) or lost seat (409) surfaces through the new onInvitationFailed callback (VoxeraInvitationFailure.expired / .seatUnavailable / .unknown), and the default widget shows a localized snack (invitationExpired / invitationSeatTaken strings, EN + AR).

3.1.0 #

New moderator-list API + bundled fixes. No breaking changes.

  • VoxeraRoomController.roomAdmins — a render-ready list of the moderators currently in the room: host first, then admins, each carrying the app's own published attributes (avatar, frame, level, any custom keys) plus name, role, isHost and an avatarUrl convenience. Built live from room state — no server call; refresh on roleChangesStream / participantsStream.
  • The volume button now actually mutes the room. Under the media profile neither platform routes a room to the earpiece, so speaker-off was a no-op with a lying icon. It now silences all remote audio (and keeps silencing newly joining speakers), clears on a room switch, and matches the live kit's long-standing semantic.
  • The media audio profile is claimed at controller construction — the native engine is built once per process by the first thing that touches it, so the kit now claims the profile at the earliest entry point instead of at connect. See the new "Audio engine setup" README section: apps should also call VoxeraMediaClient.initialize(mediaAudioProfile: true) first thing in main().
  • pub.dev now lists the supported platforms (Android, iOS) explicitly.

3.0.3 #

iOS listener audio fix — no breaking changes. Update from 3.0.2 is required on iOS 26.

  • An iPhone listener on iOS 26 hears the room again. 3.0.2's listener recipe asked for a playback session WITH allowBluetoothA2DP + allowAirPlay — options AVAudioSession only accepts on playAndRecord. iOS 26 rejects the whole setCategory call (OSStatus -50), so the session was never configured or activated: audio arrived (device telemetry showed ~40 kbps Opus, zero packet loss) and the device played none of it. The options are now gone — on an output-only category Bluetooth A2DP and AirPlay route implicitly, so nothing is lost. The options set stays explicitly empty so WebRTC's own playAndRecord-only defaults cannot leak into the playback session.
  • New guard test: every recipe's options are checked against what AVAudioSession actually accepts for its category, for every track state — not just the one option that bit us before.

3.0.2 #

iOS audio session fix — no breaking changes.

  • An iPhone listener can hear the room again. On iOS the kit forced the audio session through flutter_webrtc's setSpeakerphoneOnButPreferBluetooth, which sets WebRTC's own call profile — playAndRecord + voiceChat + setActive — on every join, listeners included, discarding the media session this kit builds. playAndRecord needs microphone permission, so a listener who never granted it had the session fail to activate and heard NOTHING; going up on the mic made it work, which is exactly how it was reported.
  • Coming off the mic no longer leaves the room on call audio. The same call left the session in voiceChat, the telephony mode: earpiece routing at call volume. The media recipe (playback for listeners, playAndRecord + videoChat + defaultToSpeaker while publishing) now owns the session on iOS, and Bluetooth comes from its own per-state options.
  • Android is unchanged: under MODE_NORMAL that helper is what re-enables device selection, so it still runs there — now behind a single guarded call site so it cannot drift back onto the iOS path.
  • Joining a room no longer inherits "sound off" from the previous one. The controller is reused across rooms (a room switch, restore-from-minimize) and isAllRemoteAudioMuted survived the move. It is re-applied to every newly subscribed remote mic track, so the user joined a new room and heard NOBODY, on any platform, with nothing on screen to explain it. An admin's mute (mutedByAdmin) leaked the same way and kept refusing the user's own unmute in a room its moderator never touched. Both are now cleared when the room actually changes — a reconnect into the same room still keeps what was set there.
  • The seat sheet no longer offers Lock on a reserved seat. The engine refuses any lock on a seat with reserved_for set (403, "Cannot lock a reserved seat"), and the room owner's seat always has it — so an admin tapping Lock there always failed. Measured on production: 145 refused locks in a day across seven projects, all on seat 0. Unlock stays available, since a seat locked before it was reserved still has to be releasable.

3.0.1 #

Seat fixes — no breaking changes.

  • Leaving a seat no longer fails when the server already freed it. The engine answers POST /seats/leave with 404 Not seated whenever the seat is already free — after a reconnect (the participant_left webhook frees it), after the server-side seat reconciler, or on a second tap. The kit reported that as a failure, so the host app showed an error AND kept the stale seat on screen: the user tapped again, failed again, and could not get off the seat. Measured on production before the fix: 224 of 948 leave calls (24%) answered
    1. It is now treated as the goal state, and the grid resyncs.
  • Changing a seat no longer fails on a no-op. 409 Already on this seat (a double tap, or a broadcast not yet applied) is likewise treated as success with a resync. The other 409s — Seat is locked, Target seat was taken — still fail, since those are refusals the user needs to see.

3.0.0 #

Admin join fix — admins connect as admin from the first frame — plus built-in audio quality telemetry.

  • Quality telemetry (new). The kit now samples WebRTC getStats() every 15s and posts one audio snapshot per interval to the engine (POST /api/v1/quality): bitrate, packet-loss fraction, jitter, and RTT while publishing. This feeds the engine's per-room quality dashboard (SLA/MOS), which had no media-side data source before. Zero configuration, entirely best-effort (a failed report is dropped, never retried), and the payload is a few hundred bytes per interval.

  • The admin role is now resolved BEFORE the token is minted. When the kit mints its own token and the best-known role would be audience, the join awaits adminIdsResolver (capped at 5s) and mints the token with the resolved role. The engine grants server-side RTC permissions at issuance — no post-connect upgrade, no reconnect. A resolver slower than the cap (or throwing) falls back to an audience join instead of blocking on a degraded backend.

  • Removed the post-connect self-upgrade + silent reconnect. The old path (join as audience, then PUT /participants/{self}/role claiming the owner as actor, then a full token re-issue + reconnect when that failed) could never succeed under server-signed bearer auth — the engine ignores the client-asserted actor — so every admin join ended in a forced reconnect: an audible audio cut for the admin and a join/leave flicker for the whole room. Measured on production before the fix: ~5,200 forced reconnects per day.

  • BREAKING — VoxeraRoomController.upgradeSelfRole is removed. Only breaking if you called it directly; the widget no longer needs it and there is no legitimate client-side path for a participant to change its own role. changeRole (owner-only) is unchanged.

  • If your app passes a tokenProvider (server-signed setups): the resolver is not consulted — your backend decides the role when it mints the token, so make sure it mints admins with role: 'admin' directly.

2.1.0 #

Seat-layout occupant policy — no breaking changes.

  • setupSeats now accepts occupantPolicy ("clear" | "retain" | "compact"): control what happens to current speakers when the seat layout or count changes. clear (default, and the behaviour when omitted) takes everyone off the seats; retain keeps everyone whose seat index still exists and removes only those whose seat disappeared on shrink; compact additionally slides displaced speakers onto free eligible seats within the new count, removing only those left without a seat. Seat 0 stays reserved for the room owner and is never a slide destination for anyone else.
  • Requires an engine build from 2026-08-21 or later for the new values; older engines return 422 for them. Omitting the parameter keeps the old behaviour on every engine version.

2.0.2 #

Speaker audio quality fix — no breaking changes.

  • Echo cancellation / noise suppression / auto gain control now actually run for on-seat speakers. The media-mode setup used to flip the engine's bypassVoiceProcessing switch, which (besides picking the media session profile) also forced every capture constraint off — so speakers published a raw, unprocessed microphone: echo of the room mix, background noise, no gain levelling. The kit now uses voxera_media_client 2.8.0's mediaAudioProfile engine mode: the session/routing behaviour is unchanged (media volume, loudspeaker, no OS "in call" state, no OS voice-processing unit) while WebRTC's software AEC/NS/AGC stay enabled on the mic.
  • Requires voxera_media_client ^2.8.0.

2.0.1 #

Battery/network fixes — no breaking changes.

  • Background video pause. The app-lifecycle watcher now also disables any remote video publications while the app is backgrounded and re-enables them on resume (a no-op in a pure audio room — it only matters when a host app runs stage/guest video through this kit). Audio keeps playing in the background by design, and Android OS Picture-in-Picture keeps its video.

2.0.0 #

Includes every fix from the 1.11.0–1.14.0 maintenance line, ported onto the new engine:

  • Audio rooms no longer put the phone "in a call" — the room runs in the MEDIA/playback profile (VoxeraAudioMode.enableMediaMode(), installed before every connect): media volume, loudspeaker default, no system in-call state, other apps (e.g. WhatsApp voice notes) keep microphone access. Bluetooth routing re-asserts the media config instead of re-arming the call profile.
  • Self-mute vs admin-mute are now distinct — new VoxeraMediaController.mutedByAdmin notifier, set only by a host/admin _force_mute; self-unmute is refused while it holds; applyAdminUnmute() clears it. mutedParticipants unchanged (still track-derived, for badges).
  • No more system chat announcements for comment lock/unlock and admin role changes (they rendered as broken bubbles in custom message widgets). The announceRoleChanges flag and the related strings are gone.
  • Live cosmetic updates — seat avatars/frames repaint mid-session via registerCosmeticKeys + reliable _cosmetic_update fan-out, instead of waiting for a (unreliable) SFU attribute echo.
  • Android OS Picture-in-Picture is actually armed on connect/reuse (pip.armIfEnabled()), swapping to the compact VoxeraPipView in PiP.

BREAKING — new media engine generation #

  • The kit now runs on voxera_media_client (the Voxera 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 #

  • VoxeraRoomController.adoptTokenResponse(VoxeraTokenResponse) — 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 (seat/speaker/ban/role/comment) and remembers the RTC edge URL for prewarm — exactly what generateToken does as side effects. Idempotent.
  • VoxeraAudioRoom.tokenProvider now supports externally-minted tokens. The widget adopts the provider's response on its own controller, so a server-signed token authenticates ALL in-room operations (previously only the media connect worked and moderation calls hit 401). The constructor assert that required the provider to come from generateToken on the same controller is removed.

1.10.1 #

Bugfix.

Speaking ring #

  • VoxeraSpeakingRing no 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 default VoxeraSeatWidget occupied-seat Stack is now Clip.none so 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 VoxeraSeatLayout (avatarRatio, rowPadding, rowSpacing, seatScale), passed via VoxeraAudioRoomConfig.seatLayout. The mounted room publishes it to a shared source so every seat-size consumer (grid, skeleton, computeSeatSize call 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 activeSpeakers instead of a 1s polling Timer — 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 a RepaintBoundary, 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 VoxeraRoomController.primeSeats: newer engines embed a seats snapshot in the token response (zero extra round trips); against older engines the kit fires GET /seats in 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 #

  • VoxeraDefaultAvatar now uses cached_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.devicePixelRatioOf replaces the full MediaQuery.of to avoid rebuilding every avatar on each keyboard-inset animation frame.

Seat updates #

  • Seat mutation responses from newer engines carry the synced seat state inline; the kit applies it immediately (applyMutationResponse, generation- and room-guarded) so the actor sees changes without waiting for the _seat_update broadcast round trip.
  • Blank occupant name/avatar self-heals from the occupant's live RTC participant attributes (_withLiveAttributes + a participantAttributesChanged repair 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 with turnOnMicrophoneWhenJoining. The ordering is internal to the closure; the mic permission dialog now appears over the rendered room instead of behind a skeleton. The _opEpoch/cancelPendingPublish machinery handles a leave() during setup.
  • Same-instance reconnect (connect() re-dial) now drains pending mic publishes before disconnecting, closing the addTransceiver track-is-null window.
  • Device-info collection is bounded at 150 ms; a timeout sends nulls (the engine treats all device fields as optional). deviceId stays 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).
  • connectTimeout raised 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 to kDebugMode.
  • The seat grid binds participantRolesNotifier (cached, updated on participant/role changes) instead of the participantRoles getter that jsonDecodes every participant's metadata on every rebuild.
  • MediaQuery.sizeOf / devicePixelRatioOf replace MediaQuery.of in the seat grid, skeleton, seat widget, and avatar — eliminates full-subtree rebuilds on keyboard frames.
  • VoxeraSpeakingRing keeps 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). VoxeraApiClient.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 VoxeraRoomController.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 (VoxeraRoomManager) is no longer exported, and VoxeraRoomController.roomManager is now private. Consumers use the neutral surface — VoxeraAudioRoom, VoxeraRoomController, and the seat/media/chat controllers — none of which expose transport-specific types. VoxeraConnectionState moved to its own file but is still exported unchanged.
  • Breaking (advanced API only): code that reached controller.roomManager or 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 new VoxeraServiceNotAvailableException instead of VoxeraBannedException.
  • 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, VoxeraAudioRoom now falls back to this error view instead of an endless connecting skeleton. Adds VoxeraRoomStrings.serviceNotAvailable (EN + AR).

1.6.0 #

  • Auto-collect device facts for the dashboard's per-participant view. VoxeraRoomController.generateToken now populates device_model / os / os_version / app_version automatically (via device_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 to null rather than blocking token issuance. Values are capped to the engine's column limits (device_model 100, os 50, os_version/app_version 20).
  • VoxeraRoomController.generateToken / VoxeraTokenApi.generateToken: add an optional imageUrl, sent as image on POST /api/v1/token and shown as the participant's profile image in the dashboard.
  • New direct dependencies: device_info_plus: ^12.0.0 and package_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 by device_info_plus ^12.0.0. Hosts on older toolchains should stay on 1.5.0.

1.5.0 #

  • Type-first token request: the kit now sends type: 'audio_room' on POST /api/v1/token instead of the legacy service: 'rooms'. The engine is type-first — one appId/appKey serves every product type enabled on the project, and the type is a per-request field, not a credential. The project must have audio_room in its enabled types. Seat behavior is unchanged — audio_room keeps the full seat model (take/leave/switch/lock/unlock/kick/mute/swap, apply-to-speak, seat grid); only the token field changed.
  • VoxeraRoomController.generateToken / VoxeraTokenApi.generateToken: the required service parameter is removed and replaced by an optional type (default 'audio_room'). Drop-in users of the VoxeraAudioRoom widget are unaffected — it no longer passes service internally. Direct callers of generateToken should drop service: 'rooms'; the default already targets audio_room.
  • Non-breaking on the engine side: the deployed engine still accepts the legacy service+kind fields 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 VoxeraAudioRoom(appKey: ...) / VoxeraRoomController.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 VoxeraTokenProvider / VoxeraTokenRequest / VoxeraTokenBundle types plus the VoxeraRoomController.usesTokenProvider getter (added in 1.3.0). The no-backend appKey flow replaces it — there is no longer a built-in path for integrators who run their own token backend.
  • Removed serverSecret entirely (deprecated in 1.3.0). VoxeraAudioRoom.serverSecret, VoxeraRoomController.initApi(serverSecret:) / its serverSecret getter, and the VoxeraApiClient(appSecret:) param / X-App-Secret header are all gone. appKey is now the sole, required credential. Migrate any serverSecret: callsites to appKey:.
  • 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.3.0 #

  • Secure credentials via tokenProvider (recommended): a new VoxeraAudioRoom(tokenProvider: ...) / VoxeraRoomController.initApi(tokenProvider: ...) callback lets the integrator mint tokens from their own backend, so the project serverSecret never ships in the app. The kit calls the provider with a VoxeraTokenRequest (identity, room, service, room owner, device id) and consumes the returned VoxeraTokenBundle (VoxeraTokenBundle.fromEngineJson parses the engine POST /api/v1/token response verbatim). The per-user user_token from the bundle is applied as the Authorization: Bearer for all in-room/moderation REST calls, so actions are authenticated as the server-verified user. New exported API: VoxeraTokenProvider, VoxeraTokenRequest, VoxeraTokenBundle, plus VoxeraRoomController.usesTokenProvider. The role is intentionally not sent from the client in this mode — the integrator backend is the authority on the user's role.
  • serverSecret is now deprecated and optional. VoxeraAudioRoom.serverSecret / initApi(serverSecret:) still work in legacy/dual mode (the kit keeps sending X-App-Secret), but embedding the secret in a shipped app lets anyone extract it and mint tokens for any identity/room — migrate to tokenProvider. VoxeraAudioRoom now asserts that exactly one of tokenProvider (recommended) or serverSecret (legacy) is provided.
  • VoxeraTokenResponse gains a userToken field (engine user_token; empty on legacy responses) so the per-user bearer is surfaced through the normal token flow as well.
  • README rewritten around the secure tokenProvider flow, with a backend-proxy example and an explicit "do not ship the secret" warning; the params table marks appId as a safe public identifier and serverSecret as deprecated/legacy.

1.2.0 #

  • Comment lock: host/admin can now lock room chat so only host/admin may post. New VoxeraCommentApi (exported) wrapping the engine endpoints POST /api/v1/rooms/{room}/comments/{lock,unlock}, plus VoxeraRoomController.lockComments() / unlockComments() / setCommentsLocked(), the commentsLocked notifier and the canIComment getter. The lock is driven by the server (the _chat_lock broadcast + the chat_locked room-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 admin role ("
  • VoxeraRoomController.strings lets the host app's localized VoxeraRoomStrings back controller-emitted system lines; wired automatically from VoxeraAudioRoomConfig. New strings (English + Arabic defaults) for the comment-lock UI and the admin/lock announcements; existing direct VoxeraRoomStrings callers 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://stream.zavryn.online while all in-room operations (seats, speakers, bans, roles) use the same Voxera API host. VoxeraApiClient.defaultBaseUrl is now the engine host; the new VoxeraApiClient.defaultTokenBaseUrl is the token host. initApi gained a tokenBaseUrl parameter (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 spoof role/VIP in broadcast metadata. No public API change — cosmetics still flow through userAttributes/setAttributes. Part of a coordinated rollout: the backend may then gate canUpdateOwnMetadata to privileged roles only.

1.0.1 #

  • Update the default API base URL to https://stream.zavryn.online.

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.
0
likes
140
points
1.23k
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Real-time live audio room for Flutter: seat management, real-time chat, mic/speaker controls, speak requests, moderation, and minimize/PiP.

Homepage

Topics

#audio #voice-chat #webrtc #realtime

License

MIT (license)

Dependencies

cached_network_image, device_info_plus, dio, equatable, floating, flutter, flutter_webrtc, package_info_plus, permission_handler, pretty_dio_logger, shared_preferences, voxera_media_client, voxera_room_core

More

Packages that depend on voxera_audio_room_kit