Video calling for Flutter on Azure Communication Services — with native Android screen sharing and a self-contained web bridge.
Microsoft provides no official Flutter SDK for ACS calling. This plugin fills that gap by bridging the native Android Calling SDK and the ACS Web JS SDK behind a single Dart API — built for, and extracted from, a production telehealth application where remote optometrists join field clinics by video.
- ✅ Video calling using Azure Communication Services
- ✅ Screen sharing on Android (native MediaProjection implementation, Android 14/15 compliant)
- ✅ Adaptive video quality based on network conditions
- ✅ Media controls (camera, microphone, speaker, camera switch)
- ✅ Incoming-call popup with ringtone and call history
- ✅ Room-based calling
- ✅ Real-time video streaming
- ✅ Web support via InAppWebView (self-contained vendored ACS Web SDK)
- ✅ Android (API 23+)
- ✅ Web
- ⏳ iOS — not yet implemented (roadmap)
git clone https://github.com/guss1215/flutter_acs_calling.git
cd flutter_acs_calling
bash tool/vendor_acs_sdk.sh # bundles the ACS Web SDK — needed for BOTH Android and Web
cd example
flutter runThe example boots to a setup screen asking for a backend URL — the plugin talks to a small signaling backend you host (contract below).
This package is distributed from GitHub:
dependencies:
flutter_acs_calling:
git:
url: https://github.com/guss1215/flutter_acs_calling.git
ref: v0.1.1Why not on pub.dev (yet)? The web platform relies on a bundled build of Microsoft's ACS Calling Web SDK, whose license does not allow redistributing the bundle. Consumers generate it locally with
tool/vendor_acs_sdk.sh— a step the read-only pub cache cannot run. Publishing is on the roadmap once the bundle is supplied by the host app instead.
Add the following permissions to your android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION"/>The bundled ACS Android SDK declares its own android:label, so your
<application> element must override it or the manifest merge fails:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
...
tools:replace="android:label">Make sure your minSdkVersion is at least 23:
android {
defaultConfig {
minSdk = 23
}
}Load the ACS bridge script in your app's web/index.html. It is served from
the plugin's bundled assets, so there is nothing to copy by hand:
<script src="assets/packages/flutter_acs_calling/assets/calling/acs_bridge.js" defer></script>The bridge loads a vendored bundle of the ACS Calling Web SDK
(assets/calling/acs_calling_sdk.js). That bundle is not committed —
Microsoft distributes the SDK under its own license terms — so after cloning
this repository, generate it locally (requires Node ≥ 18):
bash tool/vendor_acs_sdk.shCI runs the same script before building the example for web.
The integration contract has five steps (the example app shows all of them working together):
import 'package:flutter_acs_calling/flutter_acs_calling.dart';
// 1. Configure the backend URL before using any API (in main(), or once the
// user has picked an environment). On web, mirror it to the JS bridge.
ApiService.setApiUrl('https://your-acs-backend.example.com');
setApiUrl('https://your-acs-backend.example.com'); // no-op outside web
// 2. If your backend endpoints require auth, sync the host app's JWT after
// login:
await ApiService.saveAuthTokenToLocalStorage(token: jwt);
// 3. Mount the persistent video overlay ONCE, after an Overlay exists
// (e.g. addPostFrameCallback in your shell screen), and dispose it on
// logout:
VideoCallOverlayManager.instance.ensureMounted(context);
// 4. Stack IncomingCallOverlay over any screen that can receive calls —
// it polls for calls and shows the popup with ringtone:
Stack(children: [myScreen, const IncomingCallOverlay()])
// 5. Start an outgoing call: signal the backend, get an ACS token, join.
final call = await AcsApi.initiateCall(targetUserId: id, callerName: myName);
final tokenRes = await AcsApi.getToken();
await PersistentWebViewState().joinCall(
token: tokenRes.token,
roomId: call.roomId,
displayName: myName,
callId: call.callId,
userId: tokenRes.userId,
);This plugin requires a backend service that provides:
POST /api/token- Get ACS token for userPOST /api/token/refresh- Refresh ACS tokenPOST /api/rooms- Create a new roomPOST /api/rooms/{roomId}/participants- Add participant to roomDELETE /api/rooms/{roomId}/participants- Remove participant from roomGET /api/rooms/{roomId}- Get room informationGET /api/calls/users- List callable users and their availabilityPOST /api/calls/initiate- Create a call (room + notify target user)GET /api/calls/incoming- Poll incoming calls for the current userGET /api/calls/{callId}- Get call detailsPOST /api/calls/{callId}/accept/.../reject/.../end- Call lifecyclePOST /api/calls/reset-my-presence- Clear an orphaned "busy" state
Every endpoint except token minting is trivial CRUD; tokens come from the
Azure.Communication.Identity SDK (a few lines in any backend language).
A minimal .NET sample backend is on the roadmap.
A few deliberate choices worth knowing before adopting (or interviewing me about):
- One JS bridge for both platforms. The ACS Web Calling SDK runs inside a WebView on Android and an iframe-less bridge on web, so there is a single ACS integration to maintain. The native Android layer exists only where the WebView cannot go: MediaProjection screen capture (custom GL pipeline, foreground service, Android 14/15 compliance).
- Vendored, pinned Web SDK.
tool/vendor_acs_sdk.shpins the SDK version and bundles it as a self-contained IIFE — the app keeps working on filtered/offline-ish networks and insidefile://WebViews, and Microsoft's license is respected by not committing the bundle. - Polling for incoming calls (3s), not push. It keeps the backend contract dependency-free (no SignalR/FCM infrastructure required to try the plugin). The production path is push signaling — see the roadmap.
- Mid-call token refresh is scheduled from the JWT
expclaim; long calls used to die silently at ~1 hour without it.
Known limits: no iOS yet, polling latency for incoming calls, and the backend is bring-your-own until the sample ships.
Owns the single persistent video overlay (the call UI survives navigation).
VideoCallOverlayManager.instance.ensureMounted(context); // once, in the shell
VideoCallOverlayManager.instance.dispose(); // on logout
// Optional: brand the in-call header (defaults to 'Video call')
VideoCallOverlayManager.instance.ensureMounted(context, headerTitle: 'TeleHealth');Widget that polls for incoming calls and shows the popup (ringtone, accept/decline). Stack it over screens for roles that receive calls.
Typed client for the backend endpoints listed above (getToken,
initiateCall, checkIncomingCalls, acceptCall, rejectCall, endCall,
createRoom, addSelfToRoom, ...).
Drives the actual call: joinCall(...) requests camera/microphone
permissions, shows the overlay and joins the ACS room.
Device-local call log (ChangeNotifier): entries newest-first and a
missedToday counter for badges.
See the example directory for a complete sample application.
To run the example:
bash tool/vendor_acs_sdk.sh # once, from the repo root
cd example
flutter run- iOS support — native ACS iOS SDK binding, or extending the WebView bridge
- Push-based incoming-call signaling (SignalR / FCM) to replace polling
- Minimal .NET sample backend (token + rooms + call signaling) so the example runs end-to-end in minutes
- pub.dev publication once the ACS Web SDK bundle is host-app-supplied
- CallKit / ConnectionService integration for OS-level call UX
flutter_inappwebview: ^6.1.5- Web view for web platformpermission_handler: ^12.0.1- Permission handlinghttp: ^1.5.0- HTTP requestsshared_preferences: ^2.5.3- Local storage- Azure Communication Services SDK for Android
MIT — see LICENSE.
Contributions are welcome! Please feel free to submit a Pull Request.
Built by Gustavo Guillen — Flutter & .NET developer focused on offline-first health tech and real-time video (Azure Communication Services). This plugin was extracted from a production telehealth platform I work on.
Available for freelance work (Flutter · .NET · Azure). GitHub · LinkedIn · guss1215@gmail.com
For issues, feature requests, or questions, please file an issue on this repository.