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_meetFeatures
- 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
| Platform | Support |
|---|---|
| Android | Full Support |
| iOS | Full 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,
);| Option | Type | Required | Description |
|---|---|---|---|
apiKey | String | Yes | Your API key |
secretKey | String | Yes | Your secret key for HMAC signing |
baseUrl | String? | No | API base URL (default: v4 endpoint) |
enableNetworkLogging | bool | No | Log 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>>.
| Operation | Method | Result |
|---|---|---|
| Quick video | createQuickVideoRoom(...) | Read final_link, join with video |
| Quick audio | createQuickAudioRoom(...) | Read final_link, join with videoAvailable: false |
| Scheduled video | createScheduledVideoRoom(...) | Store room ID, start later |
| Scheduled audio | createScheduledAudioRoom(...) | Start later, join with videoAvailable: false |
| Start scheduled | startScheduledRoom(roomId, name: ...) | Read a fresh final_link |
| Join | joinRoom(roomId: ..., userInfo: ...) | Read a fresh final_link |
| Invitation | createInvitationLink(roomId: ..., role: ...) | Share an HTTP(S) link |
| Inspect active room | getActiveRoomInfo(roomId) | Active-room details |
| Inspect all active | getActiveRoomsInfo() | Active-room collection |
| Status | getRoomStatus(roomId) | Lifecycle status |
| Past rooms | fetchPastRooms(...) | Paged history |
| End | endRoom(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
);Create Invitation Link
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
},
);| Parameter | Type | Required | Description |
|---|---|---|---|
context | BuildContext | Yes | Used to present the meeting route |
finalLink | String | Yes | A fresh final_link from a room response |
userName | String? | No | Display name shown to other participants |
userImage | String? | No | Avatar shown in the meeting |
configuration | QriibMeetingConfiguration? | No | Meeting UI and behavior |
onDisconnected | VoidCallback? | No | Fired 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();
},
);| Property | Type | Default | Description |
|---|---|---|---|
title | String | 'Meeting' | Meeting display title |
theme | QriibMeetingTheme | QriibMeetingTheme() | Visual tokens for the default view |
showTopBar | bool | true | Show the top bar |
showParticipantNames | bool | true | Show participant name labels |
showCameraControl | bool | true | Show the camera toggle |
showMicrophoneControl | bool | true | Show the microphone toggle |
showAudioDeviceControl | bool | true | Show the audio-route control |
showLeaveControl | bool | true | Show the leave button |
videoAvailable | bool | true | Whether this meeting has a video track |
preJoin | QriibPreJoinConfiguration | disabled | Lobby shown before connecting |
participantBuilder | QriibParticipantBuilder? | — | Replace the participant tile |
topBarBuilder | QriibMeetingTopBarBuilder? | — | Replace the top region |
controlsBuilder | QriibMeetingControlsBuilder? | — | Replace the controls region |
onCameraChanged | QriibMeetingMediaChanged? | — | Called after a camera change |
onMicrophoneChanged | QriibMeetingMediaChanged? | — | Called after a microphone change |
onLeaveRequested | QriibMeetingLeaveRequested? | — | Return false to cancel leaving |
Theme Tokens
| Property | Type | Default |
|---|---|---|
backgroundColor | Color | Color(0xFF0B0C10) |
surfaceColor | Color | Color(0xE61A1D27) |
primaryColor | Color | Color(0xFF8B6CFF) |
dangerColor | Color | Color(0xFFFF5D73) |
tileColor | Color | Color(0xFF171923) |
participantNameStyle | TextStyle | white, 13, w600 |
topBarTextStyle | TextStyle | white, 16, w700 |
cameraOnIcon / cameraOffIcon | IconData | Icons.videocam_rounded / Icons.videocam_off_rounded |
microphoneOnIcon / microphoneOffIcon | IconData | Icons.mic_rounded / Icons.mic_off_rounded |
leaveIcon | IconData | Icons.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,
),
);| Property | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Show the lobby before connecting |
theme | QriibMeetingTheme | QriibMeetingTheme() | Lobby appearance |
title | String? | — | Title shown on the lobby |
showDeviceSelector | bool | true | Show the device picker |
builder | QriibPreJoinBuilder? | — | 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.
| Member | Signature | Description |
|---|---|---|
state | ValueListenable<QriibMeetingState> | Observable meeting state |
setCameraEnabled | Future<void> setCameraEnabled(bool) | Toggle the local camera |
setMicrophoneEnabled | Future<void> setMicrophoneEnabled(bool) | Toggle the local microphone |
setSpeakerphoneOn | Future<void> setSpeakerphoneOn(bool) | Mobile-only speakerphone toggle |
selectAudioInput | Future<void> selectAudioInput(QriibAudioDevice) | Switch the active microphone |
selectAudioOutput | Future<void> selectAudioOutput(QriibAudioDevice) | Switch the active output |
videoViewFor | Widget videoViewFor(QriibMeetingParticipant) | Video widget for a participant |
leave | Future<void> leave() | Leave the meeting |
QriibMeetingState
| Property | Type | Description |
|---|---|---|
participants | List<QriibMeetingParticipant> | Current attendees |
cameraEnabled | bool | Local camera state |
microphoneEnabled | bool | Local microphone state |
audioInputs | List<QriibAudioDevice> | Available microphones |
audioOutputs | List<QriibAudioDevice> | Available audio outputs |
selectedAudioInputId | String? | Active microphone id |
selectedAudioOutputId | String? | Active output id |
speakerOn | bool | Speakerphone state |
canToggleSpeakerphone | bool | True on mobile, where routing is a speaker on/off toggle |
canSelectSpecificDevice | bool | True 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
| Function | Signature | Description |
|---|---|---|
newClientRoomId | String newClientRoomId() | Generates a UUID v4-style client_room_id |
finalLinkFromResponse | String? 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.
| Exception | Status Code | Description |
|---|---|---|
QriibAuthenticationException | 401 | Invalid API key or signature |
QriibValidationException | 400 | Request validation failed |
QriibNotFoundException | 404 | Resource not found |
QriibRateLimitException | 429 | Rate 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().
| Exception | Description |
|---|---|
QriibNetworkException | The meeting service could not be reached |
QriibSessionException | A session could not be started or continued |
QriibMeetException | Base class for meeting errors |
Parameter Reference
Create Quick Room
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | String | Yes | Project UUID |
clientRoomId | String | Yes | Your unique room identifier |
metadata | QriibRoomMetadata | Yes | Room metadata (see below) |
name | String? | No | Room display name |
moderatorId | String? | No | Moderator user ID |
maxParticipants | Object? | No | Max allowed participants |
emptyTimeout | Object? | No | Seconds before an empty room closes |
Create Scheduled Room
Same as above, plus:
| Parameter | Type | Required | Description |
|---|---|---|---|
startAt | String | Yes | ISO8601 datetime for the scheduled start |
Scheduled rooms take no name or moderatorId at creation time — pass name to startScheduledRoom instead.
QriibRoomMetadata
| Parameter | Type | Required | Description |
|---|---|---|---|
roomTitle | String | Yes | Main title shown to participants |
welcomeMessage | String? | No | Welcome message displayed inside the room |
roomDuration | int? | No | Room duration limit |
QriibUserInfo
| Parameter | Type | Required | Description |
|---|---|---|---|
name | String | Yes | Display name |
role | String? | No | Room role, for example 'attendee' |
isAdmin | bool? | No | Grant admin privileges |
isHidden | bool? | No | Hide the participant from the roster |
Fetch Past Rooms
| Parameter | Type | Required | Description |
|---|---|---|---|
projectId | String | Yes | Project UUID |
roomIds | List<String>? | No | Restrict to specific room IDs |
from | int? | No | Pagination offset |
limit | int? | No | Max results to return |
orderBy | QriibPastRoomsOrder? | No | .asc or .desc |
Enums
| Enum | Values |
|---|---|
QriibInvitationRole | admin, attendee |
QriibPastRoomsOrder | asc, desc |
QriibAudioDeviceKind | input, 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-linkAuthentication
All API requests are authenticated using:
keyheader: Your API keyhash-signatureheader: 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
QriibMeetClientfor the app and callclose()fromdispose(). - Generate room identifiers with
newClientRoomId()rather than rolling your own. - Read
final_linkwithfinalLinkFromResponse()immediately before joining; never store it. - Gate audio controls on
canToggleSpeakerphone/canSelectSpecificDeviceinstead of checking the platform. - Set
videoAvailable: falsewhen joining an audio-only room so the view hides video controls. - Keep API keys out of source: pass them with
--dart-defineor a secure store, not literals.