qriib_meet 0.2.1
qriib_meet: ^0.2.1 copied to clipboard
Qriib Flutter SDK for room management and embedded video meetings.
Qriib Meet — Flutter integration guide #
qriib_meet is the single Flutter package an application uses to create and
manage Qriib rooms, then open the embedded Qriib meeting experience.
import 'package:qriib_meet/qriib_meet.dart';
Do not import an additional meeting or media package in the application.
qriib_meet contains its private media engine and exports the public room
models and errors needed by the examples below.
Before you start #
qriib_meet is a Flutter package for Android and iOS applications. The API
client signs management requests with Qriib credentials, while a meeting is
opened with the fresh final_link returned by the Qriib API.
For production, keep organization/project management credentials on your
backend. Client configuration, build-time variables, and obfuscation do not
keep shipped secrets confidential. Have the backend authorize the participant
and return a fresh final_link; never log that link. The shipped whiteboard
implementation also contains client-visible signing/API keys (see Whiteboard
below); it is not a backend-only signing design.
Join a fresh link without management credentials #
A consumer that already has a participant-specific link does not need a
QriibMeetClient or project/organization credentials:
await QriibMeetings().join(
context: context,
finalLink: freshFinalLinkFromYourBackend,
// audioOnly: true, // Required when entering an audio-only room.
);
Omit roomId here to avoid offering end-for-all: standalone QriibMeetings()
has no management transport. Use a backend-authorized management flow for
ending rooms. join obtains permissions and connects before pushing the route;
show progress in the calling screen and catch connection failures there.
Updating to 0.2.1 #
The published package name remains qriib_meet. Keep using
import 'package:qriib_meet/qriib_meet.dart';, update the dependency to
qriib_meet: ^0.2.1, and run flutter pub get once the release is available.
Public Dart names such as QriibMeetClient, QriibMeetings, and
QriibMeetException remain unchanged.
Installation #
After the hosted package release, add the package to your Flutter app:
dependencies:
flutter:
sdk: flutter
qriib_meet: ^0.2.1
Then fetch dependencies:
flutter pub get
Import only the public package entry point:
import 'package:qriib_meet/qriib_meet.dart';
Response helpers used below #
Use the exported finalLinkFromResponse(response) for a top-level response or
one wrapped in data. It returns a trimmed link or null. Room IDs use the
same envelope rule; this helper is application code, not an SDK export:
String? roomIdFromResponse(Map<String, dynamic> response) {
final payload = response['data'] is Map
? Map<String, dynamic>.from(response['data'] as Map)
: response;
final roomId = payload['room_id']?.toString().trim();
return roomId == null || roomId.isEmpty ? null : roomId;
}
Management-enabled integration #
The following SDK management examples are for trusted/test integrations, not a
recommendation to distribute management secrets in consumer apps. Flutter widget
examples also require import 'package:flutter/material.dart';.
- Create one
QriibMeetClientwhen your feature starts. - Call a room API such as
createQuickVideoRoomorjoinRoom. - Read the server-returned
final_linkand callclient.meetings.join. - The package pushes the full-screen meeting UI. It owns camera, microphone, participants, leaving, and the room-ID-gated end-for-everyone action.
class VideoCallButton extends StatefulWidget {
const VideoCallButton({super.key});
@override
State<VideoCallButton> createState() => _VideoCallButtonState();
}
class _VideoCallButtonState extends State<VideoCallButton> {
late final QriibMeetClient _qriib;
@override
void initState() {
super.initState();
_qriib = QriibMeetClient.withProjectCredentials(
apiKey: 'YOUR_PROJECT_API_KEY',
secretKey: 'YOUR_PROJECT_SECRET_KEY',
baseUrl: 'https://api.qriib.dev',
enableNetworkLogging: false,
);
}
@override
void dispose() {
_qriib.close();
super.dispose();
}
Future<void> _startCall() async {
try {
final response = await _qriib.rooms.createQuickVideoRoom(
projectId: 'YOUR_PROJECT_ID',
clientRoomId: 'support-call-123',
metadata: const QriibRoomMetadata(roomTitle: 'Support call'),
);
final finalLink = finalLinkFromResponse(response);
if (finalLink == null) {
throw const QriibMeetException(
'The room response has no meeting link.',
);
}
if (!mounted) return;
await _qriib.meetings.join(
context: context,
finalLink: finalLink,
roomId: roomIdFromResponse(response),
);
} on QriibMeetException catch (error) {
_showError(error.message);
} on QriibApiException catch (error) {
_showError(error.message);
}
}
void _showError(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
}
@override
Widget build(BuildContext context) => ElevatedButton.icon(
onPressed: _startCall,
icon: const Icon(Icons.video_call),
label: const Text('Start video call'),
);
}
clientRoomId must be unique in the selected project. Use an ID from your
backend domain, for example an order, appointment, or support-ticket ID.
What it provides #
QriibMeetClient— API client that can use project and organization credentials.client.rooms— create, schedule, start, join, inspect, and end rooms.client.meetings— opens the full-screen embedded Qriib meeting from a server-issuedfinal_link.client.analytics,client.projects, andclient.recordings— management and reporting endpoints.
Credentials and resource access #
The client automatically signs the appropriate requests. You do not create
your own Dio client, headers, key, or hash-signature values.
| Resource | Required credentials | Main use |
|---|---|---|
client.rooms |
Project | Create, join, schedule, inspect, and end rooms |
QriibMeetings() / client.meetings |
Fresh final_link to join; project-signed transport only for end-for-all |
Open the embedded meeting UI |
client.analytics |
Project | Read room analytics |
client.recordings |
Project | Read recording data and links |
client.projects |
Organization | Create and manage projects |
Use QriibMeetings() for link-only applications and withProjectCredentials
only for integrations that need project management requests. Use
withCredentials when the same app also calls organization/project-management
operations.
Create the client once #
Create one client for the selected Qriib project and close it with the widget, service, or dependency container that owns it.
late final QriibMeetClient qriib;
@override
void initState() {
super.initState();
qriib = QriibMeetClient.withProjectCredentials(
apiKey: 'YOUR_PROJECT_API_KEY',
secretKey: 'YOUR_PROJECT_SECRET_KEY',
baseUrl: 'https://api.qriib.dev',
enableNetworkLogging: true,
);
}
@override
void dispose() {
qriib.close();
super.dispose();
}
enableNetworkLogging prints coloured request/response summaries. Disable it
in production. Never hard-code or log project credentials, final_link values,
or decoded meeting tokens.
Create a quick video room and open it #
Quick rooms return a final_link. Pass that complete string unchanged to
meetings.join; do not decode the token yourself.
Future<void> createAndOpenVideoRoom(BuildContext context) async {
final response = await qriib.rooms.createQuickVideoRoom(
projectId: 'YOUR_PROJECT_ID',
clientRoomId: 'order-123-video', // Unique inside the project.
name: 'Support call',
moderatorId: 'moderator-42',
maxParticipants: 20,
emptyTimeout: 1_000,
metadata: const QriibRoomMetadata(
roomTitle: 'Support call',
welcomeMessage: 'Welcome to the call',
roomDuration: 60,
),
);
await openReturnedMeeting(context, response, userName: 'Karim');
}
Future<void> openReturnedMeeting(
BuildContext context,
Map<String, dynamic> response, {
String? userName,
bool audioOnly = false,
}) async {
final finalLink = finalLinkFromResponse(response);
if (finalLink == null) {
throw const QriibMeetException('The room response has no meeting link.');
}
await qriib.meetings.join(
context: context,
finalLink: finalLink,
roomId: roomIdFromResponse(response),
userName: userName,
audioOnly: audioOnly,
);
}
Quick audio room #
An audio room API does not automatically select audio-only local media. Pass
audioOnly: true through to meetings.join to avoid requesting camera access
and to hide camera controls:
final audio = await qriib.rooms.createQuickAudioRoom(
projectId: 'YOUR_PROJECT_ID',
clientRoomId: newClientRoomId(),
metadata: const QriibRoomMetadata(roomTitle: 'Audio call'),
);
await openReturnedMeeting(context, audio, audioOnly: true);
Join an existing room #
joinRoom returns the participant-specific final_link used to open the
meeting. The role is sent as user_metadata.role.
final response = await qriib.rooms.joinRoom(
roomId: 'ROOM_ID',
userInfo: const QriibUserInfo(
name: 'Karim',
role: 'attendee',
isAdmin: false,
isHidden: false,
),
);
await openReturnedMeeting(context, response, userName: 'Karim');
Scheduled rooms #
Creating a scheduled room is not the same as starting a meeting:
create scheduled room -> room_id
start scheduled room -> final_link
open Qriib meeting -> meetings.join(finalLink: final_link)
Create the schedule and store the returned room_id in your own backend or
application state:
final scheduled = await qriib.rooms.createScheduledVideoRoom(
projectId: 'YOUR_PROJECT_ID',
clientRoomId: 'order-123-scheduled-video',
startAt: '2026-12-31T18:30', // Local ISO-8601 minute precision.
maxParticipants: 20,
emptyTimeout: 1_000,
metadata: const QriibRoomMetadata(roomTitle: 'Scheduled review'),
);
final roomId = roomIdFromResponse(scheduled);
if (roomId == null) throw StateError('Response has no room_id');
When the room is started, open its returned meeting link:
final started = await qriib.rooms.startScheduledRoom(
roomId,
name: 'Scheduled review',
);
await openReturnedMeeting(context, started, userName: 'Karim');
Scheduled audio room #
final scheduledAudio = await qriib.rooms.createScheduledAudioRoom(
projectId: 'YOUR_PROJECT_ID',
clientRoomId: newClientRoomId(),
startAt: '2026-12-31T18:30',
metadata: const QriibRoomMetadata(roomTitle: 'Scheduled audio'),
);
final audioRoomId = roomIdFromResponse(scheduledAudio);
if (audioRoomId == null) throw StateError('Response has no room_id');
final startedAudio = await qriib.rooms.startScheduledRoom(audioRoomId);
final audioEntry = finalLinkFromResponse(startedAudio) != null
? startedAudio
: await qriib.rooms.joinRoom(
roomId: audioRoomId,
userInfo: const QriibUserInfo(name: 'Karim', role: 'attendee'),
);
await openReturnedMeeting(context, audioEntry, audioOnly: true);
The same start-then-join fallback applies to scheduled video if starting does
not return a usable link. When joining any existing audio room, also pass
audioOnly: true; room type is not inferred from the link.
Other room operations #
client.rooms currently provides these methods:
| Category | Methods |
|---|---|
| Create | createQuickAudioRoom, createQuickVideoRoom, createScheduledAudioRoom, createScheduledVideoRoom |
| Enter/share | joinRoom, createInvitationLink |
| Scheduled | startScheduledRoom |
| Inspect | getRoomStatus, getActiveRoomInfo, getActiveRoomsInfo, fetchPastRooms |
| End | endRoom |
All resource methods return Future<Map<String, dynamic>>. This deliberately
preserves the server response while API response schemas remain flexible. Read
only the fields your integration needs, such as room_id and final_link.
Analytics, projects, and recordings #
client.analytics.getAnalytics(roomId: ...) and all client.recordings
methods require project credentials. Project management methods require
organization credentials, so initialize the client with both scopes when your
app uses all resources:
final qriib = QriibMeetClient.withCredentials(
organization: const QriibOrganizationCredentials(
apiKey: 'YOUR_ORGANIZATION_API_KEY',
secretKey: 'YOUR_ORGANIZATION_SECRET_KEY',
),
project: const QriibProjectCredentials(
apiKey: 'YOUR_PROJECT_API_KEY',
secretKey: 'YOUR_PROJECT_SECRET_KEY',
),
);
final analytics = await qriib.analytics.getAnalytics(roomId: 'ROOM_ID');
final recordings = await qriib.recordings.getRecords(projectId: 'PROJECT_ID');
final project = await qriib.projects.getProject(projectId: 'PROJECT_ID');
All three resources return Map<String, dynamic> so every server field stays
available while the API response schemas remain undocumented.
Update a project by supplying at least one documented field. Nested settings are passed as JSON-shaped collections because their inner schema is API-owned:
await qriib.projects.updateProject(
projectId: 'PROJECT_ID',
defaultProject: true,
webhookUrl: 'https://example.com/qriib-webhook',
roomFeatures: const [
{'chat': true},
],
defaultLockSettings: const {
'locked': true,
},
);
Meeting behavior #
meetings.join parses the link, requests media permissions, connects and
initializes local media, then pushes the embedded meeting route. The caller
must handle pre-route progress/errors; a loadingBuilder on the meeting view
is not a pre-connection screen. The current signature accepts:
Future<void> join({
required BuildContext context,
required String finalLink,
String? roomId,
String? userName,
String? userImage,
String? mediaServerUrl,
bool audioOnly = false,
bool? initialWhiteboardOpen,
QriibMeetingUiConfig? ui,
VoidCallback? onDisconnected,
})
The package extracts the required internal credentials from finalLink, then
the embedded Qriib meeting handles camera, microphone, participants, leaving,
and requesting that the room end for everyone. A non-null roomId enables the
End for everyone UI; it is a presence guard, not moderator validation.
The request needs a project-signed management transport (as supplied by
client.meetings), and the server is responsible for authorization. Supplying
a room ID to standalone QriibMeetings() does not add that transport or grant
permission. onDisconnected runs after the pushed meeting route returns;
pre-route connection failures instead propagate to the caller.
| Parameter | Use |
|---|---|
context |
The Flutter context used to push the full-screen meeting route. |
finalLink |
Required. Pass the complete, fresh server response unchanged. |
roomId |
Optional; a non-null value exposes End for everyone, without checking participant role or transport availability. |
audioOnly |
Defaults to false; set true explicitly for quick, scheduled, and existing audio rooms. |
initialWhiteboardOpen |
Explicit value, including false, overrides ui.initialWhiteboardOpen; otherwise defaults to that UI setting, then false. |
ui |
Optional built-in styles and widget builders. |
userName, userImage |
Accepted but currently unused by join; they do not override participant identity/name/image from the connected session. |
mediaServerUrl |
Optional Qriib-provided staging/self-hosted endpoint override. |
onDisconnected |
Optional callback after the meeting route closes. |
mediaServerUrl is optional and defaults to the Qriib meeting endpoint. Use
it only for a Qriib-provided staging or self-hosted endpoint; never place media
secrets in the client application.
Never pass only one decoded token, a room ID, or an invitation URL to
meetings.join: it requires the complete fresh final_link returned by the
server for that participant.
Whiteboard #
The public barrel exports QriibWhiteboardView, QriibWhiteboardConfig, token
and config-update request/response models, JwtTokenGenerator,
WhiteboardService, and native bridge/dock types. The service's lock/unlock
operations (lockUserWhiteboard, unlockUserWhiteboard,
lockRoomWhiteboard, unlockRoomWhiteboard) require a repository; there is no
client.whiteboard facade. The repository types are not exported by the public
barrel, so this service is not a turnkey public-barrel-only management entry.
Open the built-in board with initialWhiteboardOpen: true on join, or with
QriibMeetingUiConfig(initialWhiteboardOpen: true). The join argument takes
precedence, including an explicit false. Custom meeting controls can call
actions.setWhiteboardOpen(true) or actions.toggleWhiteboard(); the control
enum includes QriibMeetingControl.whiteboard.
The built-in meeting supplies only room ID, participant identity/name, and a
close callback. It uses the standalone view's defaults, including editable
mode, light theme, English, and production config. join and
QriibMeetingUiConfig do not accept a QriibWhiteboardConfig, board permissions,
language, or bridge controller. Use a separately hosted view to configure them:
QriibWhiteboardView(
roomId: roomId,
identity: participantIdentity,
name: participantName,
canEdit: false,
theme: 'dark',
lang: 'en',
config: const QriibWhiteboardConfig(),
onClose: closeWhiteboard,
)
The standalone view also accepts color, controller, showTopBar,
showDockControls, interactionEnabled, and gestureRecognizers.
QriibWhiteboardConfig supplies url, apiUrl, apiKey, and jwtSecret, with
production/development/staging presets and copyWith. Native bridge/dock APIs
support drawing tools and board controls; for example,
WhiteboardNativeBridgeController.selectTool, undo, redo, addPage,
goToPage, zoomIn, zoomOut, fitToContent, and clearCanvas. Dispatch
depends on board readiness and edit state. These are separate from the two
meeting-level open/close actions.
Security limitation: shipped defaults include client-visible API and HS256
signing keys, and the board token is signed in the client. Overriding a key in
client configuration does not make it secret. canEdit and UI visibility are
not authorization boundaries. A production deployment needs server-enforced
access policy; moving signing behind a backend requires integration changes,
not merely changing these examples. Do not log tokens or reproduce key values.
Customize the meeting UI #
For data-only styling, set QriibMeetingUiConfig.style to a
QriibMeetingStyle. It supports global colors/text plus appBar, controls,
participantTile, participantsPanel, leaveDialog, and feedback styles.
For matching global properties, precedence is style → legacy direct UI
property → SDK default. Region styles refine the built-in widgets; a custom
builder replaces its region and is responsible for its own styling.
const brandedUi = QriibMeetingUiConfig(
style: QriibMeetingStyle(
accentColor: Color(0xff7C5CFC),
appBar: QriibMeetingAppBarStyle(title: 'Team room'),
controls: QriibMeetingControlsStyle(
microphone: QriibMeetingControlStyle(tooltip: 'Microphone'),
),
),
);
Pass QriibMeetingUiConfig when opening the meeting to brand individual
regions or replace them with your own Flutter widgets. The SDK still owns
connection state, media permissions, camera/microphone actions, participant
state, leaving, ending the room, and route cleanup.
await qriib.meetings.join(
context: context,
finalLink: finalLink,
roomId: roomId,
ui: QriibMeetingUiConfig(
backgroundColor: const Color(0xff0B1020),
accentColor: const Color(0xff7C5CFC),
appBarBuilder: (_, chrome) => AppBar(
title: Text('Team room · ${chrome.participantCount} online'),
),
participantTileBuilder: (_, tile) => Stack(
fit: StackFit.expand,
children: [
if (tile.hasVideo) tile.video else tile.placeholder,
Positioned(
left: 12,
bottom: 12,
child: Text(tile.participant.name),
),
],
),
controlsBuilder: (_, controls) => Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
tooltip: 'Mute',
icon: const Icon(Icons.mic_off),
onPressed: () => controls.actions.setMicrophoneEnabled(false),
),
FilledButton.icon(
icon: const Icon(Icons.call_end),
label: const Text('Leave'),
onPressed: controls.actions.requestLeave,
),
],
),
),
);
Available widget slots are appBarBuilder, participantTileBuilder,
controlsBuilder, participantsSheetBuilder, leaveDialogBuilder,
loadingBuilder, errorBuilder, and emptyVideoBuilder. For lighter
customization, use backgroundColor, surfaceColor, accentColor,
dangerColor, textStyle, controlIconBuilder, and
controlTooltipBuilder.
Always invoke microphone, camera, participants, leave, and end-for-all through
QriibMeetingActions from the builder context. The contexts intentionally do
not expose media-engine types, room objects, credentials, or direct navigator
cleanup controls.
Errors #
Handle SDK errors at the call site and show only safe messages to the user:
try {
await createAndOpenVideoRoom(context);
} on QriibMeetException catch (error) {
showMessage(error.message);
} on QriibApiException catch (error) {
showMessage(error.message);
}
QriibMeetException covers local validation, including a missing or malformed
meeting link and local meeting/device errors. QriibSessionException and
QriibNetworkException are meeting-specific subclasses. QriibApiException
represents a failed API request and includes the server response and HTTP
status for application-level error handling.
For API errors, prefer error.message; it contains a meaningful server message
when one is available, for example Client room ID already exists. Do not show
or log raw request credentials or meeting links.
Platform requirements #
The package declares Dart >=3.6.0 <4.0.0 and Flutter >=3.27.0; its own
Android plugin declares minSdk = 21, compileSdk = 34, and its iOS pod declares
13.0. These are package declarations, not a verified sufficient host baseline.
Transitive media, permission, and WebView plugins can require newer SDKs,
deployment targets, build tools, or OS versions.
The current local resolution used Flutter 3.35.7 / Dart 3.9.2 and selected
webview_flutter 4.13.1, webview_flutter_android 4.12.0, and
webview_flutter_wkwebview 3.25.0. Installed metadata for these platform
implementations requires Dart 3.9 / Flutter 3.35; the Android implementation
requires minSdk = 24 and uses Flutter's compile SDK. Android 21 and the
package's declared Flutter floor therefore do not suffice for this resolution.
The installed WKWebView podspec/Swift package and flutter_webrtc 1.2.1 podspec
declare iOS 13; the latter depends on WebRTC-SDK 137.7151.04. That does not
validate the complete transitive CocoaPods graph or prove an iOS 13 host works.
A successful pub get verifies dependency resolution, not native build/runtime
compatibility. Inspect the resolved plugin
metadata and validate both native host builds and physical-device behavior;
do not assume Android 21 or iOS 13 is sufficient. Recheck after dependency
updates rather than treating this resolution as a permanent support promise.
Host application permissions #
Before connecting and pushing the route, the package requests microphone
access and, unless audioOnly: true, camera access. Its
Android plugin declares the required Android permissions automatically. Host
apps must provide meaningful iOS usage descriptions in
ios/Runner/Info.plist:
<key>NSCameraUsageDescription</key>
<string>Camera access is used for Qriib meetings.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is used for Qriib meetings.</string>
For CocoaPods-based iOS apps, enable the two permission-handler flags in the
app's ios/Podfile post_install block:
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'PERMISSION_CAMERA=1'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << 'PERMISSION_MICROPHONE=1'
The embedded meeting requests media access when the user enters the meeting flow. A simulator may not provide a real camera or microphone; in that case the meeting can still open with local media disabled. Verify camera and microphone behavior on physical Android and iOS devices before releasing your app.
Further documentation #
See the developer guide and the engineering methodology. Existing PDF exports are historical snapshots; this update does not regenerate or synchronize them.
Copy lib/config.example.dart to lib/config.dart and provide test-only
credentials before running it. Never commit that generated configuration file.