qriib_meet

Flutter room and meeting APIs for Qriib applications.

Getting started

Add the package to your Flutter application, create a single client from trusted credentials, create or join a room, then open the fresh final_link returned by Qriib. The SDK owns request signing and the embedded meeting experience; your application owns its navigation, identity, and secure credential delivery.

QriibMeetClient keeps room REST calls and the meeting experience together.

The identifiers and room settings below are application-owned values. Obtain them from authenticated application state or your backend; do not copy user data, credentials, or environment-specific values into source code.

Use the built-in newClientRoomId() for every create-room request. It creates a cryptographically random UUID-style string and contains no user data, credentials, or room metadata.

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

final response = await qriib.rooms.createQuickVideoRoom(
  projectId: projectId,
  moderatorId: moderatorId,
  name: participantName,
  maxParticipants: roomCapacity,
  emptyTimeout: emptyRoomTimeoutSeconds,
  clientRoomId: newClientRoomId(),
  metadata: QriibRoomMetadata(
    roomTitle: roomTitle,
    welcomeMessage: welcomeMessage,
  ),
);

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,
  configuration: meetingConfiguration,
);

Use the built-in finalLinkFromResponse to read the fresh final_link out of any create, join, or start response. It accepts both the response['data']['final_link'] envelope and a top-level final_link, and returns null when no usable link is present.

QriibMeetingConfiguration is optional. Without it, the package displays its premium default meeting view. The supplied builders receive Qriib session and participant types only; consumers do not need to import the media engine.

Always obtain a fresh final_link before joining. Do not store or log it.

Meeting UI configuration

Pass a QriibMeetingConfiguration to join to style the built-in meeting surface or replace individual regions. All options are optional; omit a builder to retain that part of the premium default UI.

final meetingConfiguration = QriibMeetingConfiguration(
  title: meetingTitle,
  theme: QriibMeetingTheme(
    backgroundColor: meetingBackgroundColor,
    surfaceColor: meetingSurfaceColor,
    primaryColor: brandColor,
    dangerColor: leaveButtonColor,
    tileColor: participantTileColor,
    participantNameStyle: participantNameTextStyle,
    topBarTextStyle: topBarTextStyle,
    cameraOnIcon: cameraOnIcon,
    cameraOffIcon: cameraOffIcon,
    microphoneOnIcon: microphoneOnIcon,
    microphoneOffIcon: microphoneOffIcon,
    screenShareOnIcon: screenShareOnIcon,
    screenShareOffIcon: screenShareOffIcon,
    switchCameraIcon: switchCameraIcon,
    leaveIcon: leaveIcon,
    speakingColor: activeSpeakerHighlight,
  ),
  showTopBar: showTopBar,
  showParticipantNames: showParticipantNames,
  showCameraControl: showCameraControl,
  showSwitchCameraControl: showSwitchCameraControl,
  showMicrophoneControl: showMicrophoneControl,
  showScreenShareControl: showScreenShareControl,
  showAudioDeviceControl: showAudioDeviceControl,
  showLeaveControl: showLeaveControl,
  showParticipantsList: true,
  showViewModeToggle: true,
  showMeetingTimer: true,
  confirmLeave: true,
  initialViewMode: QriibMeetingViewMode.speaker,
  enableViewModeSwipe: true,
  controlsAutoHideDelay: const Duration(seconds: 5),
  videoAvailable: roomAllowsVideo,
  onCameraChanged: (session, enabled) {
    // Update app-owned UI or analytics without recording sensitive media data.
  },
  onMicrophoneChanged: (session, enabled) {
    // Update app-owned UI or analytics without recording sensitive media data.
  },
  onScreenShareChanged: (session, enabled) {},
  onLeaveRequested: (session) async {
    // Return false to keep the participant in the meeting. When supplied, this
    // replaces the built-in leave confirmation sheet.
    return await confirmLeaveWithUser();
  },
);

Meeting experience

A fuller walkthrough of the layout, every flag, and the widget-test keys is in docs/meeting-ui.md.

The default view follows the conventions people know from mainstream video-conferencing apps, and ships with the familiar toolbar out of the box: a bottom bar with Mute · Stop Video · Share · Participants, and a top bar with the audio-route and flip-camera buttons on the left, the title and timer in the middle, and a red Leave button on the right. Every element is a flag you can switch off, and the bottom bar scales itself down on narrow phones rather than overflowing. Hiding the top bar moves its utilities and Leave into the bottom bar so nothing becomes unreachable.

  • Speaker view and gallery view. Swipe horizontally (or use the top-bar toggle) to move between a single large stage and an equal grid. initialViewMode picks the opening layout and enableViewModeSwipe: false locks it. In speaker view the stage shows an active screen share first, then a pinned participant (long-press any tile to pin, long-press again to unpin), then whoever spoke most recently.
  • Draggable self-view. In speaker view your own camera floats in a small thumbnail that can be dragged anywhere and snaps to the nearest corner.
  • Screen sharing. session.setScreenShareEnabled(true) (or the share control) publishes the screen. The sharer is spotlighted for everyone; in gallery view the other participants collapse into a filmstrip beneath it. On Android the package asks for the screen-capture consent, then starts the required mediaProjection foreground service, then begins capturing — the order Android 14+ enforces — and declares the permissions in its own manifest, so host apps need no extra setup.
  • Front/back camera. session.switchCamera() flips lenses; the default control appears only where QriibMeetingState.canSwitchCameraPosition is true (mobile) and the camera is on.
  • Live tile badges. Every tile highlights the active speaker with speakingColor, shows a muted-mic badge, an initial avatar when the camera is off, and a warning glyph when connectionQuality is poor or lost. QriibMeetingParticipant exposes isMicrophoneEnabled, isCameraEnabled, isSpeaking, isScreenSharing and connectionQuality for custom tiles.
  • Auto-hiding chrome. The top bar and controls slide away after controlsAutoHideDelay (5 s by default) and return on tap. Pass null to keep them always visible.
  • Meeting timer, participants sheet, leave confirmation. The top bar shows the elapsed time and a participant count that opens a sheet listing everyone with their mic/camera state (showMeetingTimer, showParticipantsList). Leaving asks for confirmation (confirmLeave) unless you supply your own onLeaveRequested.

Visibility and audio-only behavior

showTopBar, showParticipantNames, showCameraControl, showSwitchCameraControl, showMicrophoneControl, showScreenShareControl, showAudioDeviceControl, showLeaveControl, showParticipantsList, showViewModeToggle, and showMeetingTimer hide default elements. videoAvailable describes whether the meeting has video at all: set it to false for rooms created with createQuickAudioRoom or createScheduledAudioRoom so your own builders can render an audio-only surface instead of a camera frame.

When you replace controls for an audio-only room, keep a visible, disabled camera-off control with an accessible explanation rather than hiding it, so participants can tell that video is unavailable rather than broken.

Pre-join screen

The lobby screen is opt-in. Enable it to let participants confirm their camera, microphone, and audio devices before the meeting connects:

final meetingConfiguration = QriibMeetingConfiguration(
  preJoin: QriibPreJoinConfiguration(
    enabled: true,
    title: meetingTitle,
    showDeviceSelector: true,
  ),
);

The camera and microphone states chosen in the lobby are carried into the meeting. Cancelling the lobby aborts the join without connecting.

Replace individual regions

The three builders receive neutral Qriib types and a QriibMeetingSession:

final meetingConfiguration = QriibMeetingConfiguration(
  participantBuilder: (context, participant, video, session) {
    // `video` is the SDK-provided renderer. Do not retain it when local video
    // is disabled; listen to the neutral session state instead.
    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
            ? fallbackInitial
            : participant.label.characters.first.toUpperCase();
        return ColoredBox(
          color: participantTileColor,
          child: Center(child: CircleAvatar(child: Text(initial))),
        );
      },
    );
  },
  topBarBuilder: (context, state, session) => MeetingHeader(
    title: meetingTitle,
    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,
  ),
);

When replacing controls, provide the same accessible labels, focus behavior, disabled video indication for audio-only rooms, and a way to leave. When replacing participant tiles, do not render a camera frame when the relevant camera state is off; show an initial/avatar on a fixed background instead.

Audio device configuration

Participants can choose which microphone they are heard through and which speaker they hear the meeting on, both before joining and mid-call.

The built-in audio-route control

The default meeting view ships an audio-route button at the left of its top bar (next to the flip-camera button; it drops into the bottom bar when showTopBar is false). Tapping it opens a picker whose contents depend on what the platform reports:

  • Mobile — a single Speaker switch that routes audio between the earpiece and the loudspeaker. Wired headsets and Bluetooth earbuds still take priority while they are connected. The button icon follows the current route.
  • Desktop and web — a Microphone list and a Speaker list, each showing the OS-provided device names with the active device marked. Selecting an entry switches to it immediately.

The button only renders when the platform reports at least one of these capabilities, so nothing appears on a device with a single fixed route. Set QriibMeetingConfiguration(showAudioDeviceControl: false) to hide it and build your own picker instead.

The opt-in pre-join lobby offers the same choices before connecting: a speaker toggle on mobile, and microphone/speaker chips on desktop and web when QriibPreJoinConfiguration(showDeviceSelector: true). The device chosen in the lobby carries into the meeting.

Driving it yourself

QriibMeetingSession (and QriibPreJoinSession before joining) exposes the same state and operations to your own builders:

final state = session.state.value;

// Mobile: toggle the speaker on/off; wired/Bluetooth devices still take
// priority when connected.
await session.setSpeakerphoneOn(!state.speakerOn);

// Desktop/web: target a specific device by id.
await session.selectAudioInput(state.audioInputs.first);
await session.selectAudioOutput(state.audioOutputs.first);

state.audioInputs and state.audioOutputs are lists of QriibAudioDevice (id, label, and a kind of QriibAudioDeviceKind.input or .output); state.selectedAudioInputId and state.selectedAudioOutputId identify the active ones. Check state.canToggleSpeakerphone (mobile) or state.canSelectSpecificDevice (desktop/web) to decide which control to show — rendering a device list on a platform that only supports a speaker toggle leaves the user with a picker that cannot change anything.

Host permissions

Add these declarations to the host app's android/app/src/main/AndroidManifest.xml, directly under <manifest>:

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

Screen sharing needs nothing further on Android: the package's own manifest declares FOREGROUND_SERVICE, FOREGROUND_SERVICE_MEDIA_PROJECTION and the mediaProjection foreground service it runs during a share, and these merge into the host app automatically. See docs/screen-sharing.md for the platform details and troubleshooting.

On iOS, screen sharing without a Broadcast Upload Extension captures only this app's own screen (ReplayKit in-app recording). Sharing other apps or the home screen requires a broadcast extension, which this release does not yet wire up; see the iOS notes in docs/screen-sharing.md.

On iOS, add purpose strings to the host application's 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>

permission_handler_apple also requires the iOS host's Podfile to enable the camera and microphone permission macros. Add these definitions to every Pods build configuration in the post_install hook:

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

Transport and credentials

qriib_meet owns the signed Qriib V4 transport. Applications do not create a second HTTP client, manually set key or hash-signature, or depend on a separate V.cloud package. Keep project API credentials and fresh final_link values out of source control and application logs.

Room workflows

Use a single QriibMeetClient for the lifetime of the host widget, then call close() from dispose().

Quick rooms

Supply a unique clientRoomId for each create request. Quick video rooms can be opened immediately from the response:

final response = await qriib.rooms.createQuickVideoRoom(
  projectId: projectId,
  clientRoomId: newClientRoomId(),
  moderatorId: moderatorId,
  name: participantName,
  maxParticipants: roomCapacity,
  emptyTimeout: emptyRoomTimeoutSeconds,
  metadata: QriibRoomMetadata(
    roomTitle: roomTitle,
    welcomeMessage: welcomeMessage,
  ),
);

final finalLink = finalLinkFromResponse(response);

Use createQuickAudioRoom for an audio-only room, then join it with a configuration whose videoAvailable is false.

Scheduled rooms

Use createScheduledVideoRoom or createScheduledAudioRoom with the same room fields plus an ISO-8601 startAt value. When it is time to open the room, call:

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

Read the fresh final_link from that response with finalLinkFromResponse before joining.

Join and manage rooms

await qriib.rooms.joinRoom(
  roomId: roomId,
  userInfo: const QriibUserInfo(
    name: participantName,
    role: 'attendee',
    isAdmin: false,
    isHidden: false,
  ),
);

The client also provides createInvitationLink, endRoom, getRoomStatus, getActiveRoomInfo, getActiveRoomsInfo, and fetchPastRooms. Catch QriibApiException and QriibMeetException to show their user-safe message values.

Complete Rooms API map

Inputs marked "room ID" must refer to the room returned by a previous create response.

Operation Method and required inputs Result / next step
Quick video createQuickVideoRoom(projectId, clientRoomId, metadata, name, moderatorId, maxParticipants, emptyTimeout) Read final_link, then join with video enabled.
Quick audio createQuickAudioRoom(...) with the same fields Read final_link, then join with videoAvailable: false.
Scheduled video createScheduledVideoRoom(projectId, clientRoomId, startAt, metadata, maxParticipants, emptyTimeout) Store the returned room ID and start it later.
Scheduled audio createScheduledAudioRoom(...) with the same scheduled fields Start later, then join with videoAvailable: false.
Start scheduled startScheduledRoom(roomId, name: participantName) Read its fresh final_link; match the configuration to the room type.
Join joinRoom(roomId: roomId, userInfo: QriibUserInfo(...)) Read its fresh final_link and open the meeting.
Invitation createInvitationLink(roomId: roomId, role: QriibInvitationRole.attendee) Share the returned HTTP(S) link only with the intended participant.
Inspect active room getActiveRoomInfo(roomId) Returns current active-room details.
Inspect all active rooms getActiveRoomsInfo() Returns the active-room collection.
Status getRoomStatus(roomId) Returns the current lifecycle status.
Past rooms fetchPastRooms(projectId: projectId, from: offset, limit: pageSize, orderBy: QriibPastRoomsOrder.desc) Returns a paged, newest-first history.
End endRoom(roomId) Ends an active room; confirm this action with the user before invoking it in production.

final_link is short-lived meeting access data. Read it with finalLinkFromResponse, use it immediately, and do not persist it.

Run the example

The example app is a generated Android/iOS smoke host. It reads QRIIB_API_KEY, QRIIB_SECRET_KEY, and a fresh QRIIB_FINAL_LINK from compile-time environment definitions; it contains no credential or link values.

Run it on a trusted device with values supplied only at build time:

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

Supply these through your development or CI secret store. Treat all credentials and final links as sensitive: do not commit, log, or send them through analytics.

Libraries

qriib_meet