qriib_meet 0.2.0
qriib_meet: ^0.2.0 copied to clipboard
Qriib Flutter room management and embedded meeting APIs.
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.
initialViewModepicks the opening layout andenableViewModeSwipe: falselocks 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 requiredmediaProjectionforeground 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 whereQriibMeetingState.canSwitchCameraPositionis 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 whenconnectionQualityis poor or lost.QriibMeetingParticipantexposesisMicrophoneEnabled,isCameraEnabled,isSpeaking,isScreenSharingandconnectionQualityfor custom tiles. - Auto-hiding chrome. The top bar and controls slide away after
controlsAutoHideDelay(5 s by default) and return on tap. Passnullto 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 ownonLeaveRequested.
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.