qriib_meet 0.2.1 copy "qriib_meet: ^0.2.1" to clipboard
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.

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';.

  1. Create one QriibMeetClient when your feature starts.
  2. Call a room API such as createQuickVideoRoom or joinRoom.
  3. Read the server-returned final_link and call client.meetings.join.
  4. 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-issued final_link.
  • client.analytics, client.projects, and client.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.