Vconnct Developers

Flutter SDK

Official Flutter/Dart SDK for Qriib rooms and embedded meetings

Flutter SDK

Official Flutter SDK for Qriib. It combines the v4 room management API with a fully embedded, customizable meeting experience — create a room, read its final_link, and push a native meeting screen without leaving your app.

flutter pub add qriib_meet

Features

  • Room management and an embedded meeting UI in a single package
  • Automatic HMAC SHA256 signature generation
  • Premium default meeting view with theming, or bring your own widgets
  • Optional pre-join lobby with camera/microphone preview and device picker
  • Audio routing: speakerphone toggle on mobile, explicit device selection on desktop/web
  • Typed exceptions for API and session failures

Supported Platforms

PlatformSupport
AndroidFull Support
iOSFull Support

Android and iOS are the documented targets and the ones with setup steps below. The audio API also exposes desktop/web-style device selection — check state.canSelectSpecificDevice and state.canToggleSpeakerphone before rendering audio controls rather than assuming a platform.


Platform Setup

Android

Add the permissions to android/app/src/main/AndroidManifest.xml, inside <manifest>:

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

iOS

Add the purpose strings to ios/Runner/Info.plist:

<key>NSCameraUsageDescription</key>
<string>Use the camera to share video in a meeting.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Use the microphone to share audio in a meeting.</string>

Then enable the permission macros in the post_install hook of ios/Podfile:

config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
  '$(inherited)',
  'PERMISSION_CAMERA=1',
  'PERMISSION_MICROPHONE=1',
]

Quick Start

import 'package:qriib_meet/qriib_meet.dart';

// Initialize the client
final qriib = QriibMeetClient.withProjectCredentials(
  apiKey: const String.fromEnvironment('QRIIB_API_KEY'),
  secretKey: const String.fromEnvironment('QRIIB_SECRET_KEY'),
  enableNetworkLogging: false,
);

// Create a video room
final response = await qriib.rooms.createQuickVideoRoom(
  projectId: projectId,
  clientRoomId: newClientRoomId(),
  moderatorId: moderatorId,
  name: participantName,
  maxParticipants: 10,
  emptyTimeout: 300,
  metadata: const QriibRoomMetadata(
    roomTitle: 'Team Meeting',
    welcomeMessage: 'Welcome to the meeting!',
  ),
);

// Read the join link and open the meeting
final finalLink = finalLinkFromResponse(response);
if (finalLink == null) {
  throw const QriibMeetException('The response has no final_link to open.');
}

await qriib.meetings.join(
  context: context,
  finalLink: finalLink,
  userName: participantName,
);

final_link values are short-lived. Read them immediately and do not persist them. Treat credentials and links as sensitive — keep them out of commits, logs, and analytics.

Configuration

// Project credentials (rooms, meetings, analytics)
final qriib = QriibMeetClient.withProjectCredentials(
  apiKey: 'your-api-key',        // Required: Your API key
  secretKey: 'your-secret-key',  // Required: Your secret key for HMAC signing
  baseUrl: null,                 // Optional: API base URL (defaults to the v4 endpoint)
  enableNetworkLogging: true,    // Optional: Log network traffic (default: true)
);

// Or supply project and organization credentials together
final qriib = QriibMeetClient.withCredentials(
  project: const QriibProjectCredentials(
    apiKey: 'your-api-key',
    secretKey: 'your-secret-key',
  ),
  organization: organizationCredentials, // Optional: management requests
  enableNetworkLogging: false,
);
OptionTypeRequiredDescription
apiKeyStringYesYour API key
secretKeyStringYesYour secret key for HMAC signing
baseUrlString?NoAPI base URL (default: v4 endpoint)
enableNetworkLoggingboolNoLog network traffic (default: true)

The client owns network resources. Use a single instance for the app and release it when you are done:

@override
void dispose() {
  qriib.close();
  super.dispose();
}

API Reference

Rooms

All room methods live on qriib.rooms and return Future<Map<String, dynamic>>.

OperationMethodResult
Quick videocreateQuickVideoRoom(...)Read final_link, join with video
Quick audiocreateQuickAudioRoom(...)Read final_link, join with videoAvailable: false
Scheduled videocreateScheduledVideoRoom(...)Store room ID, start later
Scheduled audiocreateScheduledAudioRoom(...)Start later, join with videoAvailable: false
Start scheduledstartScheduledRoom(roomId, name: ...)Read a fresh final_link
JoinjoinRoom(roomId: ..., userInfo: ...)Read a fresh final_link
InvitationcreateInvitationLink(roomId: ..., role: ...)Share an HTTP(S) link
Inspect active roomgetActiveRoomInfo(roomId)Active-room details
Inspect all activegetActiveRoomsInfo()Active-room collection
StatusgetRoomStatus(roomId)Lifecycle status
Past roomsfetchPastRooms(...)Paged history
EndendRoom(roomId)Ends an active room

Create Quick Video Room

Create an instant video room for immediate use.

final response = await qriib.rooms.createQuickVideoRoom(
  projectId: 'project-uuid',
  clientRoomId: newClientRoomId(),
  metadata: const QriibRoomMetadata(
    roomTitle: 'Display Title',      // required
    welcomeMessage: 'Welcome!',      // optional
    roomDuration: 60,                // optional
  ),
  name: 'Room Name',                 // optional
  moderatorId: 'moderator-user-id',  // optional
  maxParticipants: 10,               // optional
  emptyTimeout: 300,                 // optional (seconds)
);

Create Quick Audio Room

Create an instant audio-only room. Join it with videoAvailable: false so the meeting view hides video affordances.

final response = await qriib.rooms.createQuickAudioRoom(
  projectId: 'project-uuid',
  clientRoomId: newClientRoomId(),
  metadata: const QriibRoomMetadata(roomTitle: 'Audio Standup'),
);

Create Scheduled Video Room

Schedule a video room for a future time.

final response = await qriib.rooms.createScheduledVideoRoom(
  projectId: 'project-uuid',
  clientRoomId: newClientRoomId(),
  startAt: '2025-12-31T10:00:00Z',   // ISO8601 (required)
  metadata: const QriibRoomMetadata(roomTitle: 'Quarterly Review'),
  maxParticipants: 50,               // optional
  emptyTimeout: 300,                 // optional (seconds)
);

Create Scheduled Audio Room

final response = await qriib.rooms.createScheduledAudioRoom(
  projectId: 'project-uuid',
  clientRoomId: newClientRoomId(),
  startAt: '2025-12-31T10:00:00Z',
  metadata: const QriibRoomMetadata(roomTitle: 'Audio Briefing'),
);

Start Scheduled Room

Start a previously scheduled room and read a fresh link.

final response = await qriib.rooms.startScheduledRoom(
  roomId,
  name: participantName,  // optional
);

Join Room

Enter an existing room as a specific identity.

final response = await qriib.rooms.joinRoom(
  roomId: 'room-uuid',
  userInfo: const QriibUserInfo(
    name: 'Sara',
    role: 'attendee',   // optional
    isAdmin: false,     // optional
    isHidden: false,    // optional
  ),
);

Get Active Room Info

final roomInfo = await qriib.rooms.getActiveRoomInfo('room-uuid');

Get All Active Rooms

final activeRooms = await qriib.rooms.getActiveRoomsInfo();

Get Room Status

final status = await qriib.rooms.getRoomStatus('room-uuid');

Fetch Past Rooms

final pastRooms = await qriib.rooms.fetchPastRooms(
  projectId: 'project-uuid',
  roomIds: ['room-id-1', 'room-id-2'],  // optional
  from: 0,                              // optional (offset)
  limit: 10,                            // optional
  orderBy: QriibPastRoomsOrder.desc,    // optional
);
final invite = await qriib.rooms.createInvitationLink(
  roomId: 'room-uuid',
  role: QriibInvitationRole.attendee,  // .admin | .attendee
);

End Room

final result = await qriib.rooms.endRoom('room-uuid');

Meetings

qriib.meetings.join pushes the meeting screen and connects using a fresh final_link.

await qriib.meetings.join(
  context: context,                     // required
  finalLink: finalLink,                 // required
  userName: participantName,            // optional
  userImage: avatarUrl,                 // optional
  configuration: meetingConfiguration,  // optional
  onDisconnected: () {                  // optional
    // Restore app state after the meeting closes
  },
);
ParameterTypeRequiredDescription
contextBuildContextYesUsed to present the meeting route
finalLinkStringYesA fresh final_link from a room response
userNameString?NoDisplay name shown to other participants
userImageString?NoAvatar shown in the meeting
configurationQriibMeetingConfiguration?NoMeeting UI and behavior
onDisconnectedVoidCallback?NoFired when the session ends

Meeting Configuration

QriibMeetingConfiguration controls the default meeting view and its replacement slots.

final meetingConfiguration = QriibMeetingConfiguration(
  title: 'Team Meeting',
  theme: const QriibMeetingTheme(
    backgroundColor: Color(0xFF0B0C10),
    surfaceColor: Color(0xE61A1D27),
    primaryColor: Color(0xFF8B6CFF),
    dangerColor: Color(0xFFFF5D73),
    tileColor: Color(0xFF171923),
  ),
  showTopBar: true,
  showParticipantNames: true,
  showCameraControl: true,
  showMicrophoneControl: true,
  showAudioDeviceControl: true,
  showLeaveControl: true,
  videoAvailable: true,
  onCameraChanged: (session, enabled) {
    // Update app-owned UI or analytics
  },
  onMicrophoneChanged: (session, enabled) {
    // Update app-owned UI or analytics
  },
  onLeaveRequested: (session) async {
    return await confirmLeaveWithUser();
  },
);
PropertyTypeDefaultDescription
titleString'Meeting'Meeting display title
themeQriibMeetingThemeQriibMeetingTheme()Visual tokens for the default view
showTopBarbooltrueShow the top bar
showParticipantNamesbooltrueShow participant name labels
showCameraControlbooltrueShow the camera toggle
showMicrophoneControlbooltrueShow the microphone toggle
showAudioDeviceControlbooltrueShow the audio-route control
showLeaveControlbooltrueShow the leave button
videoAvailablebooltrueWhether this meeting has a video track
preJoinQriibPreJoinConfigurationdisabledLobby shown before connecting
participantBuilderQriibParticipantBuilder?Replace the participant tile
topBarBuilderQriibMeetingTopBarBuilder?Replace the top region
controlsBuilderQriibMeetingControlsBuilder?Replace the controls region
onCameraChangedQriibMeetingMediaChanged?Called after a camera change
onMicrophoneChangedQriibMeetingMediaChanged?Called after a microphone change
onLeaveRequestedQriibMeetingLeaveRequested?Return false to cancel leaving

Theme Tokens

PropertyTypeDefault
backgroundColorColorColor(0xFF0B0C10)
surfaceColorColorColor(0xE61A1D27)
primaryColorColorColor(0xFF8B6CFF)
dangerColorColorColor(0xFFFF5D73)
tileColorColorColor(0xFF171923)
participantNameStyleTextStylewhite, 13, w600
topBarTextStyleTextStylewhite, 16, w700
cameraOnIcon / cameraOffIconIconDataIcons.videocam_rounded / Icons.videocam_off_rounded
microphoneOnIcon / microphoneOffIconIconDataIcons.mic_rounded / Icons.mic_off_rounded
leaveIconIconDataIcons.call_end_rounded

Pre-Join Lobby

Disabled by default. Enable it to let participants confirm their devices before connecting.

final meetingConfiguration = QriibMeetingConfiguration(
  preJoin: const QriibPreJoinConfiguration(
    enabled: true,
    title: 'Team Meeting',
    showDeviceSelector: true,
  ),
);
PropertyTypeDefaultDescription
enabledboolfalseShow the lobby before connecting
themeQriibMeetingThemeQriibMeetingTheme()Lobby appearance
titleString?Title shown on the lobby
showDeviceSelectorbooltrueShow the device picker
builderQriibPreJoinBuilder?Replace the lobby entirely

Custom UI Builders

Replace individual regions while keeping the rest of the default view.

final meetingConfiguration = QriibMeetingConfiguration(
  participantBuilder: (context, participant, video, session) {
    return ValueListenableBuilder<QriibMeetingState>(
      valueListenable: session.state,
      builder: (context, state, child) {
        final localCameraOff = participant.isLocal && !state.cameraEnabled;
        if (!localCameraOff) return video;
        final initial = participant.label.isEmpty
            ? '?'
            : participant.label.characters.first.toUpperCase();
        return ColoredBox(
          color: participantTileColor,
          child: Center(child: CircleAvatar(child: Text(initial))),
        );
      },
    );
  },
  topBarBuilder: (context, state, session) => MeetingHeader(
    title: 'Team Meeting',
    participantCount: state.participants.length,
  ),
  controlsBuilder: (context, state, session) => MeetingControls(
    cameraEnabled: state.cameraEnabled,
    microphoneEnabled: state.microphoneEnabled,
    onCameraPressed: () => session.setCameraEnabled(!state.cameraEnabled),
    onMicrophonePressed: () =>
        session.setMicrophoneEnabled(!state.microphoneEnabled),
    onLeavePressed: session.leave,
  ),
);

Session and State

Builders and callbacks receive a QriibMeetingSession. Its state is a ValueListenable<QriibMeetingState>, so you can rebuild your own widgets from it.

MemberSignatureDescription
stateValueListenable<QriibMeetingState>Observable meeting state
setCameraEnabledFuture<void> setCameraEnabled(bool)Toggle the local camera
setMicrophoneEnabledFuture<void> setMicrophoneEnabled(bool)Toggle the local microphone
setSpeakerphoneOnFuture<void> setSpeakerphoneOn(bool)Mobile-only speakerphone toggle
selectAudioInputFuture<void> selectAudioInput(QriibAudioDevice)Switch the active microphone
selectAudioOutputFuture<void> selectAudioOutput(QriibAudioDevice)Switch the active output
videoViewForWidget videoViewFor(QriibMeetingParticipant)Video widget for a participant
leaveFuture<void> leave()Leave the meeting

QriibMeetingState

PropertyTypeDescription
participantsList<QriibMeetingParticipant>Current attendees
cameraEnabledboolLocal camera state
microphoneEnabledboolLocal microphone state
audioInputsList<QriibAudioDevice>Available microphones
audioOutputsList<QriibAudioDevice>Available audio outputs
selectedAudioInputIdString?Active microphone id
selectedAudioOutputIdString?Active output id
speakerOnboolSpeakerphone state
canToggleSpeakerphoneboolTrue on mobile, where routing is a speaker on/off toggle
canSelectSpecificDeviceboolTrue on desktop/web, where a specific device can be targeted

Audio Device Control

The default meeting view already includes an audio-route button. To drive it yourself:

final state = session.state.value;

// Mobile: toggle speaker
if (state.canToggleSpeakerphone) {
  await session.setSpeakerphoneOn(!state.speakerOn);
}

// Desktop/web: select a specific device
if (state.canSelectSpecificDevice) {
  await session.selectAudioInput(state.audioInputs.first);
  await session.selectAudioOutput(state.audioOutputs.first);
}

Each QriibAudioDevice carries an id, an OS-provided label (for example AirPods Pro), and a kind of QriibAudioDeviceKind.input or QriibAudioDeviceKind.output.


Helpers

FunctionSignatureDescription
newClientRoomIdString newClientRoomId()Generates a UUID v4-style client_room_id
finalLinkFromResponseString? finalLinkFromResponse(Map<String, dynamic>)Reads final_link from a response, including a data envelope; returns null if absent

Error Handling

The SDK separates API failures from meeting session failures.

try {
  final response = await qriib.rooms.createQuickVideoRoom(
    projectId: projectId,
    clientRoomId: newClientRoomId(),
    metadata: const QriibRoomMetadata(roomTitle: 'Team Meeting'),
  );

  final finalLink = finalLinkFromResponse(response);
  if (finalLink == null) {
    throw const QriibMeetException('The response has no final_link to open.');
  }

  await qriib.meetings.join(context: context, finalLink: finalLink);
} on QriibAuthenticationException catch (e) {
  // 401 — invalid API key or signature
  debugPrint('Authentication failed: ${e.message}');
} on QriibValidationException catch (e) {
  // 400 — request validation failed
  debugPrint('Validation error: ${e.message}');
} on QriibNotFoundException catch (e) {
  // 404 — resource not found
  debugPrint('Not found: ${e.message}');
} on QriibRateLimitException catch (e) {
  // 429 — rate limit exceeded
  debugPrint('Rate limited: ${e.message}');
} on QriibApiException catch (e) {
  // Any other API error
  debugPrint('API error: ${e.message} (status ${e.statusCode})');
} on QriibNetworkException catch (e) {
  // The meeting service could not be reached
  debugPrint('Network error: ${e.message}');
} on QriibSessionException catch (e) {
  // The session could not be started or continued
  debugPrint('Session error: ${e.message}');
} on QriibMeetException catch (e) {
  // Invalid or expired meeting link
  debugPrint('Meeting error: ${e.message}');
}

API Exceptions

QriibApiException(String message, {int? statusCode, Object? response}) is the base class for request failures.

ExceptionStatus CodeDescription
QriibAuthenticationException401Invalid API key or signature
QriibValidationException400Request validation failed
QriibNotFoundException404Resource not found
QriibRateLimitException429Rate limit exceeded
QriibApiException*Base class for all API errors

Meeting Exceptions

QriibMeetException(String message) is the base class for the meeting experience. It also has two named constructors for link problems: QriibMeetException.invalidMeetingLink() and QriibMeetException.expiredMeetingLink().

ExceptionDescription
QriibNetworkExceptionThe meeting service could not be reached
QriibSessionExceptionA session could not be started or continued
QriibMeetExceptionBase class for meeting errors

Parameter Reference

Create Quick Room

ParameterTypeRequiredDescription
projectIdStringYesProject UUID
clientRoomIdStringYesYour unique room identifier
metadataQriibRoomMetadataYesRoom metadata (see below)
nameString?NoRoom display name
moderatorIdString?NoModerator user ID
maxParticipantsObject?NoMax allowed participants
emptyTimeoutObject?NoSeconds before an empty room closes

Create Scheduled Room

Same as above, plus:

ParameterTypeRequiredDescription
startAtStringYesISO8601 datetime for the scheduled start

Scheduled rooms take no name or moderatorId at creation time — pass name to startScheduledRoom instead.

QriibRoomMetadata

ParameterTypeRequiredDescription
roomTitleStringYesMain title shown to participants
welcomeMessageString?NoWelcome message displayed inside the room
roomDurationint?NoRoom duration limit

QriibUserInfo

ParameterTypeRequiredDescription
nameStringYesDisplay name
roleString?NoRoom role, for example 'attendee'
isAdminbool?NoGrant admin privileges
isHiddenbool?NoHide the participant from the roster

Fetch Past Rooms

ParameterTypeRequiredDescription
projectIdStringYesProject UUID
roomIdsList<String>?NoRestrict to specific room IDs
fromint?NoPagination offset
limitint?NoMax results to return
orderByQriibPastRoomsOrder?No.asc or .desc

Enums

EnumValues
QriibInvitationRoleadmin, attendee
QriibPastRoomsOrderasc, desc
QriibAudioDeviceKindinput, output

Running the Example

The bundled example reads credentials from compile-time environment variables:

cd example
flutter run \
  --dart-define=QRIIB_API_KEY=your-project-api-key \
  --dart-define=QRIIB_SECRET_KEY=your-project-secret \
  --dart-define=QRIIB_FINAL_LINK=a-fresh-final-link

Authentication

All API requests are authenticated using:

  • key header: Your API key
  • hash-signature header: HMAC SHA256 signature (Base64 encoded)

The SDK handles signature generation automatically:

  • GET requests: Signs the full URL path with query string
  • POST/PATCH requests: Signs the JSON request body
  • FormData requests: Signs the project_id

Best Practices

  • Use a single QriibMeetClient for the app and call close() from dispose().
  • Generate room identifiers with newClientRoomId() rather than rolling your own.
  • Read final_link with finalLinkFromResponse() immediately before joining; never store it.
  • Gate audio controls on canToggleSpeakerphone / canSelectSpecificDevice instead of checking the platform.
  • Set videoAvailable: false when joining an audio-only room so the view hides video controls.
  • Keep API keys out of source: pass them with --dart-define or a secure store, not literals.