Building video calls in Flutter becomes more reliable when the app can understand what happens during a live session. In-call statistics reveal how well the connection performs, how stable the stream is, and where issues may appear before users even notice them.
Adding these metrics creates a clearer picture of the call"s real-time behavior, making it easier to debug and optimize quality. This foundation helps developers create video experiences that feel uninterrupted, respond faster, and adapt to changing network conditions.
Prerequisites
Teams set up Flutter projects with proper permissions to access cameras and microphones without crashes. Video SDKs and app credentials prepare the foundation so calls connect instantly when users tap join.
- Flutter project created with camera and microphone permissions added to Android and iOS manifests.
- Video SDK package like Agora RTC Engine or VideoSDK included in pubspec.yaml and installed.
- UI widgets are prepared, such as Text displays and overlays for showing live stats data.
- App ID or token from the video SDK provider is ready for authentication.
Setting Up the Call Engine
The call engine starts with SDK initialization to handle video and audio streams reliably. Event listeners connect early to capture network stats from the first frame, showing quality issues before they affect viewers.
Step 1: Add the video SDK dependency to pubspec.yaml and run flutter pub get.
dependencies:
agora_rtc_engine: ^6.3.2Step 2: Import the SDK package in the main Dart file.
import 'package:agora_rtc_engine/agora_rtc_engine.dart';Step 3: Initialize the RtcEngine or call the engine instance with your App ID.
final engine = createAgoraRtcEngine();
await engine.initialize(const RtcEngineContext(
appId: 'your-app-id-here',
));Step 4: Enable the video module and set up local video view for camera preview.
await engine.enableVideo();
await engine.startPreview();Step 5: Attach event listeners for stats callbacks like onNetworkQuality or onRtcStats.
engine.registerEventHandler(RtcEngineEventHandler(
onNetworkQuality: (quality) => print('Local quality: $quality'),
));Joining the Video Call
Users enter call rooms by channel name to sync local and remote video feeds smoothly. This step establishes connections so everyone sees faces clearly without black screens or delays.
Step 1: Get the call ID or channel name from user input.
String channelName = 'meeting-room-123';Step 2: Request camera and microphone permissions if not granted.
await Permission.camera.request();
await Permission.microphone.request();Step 3: Join the channel using the call ID and generate or use a temporary token.
await engine.joinChannel(
token: 'temporary-token',
channelId: channelName,
uid: 0,
options: ChannelMediaOptions(),
);Step 4: Set up remote video view widgets for other participants.
RtcLocalView(surfaceView: true);
RtcRemoteView(SurfaceView(uid: remoteUid));Step 5: Start local video preview and enable remote video subscription.
await engine.setLocalRenderMode(VideoRenderModeHidden);Displaying Local Video Stats
Local stats overlay shows your own sent bitrate and packet loss right on the camera preview. Numbers help you spot when your WiFi drops frames, letting you fix issues before others complain.
Step 1: Create a stats overlay widget positioned over the local video view.
Positioned(
top: 20,
right: 20,
child: Container(
padding: EdgeInsets.all(8),
color: Colors.black54,
child: Column(children: [statsTextWidgets]),
),
)Step 2: Register a listener for local network quality events from the SDK.
engine.registerEventHandler(
onLocalVideoStats: (stats) {
setState(() {
localBitrate = stats.sendBitrate;
localPacketLoss = stats.txPacketLossRate;
});
},
);Step 3: Update text fields with sent bitrate, packet loss percentage, and CPU usage.
Text('Bitrate: ${localBitrate / 1000} kbps'),
Text('Loss: ${localPacketLoss}%'),Step 4: Format numbers to show changes like "Bitrate: 1.2 Mbps → ".
Text('Bitrate: ${formatBitrate(localBitrate)} ${trendIcon}'),Step 5: Refresh the display every second using a timer.
Timer.periodic(Duration(seconds: 1), (timer) => setState(() {}));Tracking Remote Connection Stats
Remote stats monitor each participant's incoming video quality with delay and jitter readings. Color warnings highlight struggling connections so the group knows who needs to switch networks.
Step 1: Listen for remote user events like userJoined or userNetworkQuality.
onUserJoined: (uid, elapsed) {
remoteUsers[uid] = RemoteUserStats();
},
onRemoteVideoStats: (stats) {
updateRemoteStats(stats.uid, stats);
},Step 2: Assign stats data to each remote video tile based on user ID.
Map<int, RemoteUserStats> remoteUsers = {};Step 3: Show incoming bitrate, delay, and jitter values per participant.
Text('Remote ${uid}: ${stats.rxBitrate/1000} kbps'),
Text('Delay: ${stats.e2eDelay}ms'),Step 4: Color-code stats panels red if packet loss exceeds 5%.
Color statsColor = stats.rxPacketLossRate > 5 ? Colors.red : Colors.green;Step 5: Update remote stats when new users join or leave the call.
onUserOffline: (uid, reason) => remoteUsers.remove(uid),Updating Stats in Real Time
Real-time updates push fresh network data to the screen every few seconds without freezing video. Smoothing prevents numbers from jumping wildly, keeping stats readable during fast network changes.
Step 1: Set up periodic callbacks from the SDK for network stats every 2 seconds.
await engine.enableNetworkQualityStats(true);Step 2: Parse incoming data for jitter, resolution, and frame rate changes.
void updateStats(RtcStats stats) {
jitter = stats.lastMileDelay;
fps = stats.videoSendFps;
}Step 3: Push updates to the StatefulWidget setState for immediate UI refresh.
setState(() {
currentStats = parsedStats;
});Step 4: Throttle updates to avoid overwhelming the render loop.
if (DateTime.now().difference(lastUpdate).inMilliseconds > 500) {
setState(() {});
}Step 5: Add smoothing to the stats display to prevent flickering numbers.
double smoothedBitrate = (previousBitrate * 0.7) + (newBitrate * 0.3);Ending Calls Cleanly
Clean shutdown releases camera access and clears memory to prevent battery drain or crashes. Final stats logs help teams review call performance after sessions end.
Step 1: Call the leaveChannel method on the engine instance.
await engine.leaveChannel();Step 2: Stop local video capture and disable all audio tracks.
await engine.stopPreview();
await engine.muteLocalAudioStream(true);Step 3: Dispose of stats listeners and timers to free resources.
_timer?.cancel();
engine.removeEventHandler(eventHandler);Step 4: Save final stats like total duration and average bitrate to logs.
print('Call ended. Avg bitrate: ${totalBitrate / duration} kbps');Step 5: Destroy the engine instance and navigate back to the home screen.
await engine.release();
Navigator.pop(context);
