技术背景
我们在做Android端同屏的时候,开发者希望可以高版本的Android系统上,在设备支持的前提下,可以采集到扬声器输出的audio,并支持和麦克风采集的audio相互切换,实现无纸化|智慧教室同屏不同audio模式的输出。Android系统出于安全和隐私的考虑,默认并不允许应用程序直接访问系统级别的音频输出。
从Android 10(API级别29)开始,Android引入了媒体投影API(MediaProjection),允许应用捕获屏幕内容以及音频。但是,直接捕获扬声器输出的音频并不是通过MediaProjection API直接实现的,而是通常与屏幕录制功能一起提供。
- 启用屏幕录制权限:应用需要请求
RECORD_AUDIO
和CAPTURE_AUDIO_OUTPUT
权限,以及CAPTURE_VIDEO_OUTPUT
和CAPTURE_SECURE_VIDEO_OUTPUT
(如果捕获安全内容)。 - 使用MediaProjectionManager:创建一个MediaProjection会话,并引导用户通过系统UI授权屏幕录制。
- 捕获音频:在录制屏幕时,音频也会同时被捕获。但是,这通常只适用于用户当前正在操作的应用的音频输出,而不是整个系统的音频。
技术实现
本文以大牛直播SDK的SmartServicePublisherV2这个demo为例,介绍下相关的技术实现。
编辑
如果需要支持音频播放采集和麦克风采集,可以想把这两个选项打开,然后,通过右侧下拉框,推送过程中,实时切换数据源。
采集麦克风实现逻辑:
/* * NTStreamMediaProjectionEngineImpl.java * Author: daniusdk.com * WeChat: xinsheng120 * * Copyright © 2014~2024 DaniuSDK. All rights reserved. */ public boolean start_audio_record(int sample_rate, int channels) { if (audio_record_.get() != null) { Log.e(TAG, "start_audio_record audio_record already exists"); return false; } if (!check_record_audio_permission()) { Log.e(TAG, "start_audio_record RECORD_AUDIO permission is missing"); return false; } NTAudioRecordV2 audio_record = new NTAudioRecordV2(get_application_context()); // audio_record.IsMicSource(true); //如音频采集声音过小,建议开启 // audio_record.IsRemoteSubmixSource(true); // Anrdoid 10以下, 有些设备可能能用 if (!audio_record.Start(sample_rate, channels)) { Log.e(TAG, "start_audio_record start failed"); return false; } if (audio_record_callback_ != null) audio_record.AddCallback(audio_record_callback_); NTAudioRecordV2 old = audio_record_.getAndSet(audio_record); if (old != null) { if (audio_record_callback_ != null) old.RemoveCallback(audio_record_callback_); old.Stop(); } Log.i(TAG, "start_audio_record ok, sample_rate:" + sample_rate + ", channels:" + channels); return true; } public boolean is_audio_record_running() { return audio_record_.get() != null; } public void stop_audio_record() { NTAudioRecordV2 old = audio_record_.getAndSet(null); if (old != null) { if (audio_record_callback_ != null) old.RemoveCallback(audio_record_callback_); old.Stop(); Log.i(TAG, "call audio_record_.Stop."); } }
采集音频播放声音(扬声器)实现逻辑:
/* * NTStreamMediaProjectionEngineImpl.java * Author: daniusdk.com * WeChat: xinsheng120 * * Copyright © 2014~2024 DaniuSDK. All rights reserved. */ public boolean start_audio_playback_capture(int sample_rate, int channels) { if (Build.VERSION.SDK_INT < 29) { Log.e(TAG, "start_audio_playback_capture Device SDK_INT:" + Build.VERSION.SDK_INT +" < 29(Android 10)"); return false; } if (audio_playback_capture_.get() != null) { Log.e(TAG, "start_audio_playback_capture capture already exists"); return false; } if (!check_record_audio_permission()) { Log.e(TAG, "start_audio_playback_capture RECORD_AUDIO permission is missing."); return false; } MediaProjection media_projection = get_media_projection(); if (null == media_projection) { Log.e(TAG, "start_audio_playback_capture media_projection is null"); return false; } NTAudioPlaybackCapture capture = new NTAudioPlaybackCapture(); if (!capture.start(get_application_context(), media_projection, sample_rate, channels)) { capture.close(); Log.e(TAG, "start_audio_playback_capture start failed"); return false; } if (audio_playback_capture_callback_ != null) capture.register_callback(audio_playback_capture_callback_); NTAudioPlaybackCapture old = audio_playback_capture_.getAndSet(capture); if (old != null) old.close(); Log.i(TAG, "start_audio_playback_capture ok, sample_rate:" + sample_rate + ", channels:" + channels); return true; } public boolean is_audio_playback_capture_running() { return audio_playback_capture_.get() != null; } public void stop_audio_playback_capture() { NTAudioPlaybackCapture old = audio_playback_capture_.getAndSet(null); if (old != null) { old.close(); Log.i(TAG, "stop_audio_playback_capture capture.close."); } }
启动RTMP推送或轻量级RTSP服务过程中,切换采集扬声器或者麦克风:
public boolean set_audio_output_type(int type) { if (type < 0 || type > 2) { Log.e(TAG, "set_audio_output_type type:" + type + " error"); return false; } Runnable r = new Runnable() { int type_; public void run() { audio_output_type_ = this.type_; Log.i(TAG, "set_audio_output_type value:" + this.type_); if (stream_publisher_.is_publishing()) switch_audio_output_type(audio_output_type_); } Runnable set(int type) { this.type_ = type; return this; } }.set(type); post_or_execute(r); return true; }
播放效果如下(Android采集屏幕和麦克风|扬声器audio,然后推送到RTMP服务和轻量级RTSP服务),扬声器audio采集,特别是视频播放模式下,比如无纸化同屏过程中,需要放个宣传片,或者一些视频材料,非常方便:
编辑
总结
Android平台扬声器播放声音的采集,在无纸化同屏等场景下,意义很大,早期低版本的Android设备,是没法直接采集扬声器audio的(从Android 10开始支持),所以,如果需要采集扬声器audio,需要先做系统版本判断,添加相应的权限。如果需要实时切换扬声器或麦克风声音,可以参考上述实现逻辑,以上是大概的流程,感兴趣的开发者,可以单独跟我沟通讨论。