flutter_amazon_chime
A Flutter plugin for Amazon Chime SDK meetings on iOS and Android.
Disclaimer: This package is not affiliated with, endorsed by, or sponsored by Amazon Web Services (AWS). "Amazon Chime" is a trademark of Amazon.com, Inc.
Table of Contents
- Quick Start
- Installation
- Permissions Setup
- iOS Screen Share Setup
- Meeting Credentials
- Usage Levels
- Data Messages
- Audio Device Management
- Meeting Quality & Analytics
- Error Handling
- Troubleshooting
- Models Reference
- Streams Reference
Quick Start
The fastest way to get a meeting running:
import 'package:flutter_amazon_chime/flutter_amazon_chime.dart';
import 'package:provider/provider.dart';
// 1. Request permissions before joining
await AmazonChime.instance.requestAudioPermissions();
await AmazonChime.instance.requestVideoPermissions();
// 2. Join the meeting
await AmazonChime.instance.joinMeeting(joinInfo);
// 3. Initialize the session with the roster (attendeeId → display name)
session.initializeMeeting(
joinInfo: joinInfo,
roster: {'attendee-id-123': 'Alice', 'attendee-id-456': 'Bob'},
);
// 4. Show the built-in meeting UI
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ChangeNotifierProvider(
create: (_) => ChimeSession(),
child: AmazonChimeView(
title: 'My Meeting',
onLeave: () => Navigator.pop(context),
),
),
),
);
Installation
Add to pubspec.yaml:
dependencies:
flutter_amazon_chime: ^<version>
provider: ^6.0.0
Permissions Setup
Android
Add to android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<!-- Android 13+ only, for screen share notification -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
iOS
Add to ios/Runner/Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is needed for meeting audio.</string>
<key>NSCameraUsageDescription</key>
<string>Camera access is needed for video.</string>
Requesting permissions at runtime
Check and request permissions before joining. Calling joinMeeting without audio permissions will fail.
// Check without prompting the user
final hasAudio = await AmazonChime.instance.hasAudioPermissions();
final hasVideo = await AmazonChime.instance.hasVideoPermissions();
// Show the system permission dialog
await AmazonChime.instance.requestAudioPermissions(); // throws ChimePermissionException if denied
await AmazonChime.instance.requestVideoPermissions(); // throws ChimePermissionException if denied
iOS Screen Share Setup
Screen sharing on iOS requires a Broadcast Upload Extension target and an App Group shared between the extension and your main app. This is an Apple platform requirement — the plugin cannot ship the extension target for you.
Steps
-
In Xcode, add a new target: File → New → Target → Broadcast Upload Extension. Any name works (the examples below assume
ScreenShareExt). -
Enable the App Groups capability on both the main app target and the extension target, and use the same group ID on both (e.g.
group.com.example.app.screenshare). -
In your extension's
SampleHandler.swift, read meeting credentials from the sharedUserDefaults(suiteName:)and forward sample buffers to Chime. The plugin writes the credentials to the App Group automatically whenjoinMeetingis called. -
Pass
appGroupIdonJoinInfo:final joinInfo = JoinInfo( // ... other fields ... appGroupId: 'group.com.example.app.screenshare', // iOS only );
Naming the extension
The plugin needs to know your extension's bundle ID in order to present the system broadcast picker. It resolves it in this order:
JoinInfo.screenShareExtensionId(if set), e.g.'com.example.app.MyBroadcastExt'.ChimeScreenShareExtensionkey in the main app'sInfo.plist.- Fallback:
<mainAppBundleId>.ScreenShareExt.
If your extension is named ScreenShareExt and sits under your main app's bundle ID you don't need to do anything. Otherwise pick one of the overrides:
// Per-meeting override
final joinInfo = JoinInfo(
// ... other fields ...
appGroupId: 'group.com.example.app.screenshare',
screenShareExtensionId: 'com.example.app.MyBroadcastExt',
);
<!-- ios/Runner/Info.plist — app-wide override -->
<key>ChimeScreenShareExtension</key>
<string>com.example.app.MyBroadcastExt</string>
Android screen share does not require any additional setup.
Meeting Credentials
JoinInfo holds all credentials returned by your server after creating a meeting and attendee via the AWS Chime API.
final joinInfo = JoinInfo(
meetingId: 'abc-123',
externalMeetingId: 'my-room',
mediaRegion: 'us-east-1',
audioHostUrl: 'https://...',
audioFallbackUrl: 'https://...',
signalingUrl: 'wss://...',
turnControlUrl: 'https://...',
attendeeId: 'attendee-id-123',
externalUserId: 'app#alice', // convention: "appId#username"
joinToken: 'join-token-abc',
appGroupId: 'group.com.example.app.screenshare', // iOS screen share only
screenShareExtensionId: 'com.example.app.ScreenShareExt', // iOS screen share only — optional
);
JoinInfo.fromJson parses the AWS Chime API response directly:
final joinInfo = JoinInfo.fromJson(responseJson);
Usage Levels
Level 1 — Drop-in UI (AmazonChimeView)
AmazonChimeView provides a complete, production-ready meeting UI out of the box:
- Participant grid — adapts layout for 1 to 7+ participants
- Meeting controls — mute, video toggle, camera switch (long-press the video button), audio device picker, screen share, leave
- Screen share — full-screen content view with participant strip
- Connection quality banner — shown when connection degrades
- Reconnecting overlay — shown during audio session recovery
- Active speaker highlighting — border highlight on the active speaker tile
ChangeNotifierProvider(
create: (_) => ChimeSession(),
child: AmazonChimeView(
title: 'My Meeting', // optional — displayed at the top
onLeave: () {}, // optional — called after meeting stops and view pops
tileBuilder: (ctx, attendee, isLocal) { // optional — custom participant tile
return MyCustomTile(attendee: attendee, isLocal: isLocal);
},
),
)
Important: AmazonChimeView requires a ChimeSession ancestor in the widget tree. Wrap it with ChangeNotifierProvider<ChimeSession> as shown above.
Call session.initializeMeeting(...) after joinMeeting to populate the roster before pushing AmazonChimeView.
Level 2 — Custom UI with ChimeSession
Use ChimeSession directly to build your own layout while still getting automatic state management. All state updates trigger notifyListeners().
final session = context.watch<ChimeSession>();
State available on ChimeSession:
| Property | Type | Description |
|---|---|---|
isMeetingActive |
bool |
Whether a meeting is currently active |
localAttendeeId |
String? |
The local attendee's ID |
localAttendee |
Attendee? |
The local attendee object |
isLocalMuted |
bool |
Whether the local attendee is muted |
isLocalVideoOn |
bool |
Whether local video is active |
currAttendees |
Map<String, Attendee> |
All attendees keyed by ID (read-only view) |
allParticipants |
List<Attendee> |
All attendees excluding content share |
remoteAttendees |
List<Attendee> |
Remote attendees only |
participantCount |
int |
Total participant count |
roster |
Map<String, String> |
attendeeId → display name (read-only view) |
activeSpeakers |
List<String> |
Currently active speaker IDs (read-only view) |
isActiveSpeaker(id) |
bool |
Check if an attendee is speaking |
activeCameraFacing |
String? |
"front", "back", or null |
isConnectionPoor |
bool |
Connection quality degraded |
isReconnecting |
bool |
Audio session is reconnecting |
shouldShowScreenShare |
bool |
A remote screen share is active |
isLocalScreenSharing |
bool |
Local attendee is sharing their screen |
deviceList |
List<String> |
Available audio device labels (read-only view) |
selectedAudioDevice |
String? |
Currently active audio device label |
Actions available on ChimeSession:
All action methods return Future<void> and rethrow on failure after logging, so you can await and catch them if you need error handling. Fire-and-forget usage (no await) is also safe.
session.initializeMeeting(joinInfo: joinInfo, roster: roster);
// Fire-and-forget (errors logged internally)
session.sendLocalMuteToggle();
session.sendLocalVideoTileOn();
session.switchCamera();
session.sendLocalScreenShareToggle();
session.updateCurrentDevice(deviceLabel);
session.stopMeeting();
// Awaited with error handling
try {
await session.sendLocalMuteToggle();
} on ChimeDeviceException catch (e) {
showError(e.message);
}
Reusable sub-widgets:
These are the same widgets used inside AmazonChimeView, available for custom layouts:
// Display a single participant's video (or avatar if video is off)
ParticipantTile(
attendee: attendee,
displayName: 'Alice',
isActiveSpeaker: session.isActiveSpeaker(attendee.attendeeId),
)
// Display a raw video tile by tile ID
VideoTile(tileId: attendee.videoTileInfo?.tileId)
// Adaptive multi-participant grid
ParticipantGrid(
participants: session.allParticipants,
localAttendeeId: session.localAttendeeId,
roster: session.roster,
activeSpeakers: session.activeSpeakers.toSet(),
)
// The full meeting controls bar
MeetingControls()
Example — custom layout:
Scaffold(
body: Column(
children: [
Expanded(
child: ParticipantGrid(
participants: session.allParticipants,
localAttendeeId: session.localAttendeeId,
roster: session.roster,
activeSpeakers: session.activeSpeakers.toSet(),
),
),
MeetingControls(),
],
),
)
Level 3 — Fully Headless (AmazonChime.instance)
Skip AmazonChimeView and ChimeSession entirely. Call the SDK directly and manage all state yourself.
Meeting lifecycle
await AmazonChime.instance.joinMeeting(joinInfo);
await AmazonChime.instance.stopMeeting();
Audio
await AmazonChime.instance.mute();
await AmazonChime.instance.unmute();
final devices = await AmazonChime.instance.listAudioDevices(); // List<String>
final current = await AmazonChime.instance.initialAudioSelection(); // String
await AmazonChime.instance.updateAudioDevice('Speaker'); // by label
Video
await AmazonChime.instance.startLocalVideo();
await AmazonChime.instance.stopLocalVideo();
await AmazonChime.instance.switchCamera();
final facing = await AmazonChime.instance.activeCamera(); // "front", "back", or null
// Limit video bandwidth (0 = SDK default)
await AmazonChime.instance.setLocalVideoMaxBitrate(500); // kbps
Remote video subscriptions
Subscribe to remote attendees' video. Call this in response to onRemoteVideoSourcesAvailable:
AmazonChime.instance.onRemoteVideoSourcesAvailable.listen((sources) async {
await AmazonChime.instance.updateVideoSourceSubscriptions(toAdd: sources);
});
AmazonChime.instance.onRemoteVideoSourcesUnavailable.listen((sources) async {
await AmazonChime.instance.updateVideoSourceSubscriptions(
toAdd: [],
toRemove: sources,
);
});
Screen share
await AmazonChime.instance.startScreenShare();
await AmazonChime.instance.stopScreenShare();
Listening to events
AmazonChime.instance.onAttendeeJoined.listen((attendee) { });
AmazonChime.instance.onAttendeeLeft.listen((attendee) { });
AmazonChime.instance.onAttendeeMuted.listen((attendee) { });
AmazonChime.instance.onVideoTileAdded.listen((tile) { });
AmazonChime.instance.onActiveSpeakersChanged.listen((ids) { });
AmazonChime.instance.onConnectionQualityChanged.listen((isPoor) { });
AmazonChime.instance.onAudioSessionStarted.listen((_) { });
AmazonChime.instance.onAudioSessionStopped.listen((_) { });
// See full list in Streams Reference below
Data Messages
Data messages let attendees exchange arbitrary real-time payloads on named topics. Common uses: chat, emoji reactions, polls, whiteboard sync.
Sending
await AmazonChime.instance.sendDataMessage(
'chat', // topic name
'Hello!', // UTF-8 string payload
lifetimeMs: 0, // 0 = not retained for late joiners; max 300,000 ms
);
Receiving
AmazonChime.instance.onDataMessageReceived.listen((DataMessage msg) {
print('${msg.senderExternalUserId} on ${msg.topic}: ${msg.data}');
if (msg.throttled) {
// message was throttled server-side, handle gracefully
}
});
DataMessage fields:
| Field | Type | Description |
|---|---|---|
topic |
String |
The topic the message was sent on |
data |
String |
The UTF-8 string payload |
senderAttendeeId |
String |
Sender's Chime attendee ID |
senderExternalUserId |
String |
Sender's external user ID |
timestampMs |
int |
Server timestamp (ms since epoch) |
throttled |
bool |
Whether the server throttled this message |
Audio Device Management
// List available devices (speakers, Bluetooth, wired headset, etc.)
final List<String> devices = await AmazonChime.instance.listAudioDevices();
// Get the currently active device
final String current = await AmazonChime.instance.initialAudioSelection();
// Switch to a specific device by label
await AmazonChime.instance.updateAudioDevice('Bluetooth Headset');
When using ChimeSession, the device list is managed automatically. Use session.deviceList, session.selectedAudioDevice, and session.updateCurrentDevice(label).
Meeting Quality & Analytics
Periodic metrics
Emitted every second with a map of metric name → numeric value.
AmazonChime.instance.onMeetingMetricsReceived.listen((Map<String, Object?> metrics) {
final loss = metrics['audioPacketsReceivedFractionLoss'];
final jitter = metrics['audioDecodeMs'];
});
SDK lifecycle events
Emitted for key meeting lifecycle moments (join, leave, reconnect, etc.).
AmazonChime.instance.onMeetingEventReceived.listen((MeetingEvent event) {
print('${event.name}: ${event.attributes}');
});
MeetingEvent fields:
| Field | Type | Description |
|---|---|---|
name |
String |
Event name (e.g. "meetingStartSucceeded") |
attributes |
Map<String, Object?> |
Event-specific attribute map |
Volume & signal strength
// Batch updates emitted periodically for all attendees
AmazonChime.instance.onAttendeesVolumeChanged.listen((List<AttendeeVolume> updates) {
for (final u in updates) {
print('${u.attendeeId}: volume=${u.volume}');
}
});
AmazonChime.instance.onAttendeesSignalChanged.listen((List<AttendeeSignal> updates) {
for (final u in updates) {
print('${u.attendeeId}: signal=${u.signalStrength}');
}
});
Connection quality
AmazonChime.instance.onConnectionQualityChanged.listen((bool isPoor) {
if (isPoor) showBanner('Poor connection');
else hideBanner();
});
Error Handling
All SDK methods throw typed exceptions on failure.
| Exception | When thrown |
|---|---|
ChimePermissionException |
Audio or video permissions denied |
ChimeMeetingException |
Join or stop meeting failed |
ChimeDeviceException |
Mute, video, audio device, or screen share failed |
ChimeStateException |
SDK reached an invalid state |
All extend ChimeException, which exposes message and an optional code.
try {
await AmazonChime.instance.joinMeeting(joinInfo);
} on ChimeMeetingException catch (e) {
print('Join failed: ${e.message} (${e.code})');
} on ChimePermissionException catch (e) {
print('Permission denied: ${e.message}');
}
Troubleshooting
Joined the meeting but no audio
- Android: confirm
RECORD_AUDIOis in your app'sAndroidManifest.xmland was granted at runtime. Permissions denied silently fail — callhasAudioPermissions()to check. - iOS: confirm
NSMicrophoneUsageDescriptionis inInfo.plist. Without it, iOS rejects the permission prompt and the audio session never opens.
Audio cuts out when the app goes to background (iOS)
Add the background audio mode to ios/Runner/Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
Remote video tiles are black or never appear
The SDK does not auto-subscribe to remote video. Listen to onRemoteVideoSourcesAvailable and call updateVideoSourceSubscriptions(toAdd: sources). See Remote video subscriptions.
iOS screen share picker doesn't appear when calling startScreenShare()
The plugin is looking for an extension bundle ID it can't find. Verify the resolution order:
- Did you pass
JoinInfo.screenShareExtensionId? - Is
ChimeScreenShareExtensionset in your main app'sInfo.plist? - If relying on the fallback, is your extension target's bundle ID exactly
<mainAppBundleId>.ScreenShareExt?
iOS screen share starts then immediately stops
App Group ID mismatch. The same group ID must be enabled on both the main app target and the extension target, and appGroupId on JoinInfo must match it exactly. Check Signing & Capabilities → App Groups on both targets.
Bluetooth headset isn't in the audio device list (Android 12+)
The plugin declares BLUETOOTH_CONNECT but it's a runtime permission. Request it in your app before joining; without it, the Android system returns no Bluetooth devices.
MissingPluginException after adding the plugin
Run flutter clean && flutter pub get, then rebuild. Plugin registration is a build-time step.
Models Reference
JoinInfo
Meeting and attendee credentials returned by your server. See Meeting Credentials.
Attendee
| Member | Type | Description |
|---|---|---|
attendeeId |
String |
Chime attendee ID |
externalUserId |
String |
Your app's user identifier (format: "appId#username") |
muteStatus |
bool |
Whether the attendee is currently muted |
isVideoOn |
bool |
Whether the attendee has video enabled |
videoTileInfo |
VideoTileInfo? |
Current video tile, or null if video is off |
formattedExternalId |
String (getter) |
The username portion of externalUserId — splits on # and returns the second part, or the full value if the convention isn't followed |
isContent |
bool (getter) |
Whether this attendee represents a screen share source |
Attendee.isContentId(id) |
bool (static) |
Whether a raw attendee ID string represents a content share |
VideoTileInfo
| Field | Type | Description |
|---|---|---|
tileId |
int |
Native tile ID — pass to VideoTile(tileId:) |
attendeeId |
String |
Owner of this tile |
isLocalTile |
bool |
Whether this is the local attendee's tile |
isContentShare |
bool |
Whether this tile is a screen share |
videoStreamContentWidth |
int |
Stream pixel width |
videoStreamContentHeight |
int |
Stream pixel height |
RemoteVideoSource
| Field | Type | Description |
|---|---|---|
attendeeId |
String |
Attendee whose video you want to subscribe to |
DataMessage
See Data Messages.
AttendeeVolume
| Field | Type | Description |
|---|---|---|
attendeeId |
String |
Chime attendee ID |
externalUserId |
String |
Your app's user identifier |
volume |
int |
Volume level (0–100) |
AttendeeSignal
| Field | Type | Description |
|---|---|---|
attendeeId |
String |
Chime attendee ID |
externalUserId |
String |
Your app's user identifier |
signalStrength |
int |
Signal strength (0–2) |
MeetingEvent
See Meeting Quality & Analytics.
Streams Reference
All streams on AmazonChime.instance are broadcast streams — multiple listeners are supported and each subscription must be cancelled when no longer needed.
| Stream | Type | Description |
|---|---|---|
onAttendeeJoined |
Stream<Attendee> |
A new attendee joined |
onAttendeeLeft |
Stream<Attendee> |
An attendee left gracefully |
onAttendeeDropped |
Stream<Attendee> |
An attendee was dropped (network) |
onAttendeeMuted |
Stream<Attendee> |
An attendee muted themselves |
onAttendeeUnmuted |
Stream<Attendee> |
An attendee unmuted themselves |
onVideoTileAdded |
Stream<VideoTileInfo> |
A video tile became available |
onVideoTileRemoved |
Stream<VideoTileInfo> |
A video tile was removed |
onVideoTilePaused |
Stream<VideoTileInfo> |
A video tile was paused |
onVideoTileResumed |
Stream<VideoTileInfo> |
A paused tile resumed |
onActiveSpeakersChanged |
Stream<List<String>> |
Active speaker IDs changed |
onAttendeesVolumeChanged |
Stream<List<AttendeeVolume>> |
Batch volume updates |
onAttendeesSignalChanged |
Stream<List<AttendeeSignal>> |
Batch signal strength updates |
onRemoteVideoSourcesAvailable |
Stream<List<RemoteVideoSource>> |
Remote video sources became available |
onRemoteVideoSourcesUnavailable |
Stream<List<RemoteVideoSource>> |
Remote video sources went away |
onAudioSessionStarted |
Stream<void> |
Audio session started (or reconnected successfully) |
onAudioSessionStopped |
Stream<void> |
Audio session ended |
onAudioSessionDropped |
Stream<void> |
Audio session was dropped |
onAudioSessionStartConnecting |
Stream<bool> |
Connecting (or reconnecting if true) |
onAudioSessionCancelledReconnect |
Stream<void> |
Reconnect attempt was cancelled |
onConnectionQualityChanged |
Stream<bool> |
true = poor, false = recovered |
onContentShareStateChanged |
Stream<int> |
0 = started, 1 = stopped |
onDataMessageReceived |
Stream<DataMessage> |
Received a real-time data message |
onMeetingMetricsReceived |
Stream<Map<String, Object?>> |
Periodic quality metrics |
onMeetingEventReceived |
Stream<MeetingEvent> |
SDK lifecycle analytics event |
Libraries
- amazon_chime
- chime_session
- flutter_amazon_chime
- Flutter plugin for the Amazon Chime SDK.
- logger
- method_channel_coordinator
- models/attendee
- models/attendee_signal
- models/attendee_volume
- models/data_message
- models/exceptions
- models/join_info
- models/meeting_event
- models/models
- models/remote_video_source
- models/video_tile_info
- styles/style
- views/amazon_chime_view
- views/grid/participant_grid
- views/indicator/mic_status_indicator
- views/meeting_controls/camera_toggle
- views/meeting_controls/meeting_controls
- views/meeting_controls/mic_toggle
- views/tile/participant_tile
- views/tile/video_tile