[声网音频SDK实战教程] 对于在 "Android9之后应用锁屏或切后台采集音视频无效" 问题的实战教程

简介

通过本篇教程, 您将学习到如何解决在 "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);


推荐阅读
相关专栏
开发者实践
182 文章
本专栏仅用于分享音视频相关的技术文章,与其他开发者和声网 研发团队交流、分享行业前沿技术、资讯。发帖前,请参考「社区发帖指南」,方便您更好的展示所发表的文章和内容。