Background Handling - Flutter
Due to platform-specific restrictions on iOS and Android, essential functionalities like microphone and camera access are limited when the app moves to Background Mode.
-
iOS has strict resource management policies, preventing access to the camera in the background. However, it allows microphone access, which is crucial for maintaining media transmission.
-
Android 14 and above restrict background microphone and camera access and enforce strict battery optimization policies. These policies can aggressively kill background processes.
To ensure seamless media transmission in VideoSDK when the app is in background, you must address these platform-specific limitations by:
- Enable the required background modes on iOS.
- Configure permissions and use foreground services on Android.
Alternatively, if you prefer not to implement a foreground service, you can use Picture-in-Picture (PiP) mode on both iOS and Android. This allows the app to continue media playback in a small floating window, bypassing some of the background restrictions.
If these steps aren’t correctly implemented, the app will lose the ability to send or receive media when backgrounded, disrupting core communication features.
The sections below explain how to implement background support effectively on both platforms with VideoSDK.
iOS Background Handling
For iOS devices, you need to enable the necessary capabilities in Xcode to continue publishing and playing media while the app is in the background:
- Open your project in Xcode.
- Navigate to the target settings.
- Select the "Signing & Capabilities" tab.
- Click the "+" button to add capabilities.
- Add Background Modes.
Under Background Modes, enable the following options:
- Audio, AirPlay, and Picture in Picture
This will allow your app to handle audio and video in the background, ensuring that essential media features like calls continue uninterrupted.

Android Background Handling
On Android, background media capture is more restrictive. Challenges include:
- Limited microphone/camera access when the app is backgrounded (usually ~60 seconds by default).
- OEM-specific optimizations that aggressively kill background processes.
To ensure reliable background capture and playback, use a foreground service with a persistent notification.
Update your AndroidManifest.xml
to declare permissions and the foreground service:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Foreground Service -->
<application>
<service android:name=".MyForegroundService"
android:foregroundServiceType="camera|microphone"
android:stopWithTask="true" />
</application>
</manifest>
Implement a foreground service that displays a persistent notification and keeps media capture active:
package com.example.example
import android.app.*
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
class ForegroundService : Service() {
companion object {
const val NOTIFICATION_ID = 1001
const val CHANNEL_ID = "AppChannelID"
private const val TAG = "ForegroundService"
// Actions
const val ACTION_START = "com.example.example.START_FOREGROUND_SERVICE"
const val ACTION_STOP = "com.example.example.STOP_FOREGROUND_SERVICE"
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Service onCreate")
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "Service onStartCommand")
when (intent?.action) {
ACTION_START -> {
startForegroundService()
}
ACTION_STOP -> {
stopForegroundService()
}
else -> {
// Default action - start the service
startForegroundService()
}
}
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Service onDestroy")
}
private fun startForegroundService() {
Log.d(TAG, "Starting foreground service")
try {
val notification = createNotification()
startForeground(NOTIFICATION_ID, notification)
Log.d(TAG, "Foreground service started successfully")
} catch (e: SecurityException) {
Log.e(TAG, "Security exception while starting foreground service", e)
// This usually means missing permissions
throw e
} catch (e: Exception) {
Log.e(TAG, "Error starting foreground service", e)
throw e
}
}
private fun stopForegroundService() {
Log.d(TAG, "Stopping foreground service")
stopForeground(true)
stopSelf()
}
private fun createNotification(): Notification {
val notificationIntent = Intent(this, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0,
notificationIntent,
PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Foreground Service")
.setContentText("Camera and microphone are active")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.build()
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Foreground Service",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Channel for foreground service notifications"
setShowBadge(false)
enableLights(false)
enableVibration(false)
}
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager?.createNotificationChannel(channel)
Log.d(TAG, "Notification channel created")
}
}
}
Use a MethodChannel in MainActivity to start/stop the service from Dart:
package com.example.example
import com.example.example.ForegroundService
import io.flutter.plugin.common.MethodChannel
import android.content.Intent
import io.flutter.embedding.android.FlutterActivity
import android.os.Bundle
import io.flutter.embedding.engine.FlutterEngine
import android.net.Uri
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.example.example/channel"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"startMicrophoneService" -> {
startService(Intent(this, ForegroundService::class.java).apply {
action = ForegroundService.ACTION_START
})
result.success(null)
}
"stopMicrophoneService" -> {
startService(Intent(this, ForegroundService::class.java).apply {
action = ForegroundService.ACTION_STOP
})
result.success(null)
}
else -> result.notImplemented()
}
}
}
}
Finally, request permissions and start the service from Flutter:
class MeetingScreen extends StatefulWidget {
@override State<MeetingScreen> createState() => _MeetingScreenState();
}
class _MeetingScreenState extends State<MeetingScreen> {
static const platform = MethodChannel('com.example.example/channel');
@override
void initState() {
Room room = VideoSDK.createRoom(
roomId: widget.meetingId,
token: widget.token,
customCameraVideoTrack: widget.cameraTrack,
customMicrophoneAudioTrack: widget.micTrack,
displayName: widget.displayName,
micEnabled: widget.micEnabled,
camEnabled: widget.camEnabled,
maxResolution: 'hd',
);
// Register meeting events
registerMeetingEvents(room);
// Join meeting
room.join();
}
Future<void> _startMicrophoneService() async {
try {
await platform.invokeMethod('startMicrophoneService');
} on PlatformException catch (e) {
print("Failed to start service: '${e.message}'.");
}
}
Future<void> _stopMicrophoneService() async {
try {
await platform.invokeMethod('stopMicrophoneService');
} on PlatformException catch (e) {
print("Failed to stop service: '${e.message}'.");
}
}
void registerMeetingEvents(Room _meeting) {
// Called when joined in meeting
_meeting.on(Events.roomJoined, () {
setState(() {
meeting = _meeting;
_joined = true;
});
if (!kIsWeb && Platform.isAndroid) {
_startMicrophoneService();
}
});
// Called when meeting is ended
_meeting.on(Events.roomLeft, (String? errorMsg) {
if(!kIsWeb && Platform.isAndroid){
_stopMicrophoneService();
}
});
}
}
In above code com.example.example
is our package name kindly replace with yours.
Got a Question? Ask us on discord