With the android build of my flutter app, the audi...
# ask-ai
i
With the android build of my flutter app, the audio from livekit streams is muted when the user switches to another app. The audio is unmuted when switching back. How can we set it to keep playing the livekit audio even when the app is not in the foreground?
t
Thanks for reaching out, Tracy. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I understand you want LiveKit audio in your Flutter Android app to keep playing when the user switches to another app. Below are the concrete steps you can take to enable persistent background audio. What to do (high level) • Run your LiveKit connection under an Android foreground service so the system treats the app as an active media app. • Add the required foreground permissions to AndroidManifest.xml. • Start/stop the foreground service from Flutter when the app lifecycle changes (use a MethodChannel to call native code). • Show an ongoing notification for the service (required for foreground services). • Test on real devices and advise users to disable aggressive battery optimizers if needed. Required Android changes 1. Permissions — add to android/app/src/main/AndroidManifest.xml:
Copy code
<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:
Copy code
<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:
Copy code
@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 &amp; microphone | LiveKit docs | client-sdk-android | livekit/livekit_composite | DeepWiki | Android quickstart | LiveKit docs | Android quickstart (Jetpack Compose) | LiveKit docs
i
You can do both, but one at a time, starting with the first one you listed: the sample Kotlin implemenation.
t
Thanks — I’ll provide a concise Kotlin sample foreground service you can drop into your Flutter Android module. This service keeps audio playback active (media foreground service + ongoing notification) so LiveKit/WebRTC audio can continue when the app is backgrounded. Notes before using: • Add the permissions described previously to AndroidManifest.xml. • Declare the service in the <application> tag (example shown after the Kotlin code). • You’ll need a small MethodChannel in Flutter to start/stop the service; I can provide that next if you want. Kotlin: AudioForegroundService.kt
Copy code
package 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
Copy code
<!-- 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:
Copy code
// 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:
Copy code
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 | DeepWiki
i
Yes go ahead and provide the corresponding Flutter MethodChannel integration, and then also a sample that keeps the LiveKit client alive across background/foreground transitions.
t
Thanks — I’ll provide a concise, ready-to-drop-in Flutter MethodChannel integration plus a sample approach to keep the LiveKit client alive across background/foreground transitions. 1) Flutter MethodChannel integration • Add this to your Flutter app (e.g., in a service wrapper file). It exposes
startService()
and
stopService()
to call the native Kotlin service.
Copy code
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.
Copy code
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`:
Copy code
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
Copy code
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?
i
First provide the full MainActivity.kt MethodChannel snippet integrated into a new FlutterActivity.
t
Below is a complete MainActivity.kt you can drop into your Flutter Android module. It extends FlutterActivity, registers a MethodChannel named "livekit_audio_service", and handles
startService
/
stopService
calls by starting/stopping the Kotlin
AudioForegroundService
shown earlier. MainActivity.kt
Copy code
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.
i
Now provide a full working sample that wires LiveKitManager connect flow with lifecycle events and the MethodChannel calls (as you suggested).
t
Below is a concise, ready-to-drop-in sample that wires: • a singleton LiveKitManager (Option A: keep client in Flutter), • lifecycle monitoring that starts/stops the Android foreground service via MethodChannel, • MainActivity.kt MethodChannel (already provided earlier) integration, • and where to call connect/disconnect. Adjust package names, import paths, and LiveKit client initialization to match your app. 1) Android native pieces (Kotlin) • AudioForegroundService.kt (from earlier). Ensure it's in android/app/src/main/kotlin/com/yourcompany/yourapp/ • MainActivity.kt (use the MainActivity provided previously) in same package. • AndroidManifest.xml: add permissions and service declaration inside <application>. 2) Flutter code A. pubspec.yaml (ensure you have LiveKit client package and any needed deps) • Example dependency (replace with actual package you're using): livekit_client: ^0.1.0 flutter: sdk: flutter B. lib/livekit_manager.dart
Copy code
import '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)
Copy code
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)
Copy code
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)
Copy code
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?