Skip to main content

voxera_audio_room_kit 2.2.0 copy "voxera_audio_room_kit: ^2.2.0" to clipboard
voxera_audio_room_kit: ^2.2.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.

Voxera Audio Room Kit #

A Flutter package for building live audio-room experiences, powered by a real-time audio engine and the Voxera Engine. It is a drop-in prebuilt live-audio-room solution: give it the required data and you get a fully interactive room — seats, chat, mic/speaker controls, seat actions, member list, speak requests, and moderation — with no extra UI code.

Features #

  • Drop-in room UI — a complete, interactive room from required data alone.
  • Seat management — take, leave, switch, lock, unlock, kick, mute, swap.
  • Built-in seat actions — tap a seat to take/leave it; host/admin get a moderation sheet (mute, kick, lock, invite).
  • Speak requests — audience apply-to-speak; host approve/reject queue with a live badge.
  • Member list — participants with host actions (mute, kick, invite, ban, promote/demote).
  • Media controls — microphone & speaker with reactive state (Bluetooth-preferring routing).
  • Real-time chat — data-channel messages with batching and dedup.
  • Reconnection — tiered: light sync (<15s), full sync (<60s), default force-exit (>60s).
  • Minimize / PiP — minimize to a floating overlay; optional Android OS Picture-in-Picture.
  • Theming & i18n — VoxeraRoomTheme (colors) + VoxeraRoomStrings (EN/AR built in) without replacing widgets.
  • Full customization — replace any section (header, messages, controls, background, seats) with your own widgets.

Installation #

dependencies:
  voxera_audio_room_kit: ^2.2.0

Audio engine setup (required) #

The native media engine is built once per process, and the first thing that touches it decides its audio profile. The room kit calls this itself. If your app touches WebRTC before the first room opens, call it first in main():

import 'package:voxera_media_client/voxera_media_client.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await VoxeraMediaClient.initializeForRoom();
  runApp(const MyApp());
}

That keeps echo cancellation on. Listeners stay on a media session. Publishing a microphone uses the communication session and the loudspeaker. Do not call initialize(mediaAudioProfile: true) — that turns echo cancellation off for the rest of the process. If you ever see [VoxeraMediaClient] the media engine was already initialized in the console, something ran before this line; report it to us with the log.

Platform setup #

iOS — add to ios/Runner/Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>Required for audio room participation</string>

For background audio + PiP, enable Background Modes → Audio, AirPlay, and Picture in Picture in Xcode.

Android — add to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

Set minSdkVersion to at least 21 (31+ for OS PiP).

Credentials — pass your appKey (no backend required) #

Ship appId + appKey (the project's publishable app key, from the Voxera console). The kit mints tokens directly from the engine with X-App-Key; the engine signs the returned per-user user_token with the project server_secret server-side, so the secret never ships in the app. The kit then uses that user_token as the Authorization: Bearer for all in-room calls. No token server to stand up.

appKey is not the server_secret: it can't sign tokens or call the server-to-server API, and it can be rotated on its own from the console. Treat a leak as a project-scoped incident (rotate + ship a new build).

Type-first engine. The engine groups products by a per-request type, not by credential — one appId/appKey works for every type enabled on the project. This kit always mints type: 'audio_room' on POST /api/v1/token; the project just needs audio_room in its enabled types (set per project in the Voxera console). The seat model below is the audio_room model — unchanged. (Live video broadcasts are a separate live_stream type served by the standalone live kit; this package does not use it.)

Quick start (drop-in, no backend) #

import 'package:voxera_audio_room_kit/voxera_audio_room_kit.dart';

VoxeraAudioRoom(
  appId: '<voxera-app-id>',
  appKey: '<voxera-app-key>', // publishable; the kit mints tokens itself, no backend needed
  userId: 'user123',
  userName: 'John Doe',
  roomId: 'room456',
  roomOwnerId: 'owner789', // userId == roomOwnerId ⇒ this user is the host
);

The kit defaults token and in-room requests to https://stream.zavryn.online. Override tokenBaseUrl and engineBaseUrl only for a different deployment.

With this, the package renders: a dark background, the seat grid (avatars + names), a header (minimize + exit), the chat list, and a role-aware controls bar. Tapping a seat takes/leaves it; audience can apply to speak; host/admin get member-list, the speak-request queue, and per-seat moderation. A connect failure shows an error + retry instead of an endless spinner.

The host is detected as userId == roomOwnerId. Admin membership comes from the engine's persistent room roster; adminIds is retained only for source compatibility. By default a host/admin joins mic-on (autoHostMic) and is auto-seated on hostSeatIndex if the backend didn't seat them (autoSeatHost).

The room body and seats render as soon as the token arrives — the RTC connection finishes in the background. That makes the token round trip the only thing standing between the tap and a populated room, so two one-time integrations make the FIRST entrance of a session as fast as every later one:

1. Warm up when a join becomes likely (e.g. the rooms lobby opens). The very first request of a process otherwise pays DNS + TLS to the token host inline (~100–300 ms on mobile networks), plus cold device caches:

// Fire-and-forget; safe to call repeatedly (everything is cached).
VoxeraRoomController.warmUp();

2. Prefetch the token at tap time, so the round trip overlaps the route navigation instead of starting after the room screen mounts:

final controller = VoxeraRoomController()
  ..initApi(appId: appId, appKey: appKey)
  ..registerModes(myModes);
final isHost = userId == roomOwnerId;
final seatParams = VoxeraAudioRoom.resolveTokenSeatParams(
    layoutMode: layoutMode, isHost: isHost, modes: myModes);
final tokenFuture = controller.generateToken(
  identity: userId, roomName: roomId, roomOwnerId: roomOwnerId,
  role: isHost ? 'host' : 'audience', name: userName,
  seatCount: seatParams.seatCount, seatMode: seatParams.seatMode,
  hostSeat: seatParams.hostSeat, modeId: seatParams.modeId,
);
// ...navigate, then pass BOTH to the widget:
VoxeraAudioRoom(
  // ...,
  controller: controller,
  tokenProvider: () => tokenFuture,
);

Avatars are disk-cached after the first download, so only a fresh install pays for them once. Serve resized CDN thumbnails (e.g. ?w=128) in the avatar URLs — the kit caps the decode size but cannot shrink the bytes on the wire.

Constructor #

Param Required Description
appId ✅ Voxera Engine app id (public identifier, safe to ship)
appKey ✅ Publishable app key — the kit mints tokens directly from the engine (no backend).
userId ✅ Local user identity
userName ✅ Display name
roomId ✅ Room to join/create
roomOwnerId ✅ Owner identity (host detection + owner-only actions)
adminIds – Deprecated compatibility input; use controller.assignAdmin
layoutMode – Mode id to use (default '3' = 9 seats); see Layout modes
modes – Custom VoxeraRoomModes to register
config – VoxeraAudioRoomConfig (see below)
controller – Reuse an existing VoxeraRoomController (e.g. after minimize)
onControllerReady – Get the controller (wire navigatorKey, minimize, etc.)
onConnectionChanged – (bool isConnected)
onSeatTap – Override the built-in seat action sheet
onSeatChanged – (List<SeatState> seats)
onConnectError – Override the built-in connect-error view

Configuration (VoxeraAudioRoomConfig) #

VoxeraAudioRoom(
  // …required data…
  config: VoxeraAudioRoomConfig(
    // behavior
    showControlsBar: true,
    showSeatNames: true,        // render occupant names under seats
    enableMinimize: true,
    turnOnMicrophoneWhenJoining: false,
    useSpeakerWhenJoining: true,
    hostSeatIndex: 0,
    autoHostMic: true,          // host/admin joins mic-on
    autoSeatHost: true,         // host auto-takes hostSeatIndex if empty
    useDefaultBackground: true, // paints theme.background when no backgroundWidget

    // theming & strings
    theme: VoxeraRoomTheme(),                 // or VoxeraRoomTheme().copyWith(primary: …)
    strings: VoxeraRoomStrings.ar(),          // null ⇒ English

    // user attributes shared with others (name/avatar are read on seats)
    userInRoomAttributes: {'avatar': 'https://…', 'name': 'John'},

    // section overrides (null ⇒ built-in default)
    headerWidget: null,
    messagesWidget: null,
    controlsBarWidget: null,
    backgroundWidget: null,
    foregroundWidget: null,
    pkWidget: null,

    // seat builders (null ⇒ built-in default avatar/name)
    seatBuilder: null,
    avatarBuilder: null,
    emptySeatBuilder: null,
    lockedSeatBuilder: null,
  ),
);

Every built-in default is off when you override it — supplying a controlsBarWidget, avatarBuilder, onSeatTap, etc. fully replaces the corresponding default.

Theming & localization #

VoxeraRoomTheme is an all-defaulted color set (dark by default, so the white default content is never invisible). VoxeraRoomStrings.en() / .ar() cover every default label; pass your own VoxeraRoomStrings(...) to translate or relabel:

config: VoxeraAudioRoomConfig(
  theme: const VoxeraRoomTheme().copyWith(primary: Colors.teal, background: const Color(0xFF101010)),
  strings: VoxeraRoomStrings.ar(),
);

Controller API #

onControllerReady gives you a VoxeraRoomController for programmatic control:

final c = controller;

// Seats
await c.seatController.takeSeat(0, c.localIdentity!);
await c.seatController.leaveSeat(c.localIdentity!);
await c.seatController.lockSeat(2, identity: c.localIdentity!);

// Speak requests
await c.requestToSpeak();
await c.approveSpeakerRequest(requestId);
await c.inviteToSpeak(targetId, seatIndex: 3);

// Persistent admins (owner-only; target may be offline / never joined)
await c.assignAdmin(targetIdentity: id, name: 'Moderator');
final allAdmins = await c.listAdmins();
final offlineAdmins = await c.listAdmins(presence: 'offline');
await c.revokeAdmin(id);

// Current-participant roles & bans (gated server-side)
await c.changeRole(targetIdentity: id, role: 'admin');
await c.banUser(id, reason: '…');                      // host/admin-gated

// Media
await c.mediaController.toggleMicrophone();

// Reactive state
c.seatController.seats;            // ValueNotifier<List<SeatState>>
c.seatController.pendingRequests;  // ValueNotifier<List<SpeakerRequest>>
c.activeSpeakers;                  // ValueNotifier<Set<String>>
c.participantsStream;             // Stream<List<VoxeraParticipant>>

Room music #

controller.music plays a song from this phone to the room. VoxeraAudioRoomConfig.roomMusicMode picks how:

  • VoxeraRoomMusicMode.track (default): the song goes on a second track, room-music. Listeners can mute the music alone.
  • VoxeraRoomMusicMode.aux: the song is mixed into the microphone after echo cancellation (one uplink, about 31 MB/h per listener at 48 kbps instead of about 50). The mic button then mutes the voice only; the song keeps going. A host seat mute silences both, and the seat unmute brings the song back. Listeners cannot mute the music alone. See the media client's doc/room-music-aux-mix.md.
final music = controller.music;
await music.start(path: cachedSongFile);   // a downloaded server song works the same way
music.setPublishVolume(0.8);               // what the room hears
music.setPlayoutVolume(0.5);               // this phone's speaker
await music.pause(); await music.resume(); await music.seek(const Duration(seconds: 30));
music.onFinished.listen((_) { /* next song */ });
await music.stop();

Room messaging (engine-batched chat, gifts and likes) #

When the engine offers it for your project (room_messaging in the token response), the kit sends chat, app messages and likes through the engine's room socket. The engine orders them (app messages, then host/admin chat, then chat), caps visible chat per room and batches everything into one frame per room per tick, so a busy room stays readable and light on the network. Your code does not change: messages still arrive on dataStream with the same payloads.

// true while chat / gifts / likes go through the engine
controller.roomMessagingActive.addListener(() { /* ... */ });

final ok = await controller.sendRoomMessage({'type': 'gift', 'gift_id': 7});
if (!ok) { /* the engine refused it: too fast or the room is busy */ }

controller.sendLike();                       // taps are summed for you
controller.likeCount.addListener(() { /* room total */ });
controller.likeStream.listen((n) { /* animate n likes */ });

If the project does not use it, or the socket is down, chat and app messages use the data channel exactly as before and sendLike() returns false.

Minimize / PiP #

Wrap your app with VoxeraMiniPopScope and mount VoxeraMiniOverlayPage at the root, then configure minimize in onControllerReady:

onControllerReady: (controller) {
  controller.navigatorKey = navKey; // for default dialogs
  controller.minimize.configure(
    VoxeraMinimizeConfig(
      enableOSPip: true,
      onClose: () => myExitRoom(),
    ),
  );
},

Layout modes #

The package ships one default mode (id: '3', 9 seats, 3×3). Register custom layouts via modes and select with layoutMode:

VoxeraAudioRoom(
  layoutMode: 'party',
  modes: const [
    VoxeraRoomMode(id: 'party', seatCount: 8, rows: [[0,1,2,3],[4,5,6,7]]),
  ],
);

Architecture #

VoxeraRoomController
├── (real-time engine) — connection & lifecycle (internal)
├── VoxeraSeatController  — seat state (ValueNotifier) + speak requests
├── VoxeraMediaController — mic / speaker
├── VoxeraChatController  — data-channel chat
└── VoxeraMinimizeController / VoxeraPipController

All state is exposed via ValueNotifier/Stream, so it integrates with any state-management approach. See the Quick start snippet above for a minimal drop-in app.

0
likes
0
points
1.23k
downloads

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

unknown (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