icy-salesclerk-19784
12/23/2025, 10:50 AMtall-airline-16756
12/23/2025, 10:50 AMtall-airline-16756
12/23/2025, 10:50 AM<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
1. Declare a foreground service inside the <application> tag:
<service
android:name=".AudioService"
android:foregroundServicetype="mediaPlayback"
android:exported="false">
<intent-filter>
<action android:name="android.media.becoming_noisy"/>
</intent-filter>
</service>
(If you use an existing plugin like flutter_background_service, configure it for media playback instead of writing a custom service.)
Native service behavior
• The service should start in the background when the room is connected and the app enters background.
• The service must post an ongoing Notification (media style preferred) so Android keeps the process alive and grants audio focus.
• Keep LiveKit/ WebRTC audio resources active in the service; do not release or mute tracks when the app is paused.
Flutter lifecycle integration
• Use WidgetsBindingObserver to detect backgrounding and call a MethodChannel to start the native foreground service:
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
startForegroundService(); // call native via MethodChannel
} else if (state == AppLifecycleState.resumed) {
stopForegroundService(); // optional
}
}
WebRTC / audio focus notes
• LiveKit itself relies on the platform WebRTC audio path. Android will mute/background audio unless the app holds a foreground service and audio focus.
• Ensure your native code requests audio focus for playback and does not release it when the app is backgrounded.
Device-specific caveats
• Some OEMs (Xiaomi, Huawei, Samsung variants) aggressively kill background apps — users may need to exempt your app from battery optimizations.
• Behavior can vary by Android version; test on target versions.
If you want, I can:
• Provide a sample Kotlin implementation for the foreground service and notification.
• Show a complete Flutter -> Android MethodChannel example to start/stop the service.
Which of those would you like me to send next?
Sources: Flutter quickstart | LiveKit docs | Camera & microphone | LiveKit docs | client-sdk-android | livekit/livekit_composite | DeepWiki | Android quickstart | LiveKit docs | Android quickstart (Jetpack Compose) | LiveKit docsicy-salesclerk-19784
12/23/2025, 11:12 AMtall-airline-16756
12/23/2025, 11:13 AMpackage com.yourcompany.yourapp
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
class AudioForegroundService : Service() {
companion object {
const val CHANNEL_ID = "livekit_audio_channel"
const val NOTIF_ID = 1001
const val ACTION_START = "com.yourcompany.yourapp.action.START"
const val ACTION_STOP = "com.yourcompany.yourapp.action.STOP"
}
private lateinit var audioManager: AudioManager
private var focusRequest: AudioFocusRequest? = null
override fun onCreate() {
super.onCreate()
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
createNotificationChannel()
requestAudioFocus()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START -> startForeground(NOTIF_ID, buildNotification())
ACTION_STOP -> {
stopForeground(true)
stopSelf()
}
else -> {
// default: ensure foreground if service started
startForeground(NOTIF_ID, buildNotification())
}
}
// Return START_STICKY so the system attempts to recreate the service if killed.
return START_STICKY
}
override fun onDestroy() {
abandonAudioFocus()
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? = null
private fun buildNotification(): Notification {
val intent = packageManager.getLaunchIntentForPackage(packageName)
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
else PendingIntent.FLAG_UPDATE_CURRENT
)
val stopIntent = Intent(this, AudioForegroundService::class.java).apply {
action = ACTION_STOP
}
val stopPending = PendingIntent.getService(
this,
1,
stopIntent,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
else PendingIntent.FLAG_UPDATE_CURRENT
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Live audio playing")
.setContentText("Audio will continue while the app is in background")
.setSmallIcon(android.R.drawable.ic_media_play)
.setContentIntent(pendingIntent)
.addAction(android.R.drawable.ic_media_pause, "Stop", stopPending)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = "LiveKit Audio"
val importance = NotificationManager.IMPORTANCE_LOW
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
description = "Keep LiveKit audio playback running"
}
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.createNotificationChannel(channel)
}
}
private fun requestAudioFocus() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val attr = AudioAttributes.Builder()
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_SPEECH)
.build()
focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(attr)
.setOnAudioFocusChangeListener { /* optionally handle focus changes */ }
.build()
audioManager.requestAudioFocus(focusRequest!!)
} else {
@Suppress("DEPRECATION")
audioManager.requestAudioFocus(
{ /*listener*/ },
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
)
}
}
private fun abandonAudioFocus() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
focusRequest?.let { audioManager.abandonAudioFocusRequest(it) }
} else {
@Suppress("DEPRECATION")
audioManager.abandonAudioFocus(null)
}
} catch (e: Exception) { /* ignore */ }
}
}
AndroidManifest additions
<!-- permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- inside <application> -->
<service
android:name=".AudioForegroundService"
android:foregroundServicetype="mediaPlayback"
android:exported="false">
<intent-filter>
<action android:name="android.media.becoming_noisy"/>
</intent-filter>
</service>
Flutter -> Native (start/stop example)
• Use a MethodChannel from Dart to send an intent action to start the service:
// in MainActivity.kt
import android.content.Intent
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: io.flutter.embedding.engine.FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
val channel = io.flutter.plugin.common.MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "livekit_audio_service")
channel.setMethodCallHandler { call, result ->
when (call.method) {
"startService" -> {
val intent = Intent(this, AudioForegroundService::class.java).apply { action = AudioForegroundService.ACTION_START }
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
result.success(null)
}
"stopService" -> {
val intent = Intent(this, AudioForegroundService::class.java).apply { action = AudioForegroundService.ACTION_STOP }
startService(intent)
result.success(null)
}
else -> result.notImplemented()
}
}
}
}
Then call from Dart:
static const _channel = MethodChannel('livekit_audio_service');
await _channel.invokeMethod('startService'); // when app goes background
await _channel.invokeMethod('stopService'); // optional when back foreground or leaving room
Implementation notes and next steps
• Ensure the LiveKit connection (room) and audio tracks remain active while the service is running. If you create/hold the LiveKit client only in Flutter UI code, consider moving the audio-handling logic or WebRTC audio output to the native side or ensure Flutter keeps the audio object alive while backgrounded.
• Test on devices and Android versions you target; some OEMs require exempting the app from battery optimizations.
• If you'd like, I can now provide the corresponding Flutter MethodChannel integration (detailed Dart code and where to call it) or a sample that keeps the LiveKit client alive across background/foreground transitions. Which would you prefer next? Sources: Screen sharing | LiveKit docs | client-sdk-android | livekit/livekit_composite | DeepWikiicy-salesclerk-19784
12/23/2025, 11:15 AMtall-airline-16756
12/23/2025, 11:15 AMstartService() and stopService() to call the native Kotlin service.
import 'dart:async';
import 'package:flutter/services.dart';
class AudioService {
static const MethodChannel _channel = MethodChannel('livekit_audio_service');
static Future<void> startService() async {
try {
await _channel.invokeMethod('startService');
} on PlatformException catch (e) {
// handle error
rethrow;
}
}
static Future<void> stopService() async {
try {
await _channel.invokeMethod('stopService');
} on PlatformException catch (e) {
// handle error
rethrow;
}
}
}
• Call these when lifecycle changes (see next section).
2) Monitor app lifecycle and start/stop the service
• Use WidgetsBindingObserver in a top-level widget to detect transitions. Start the foreground service when app is backgrounded, stop when resumed.
import 'package:flutter/widgets.dart';
import 'audio_service.dart'; // the class above
class LifecycleServiceBinder extends StatefulWidget {
final Widget child;
final bool keepServiceWhenBackgrounded;
const LifecycleServiceBinder({
Key? key,
required this.child,
this.keepServiceWhenBackgrounded = true,
}) : super(key: key);
@override
_LifecycleServiceBinderState createState() => _LifecycleServiceBinderState();
}
class _LifecycleServiceBinderState extends State<LifecycleServiceBinder> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (!widget.keepServiceWhenBackgrounded) return;
if (state == AppLifecycleState.paused) {
AudioService.startService();
} else if (state == AppLifecycleState.resumed) {
AudioService.stopService();
}
}
@override
Widget build(BuildContext context) => widget.child;
}
Wrap your app with this in `main.dart`:
void main() {
runApp(LifecycleServiceBinder(child: MyApp()));
}
3) Keep the LiveKit client alive across background/foreground Options (pick one based on complexity you want):
• Option A — Keep LiveKit client in Flutter memory (simpler)
◦ Ensure the LiveKit `Room`/client object is held by a long-lived singleton or top-level provider (e.g., Riverpod/Provider/Bloc) that is not disposed when route/UI widgets unmount.
◦ When app goes background, do NOT call room.disconnect() or release audio tracks. The foreground service will keep audio focus; the Flutter-held client continues to receive audio.
◦ Example pattern: create a singleton LiveKitManager that holds the Room and exposes connect/disconnect methods.
• Option B — Move audio handling to native side (more robust)
◦ Maintain the WebRTC/LiveKit connection inside the Android module (Kotlin). Start/stop that native-managed client from Flutter via MethodChannel.
◦ Native approach ensures audio continues even if Flutter engine is paused or killed by system, but requires more integration (binding LiveKit Android SDK or WebRTC there).
Sample singleton pattern (Option A) — Dart
import 'package:livekit_client/livekit_client.dart';
class LiveKitManager {
LiveKitManager._private();
static final LiveKitManager instance = LiveKitManager._private();
Room? _room;
Future<Room> connect(String url, String token) async {
if (_room != null && _room!.connected) return _room!;
_room = await LiveKitClient.connect(url, token); // pseudocode; use your LiveKit client init
// subscribe to events, keep audio tracks active
return _room!;
}
Room? get room => _room;
Future<void> disconnect() async {
try {
await _room?.disconnect();
} catch (_) {}
_room = null;
}
}
How to combine with lifecycle binder
• On background (in didChangeAppLifecycleState paused): call AudioService.startService() to start the native foreground service.
• Do not call `LiveKitManager.instance.disconnect()`; keep the Room alive.
• On resume: call AudioService.stopService() if you want to remove the ongoing notification.
Important notes
• Some devices kill background apps aggressively; the native service + ongoing notification reduces that risk but you may still need to instruct users to disable battery optimizations.
• If you find audio stops because Flutter engine is paused, consider Option B (native-managed LiveKit) for critical reliability.
If you want, I can now:
• Provide the full MainActivity.kt MethodChannel snippet integrated into a new FlutterActivity (if you haven’t already).
• Or provide a full working sample that wires LiveKitManager connect flow with lifecycle events and the MethodChannel calls. Which would you like next?icy-salesclerk-19784
12/23/2025, 11:17 AMtall-airline-16756
12/23/2025, 11:17 AMstartService / stopService calls by starting/stopping the Kotlin AudioForegroundService shown earlier.
MainActivity.kt
package com.yourcompany.yourapp
import android.content.Intent
import android.os.Build
import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
private val CHANNEL = "livekit_audio_service"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"startService" -> {
val intent = Intent(this, AudioForegroundService::class.java).apply {
action = AudioForegroundService.ACTION_START
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
result.success(null)
}
"stopService" -> {
val intent = Intent(this, AudioForegroundService::class.java).apply {
action = AudioForegroundService.ACTION_STOP
}
// Use startService to deliver the stop action to the Service; it will stopSelf()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
result.success(null)
}
else -> result.notImplemented()
}
}
}
// Optional: handle intents if you want taps on notification to route back into the app via Flutter
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
}
}
Notes
• Update the package line to match your app's package.
• Ensure AudioForegroundService class and the service declaration are present in AndroidManifest.xml as shown earlier.
• If you already use a custom FlutterEngine setup, register the MethodChannel on that engine instead.
• This MainActivity implementation assumes the Flutter app calls the MethodChannel methods (startService / stopService) when lifecycle changes occur.icy-salesclerk-19784
12/23/2025, 11:18 AMtall-airline-16756
12/23/2025, 11:18 AMimport 'package:livekit_client/livekit_client.dart';
import 'package:flutter/foundation.dart';
class LiveKitManager {
LiveKitManager._private();
static final LiveKitManager instance = LiveKitManager._private();
Room? _room;
Room? get room => _room;
Future<Room> connect(String url, String token) async {
if (_room != null && _room!.connected) return _room!;
// Replace with your LiveKit client connection code
final room = await LiveKitClient.connect(url, token); // pseudocode; adapt to real API
_room = room;
// Ensure subscriptions to track events to keep audio active
_room!.on(RoomEvent.trackSubscribed, (evt) {
// handle subscribed tracks
});
return _room!;
}
Future<void> disconnect() async {
try {
await _room?.disconnect();
} catch (e) {}
_room = null;
}
}
C. lib/audio_service.dart (MethodChannel wrapper)
import 'package:flutter/services.dart';
class AudioService {
static const MethodChannel _channel = MethodChannel('livekit_audio_service');
static Future<void> startService() async {
try {
await _channel.invokeMethod('startService');
} on PlatformException {
rethrow;
}
}
static Future<void> stopService() async {
try {
await _channel.invokeMethod('stopService');
} on PlatformException {
rethrow;
}
}
}
D. lib/lifecycle_binder.dart (wrap your app to manage lifecycle)
import 'package:flutter/widgets.dart';
import 'audio_service.dart';
import 'livekit_manager.dart';
class LifecycleServiceBinder extends StatefulWidget {
final Widget child;
final bool keepServiceWhenBackgrounded;
const LifecycleServiceBinder({Key? key, required this.child, this.keepServiceWhenBackgrounded = true}) : super(key: key);
@override
_LifecycleServiceBinderState createState() => _LifecycleServiceBinderState();
}
class _LifecycleServiceBinderState extends State<LifecycleServiceBinder> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (!widget.keepServiceWhenBackgrounded) return;
if (state == AppLifecycleState.paused) {
// App backgrounded: start foreground service to keep audio playing.
AudioService.startService();
// Do NOT disconnect LiveKitManager; keep Room alive.
} else if (state == AppLifecycleState.resumed) {
// App foregrounded: stop service (optional)
AudioService.stopService();
// UI can continue using LiveKitManager.instance.room
}
}
@override
Widget build(BuildContext context) => widget.child;
}
E. lib/main.dart (example wiring)
import 'package:flutter/material.dart';
import 'lifecycle_binder.dart';
import 'livekit_manager.dart';
void main() {
runApp(LifecycleServiceBinder(child: MyApp()));
}
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _url = '<https://your.livekit.server>';
final _token = 'YOUR_TOKEN';
@override
void initState() {
super.initState();
_connect();
}
Future<void> _connect() async {
try {
await LiveKitManager.instance.connect(_url, _token);
// connected; UI can subscribe/render participants
} catch (e) {
// handle error
}
}
@override
void dispose() {
// Optionally disconnect when app fully exits.
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('LiveKit background audio sample')),
body: Center(child: Text('Connected: ${LiveKitManager.instance.room != null}')),
),
);
}
}
3) Key integration notes
• Do NOT call LiveKitManager.instance.disconnect() when app goes to background. Keep the Room and audio tracks alive.
• Start the native foreground service when backgrounded via AudioService.startService() so Android grants audio focus and prevents suspending audio.
• Stop the service on resume if you prefer to remove the notification.
• If your LiveKit client or Flutter engine is paused/killed on backgrounding on some OEM devices, consider migrating audio handling to native (Option B) for stronger reliability.
4) Testing checklist
• Confirm AndroidManifest includes:
◦ FOREGROUND_SERVICE, FOREGROUND_SERVICE_MEDIA_PLAYBACK, WAKE_LOCK permissions.
◦ service declaration for AudioForegroundService with android:foregroundServicetype="mediaPlayback".
• Test on devices and Android versions you target.
• If audio still mutes on background, verify LiveKit/ flutter_webrtc objects remain instantiated and not garbage-collected; add logging in Room event handlers.
If you want, I can now:
• Provide the exact LiveKit client initialization code tailored to the specific LiveKit Flutter package/version you use.
• Or convert this sample to Option B (native-managed LiveKit) for maximum background reliability.
Which would you like next?