简介
通过本篇教程, 您将学习到如何解决在 "Android9之后应用锁屏或切后台采集音视频无效” 问题.
问题描述
我们在接入声网SDK初期, 遇到过这样一个问题, 发现在小米10(Android11)的手机上,
只要把APP切到后台之后, 30秒左右, 推流就会中断, 拉流正常.
后来发现是因为在Android9之后, Android系统对此做了限制
声网跟此问题有关的文章
https://docs.agora.io/cn/live-streaming-premium-legacy/faq/android_background?platform=Android
我们具体的解决方案
1] 需要编写 “前台服务”
/**
* 我的前台服务, 用于在APP进入后台时守护APP, 否则在Android9设备上, 会在APP切入后台1分钟, 无法推流....
*
* <p>
* 为什么 Android 9 应用锁屏或切后台后采集音视频无效?
* 问题现象:Android 9 设备锁屏 1 分钟内,音频无声或看不到视频。
* <p>
* 问题原因:从 Android 官网来看,这是系统强制限制。原文如下:
* <p>
* Limited access to sensors in background
* Android 9 limits the ability for background apps to access user input and sensor data. If your app is running in the background on a device running Android 9, the system applies the following restrictions to your app:
* <p>
* Your app cannot access the microphone or camera.
* Sensors that use the continuous reporting mode, such as accelerometers and gyroscopes, don't receive events.
* Sensors that use the on-change or one-shot reporting modes don't receive events.
* If your app needs to detect sensor events on devices running Android 9, use a foreground service.
* <p>
* 解决方案: 目前 Android 官网没有明确说明后台采集声音或视频应如何处理,但使用前台服务可以让应用正常工作。
* <p>
* 如果 Android 9 设备用户有锁屏后采集音频或视频的需求,可以在锁屏或退至后台前起一个 Service,并在退出锁屏或返回前台前终止 Service。
* 关于如何起 Service,请参考 https://developer.android.com/reference/android/app/Service 。
* 前台服务文档 : https://developer.android.google.cn/guide/components/foreground-services#java
* <p>
* <p>
* 一些关键的信息 :
*
* 前台服务会显示一条状态栏通知(大概内容是 : "PicoPico"正在运行 /n 可能导致系统卡顿,降低待机时间,点按关闭 ),
* 以便用户主动意识到您的应用正在前台执行任务, 并正在消耗系统资源。除非服务停止或从前台删除,否则无法关闭此通知。
*
* 运行 Android 12(API 级别 31)或更高版本的设备,系统会等待 10 秒,然后才会显示与前台服务关联的通知。
* 有几个例外;几种类型的服务总是立即显示通知。
*
* 仅当您的应用程序需要执行用户注意到的任务时,您才应该使用前台服务,即使他们没有直接与应用程序交互。
* 如果操作的重要性足够低以至于您想使用最低优先级通知,请改为创建后台任务。
*
* 面向 Android 9(API 级别 28)或更高版本并使用前台服务的应用必须请求 FOREGROUND_SERVICE 权限
* <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
*
* 在服务内部,通常在 中onStartCommand(),您可以请求您的服务在前台运行。
* 为此,请调用startForeground()。该方法有两个参数:一个唯一标识状态栏中通知的正整数 和 Notification对象本身。
*/
public class MyForegroundServiceForGuardAppInBackground extends Service {
private static final String TAG = "MyForegroundServiceForGuardAppInBackground";
private static final String CHANNEL_ID = MyForegroundServiceForGuardAppInBackground.class.getName();
private static String CHANNEL_NAME = "语音交友房";
private static String CHANNEL_DESC = "";
private static final int ONGOING_NOTIFICATION_ID = 19811127;
private boolean isStartedForeground;
public MyForegroundServiceForGuardAppInBackground() {
DebugLog.e(TAG, "MyForegroundServiceForGuardAppInBackground(构造函数) --> ");
}
public static boolean isAndroid9() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
}
/**
* TODO : 一定要保证在APP处于前台时, 再调用 startForegroundService , 否则虽然能在状态栏看见"PUSH消息", 实际上声网回到前台时, 声音采集起不来
*
* @param context
*/
public static void start(Context context) {
DebugLog.e(TAG, "start --> ");
if (context == null) {
DebugLog.e(TAG, "start --> context is null.");
return;
}
if (!isAndroid9()) {
DebugLog.e(TAG, "start --> is not Android9.");
return;
}
try {
Intent intent = new Intent(context, MyForegroundServiceForGuardAppInBackground.class);
context.startForegroundService(intent);
} catch (Exception e) {
DebugLog.e(TAG, "start --> catch_Exception = " + e.getMessage());
}
}
public static void stop(Context context) {
DebugLog.e(TAG, "stop --> ");
if (context == null) {
DebugLog.e(TAG, "stop --> context is null.");
return;
}
if (!isAndroid9()) {
DebugLog.e(TAG, "stop --> is not Android9.");
return;
}
try {
Intent intent = new Intent(context, MyForegroundServiceForGuardAppInBackground.class);
context.stopService(intent);
} catch (Exception e) {
DebugLog.e(TAG, "stop --> catch_Exception = " + e.getMessage());
}
}
@Override
public void onCreate() {
super.onCreate();
DebugLog.e(TAG, "onCreate --> ");
}
@TargetApi(Build.VERSION_CODES.O)
private void startForeground() {
DebugLog.e(TAG, "startForeground --> ");
try {
NotificationManager manager = ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE));
if (manager != null) {
NotificationChannel channel = manager.getNotificationChannel(CHANNEL_ID); // 已经存在就不要再创建了,无法修改通道配置
if (channel == null) {
channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
channel.setDescription(CHANNEL_DESC);
manager.createNotificationChannel(channel);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(getBaseContext(), CHANNEL_ID);
/**
* @param id The identifier for this notification as per {@link NotificationManager#notify(int, Notification)
* NotificationManager.notify(int, Notification)}; must not be 0.
* 唯一标识状态栏中通知的正整数, 不能为零.
*
* @param notification The Notification to be displayed.
*/
startForeground(ONGOING_NOTIFICATION_ID, builder.build());
}
} catch (Exception e) {
DebugLog.e(TAG, "startForeground --> catch_Exception = " + e.getMessage());
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
DebugLog.e(TAG, "onStartCommand --> ");
if (!isStartedForeground) {
startForeground();
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
DebugLog.e(TAG, "onDestroy --> ");
}
@Override
public boolean onUnbind(Intent intent) {
DebugLog.e(TAG, "onUnbind --> ");
return super.onUnbind(intent);
}
@Override
public void onRebind(Intent intent) {
DebugLog.e(TAG, "onRebind --> ");
super.onRebind(intent);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
DebugLog.e(TAG, "onBind --> ");
return null;
}
}
2] 在 AndroidManifest.xml 中对 该前台服务 进行配置
<service
android:name="com.xintiaotime.foundation.service.MyForegroundServiceForGuardAppInBackground"
android:foregroundServiceType="microphone" />
注意 : android:foregroundServiceType="microphone"
3] 调用 “前台服务” 的时机
进入语音房界面之后
// TODO : 一定要保证在APP处于前台时, 再调用 startForegroundService , 否则虽然能在状态栏看见"PUSH消息", 实际上声网回到前台时, 声音采集起不来
MyForegroundServiceForGuardAppInBackground.start(context);
离开语音房时
// TODO : 关闭 "前台服务"
MyForegroundServiceForGuardAppInBackground.stop(context);