涉及硬件的音视频能力,比如采集、渲染、硬件编码、硬件解码,通常是与客户端操作系统强相关的,就算是跨平台的多媒体框架也必须使用平台原生语言的模块来支持这些功能。本系列文章将详细讲述移动端音视频的采集、渲染、硬件编码、硬件解码这些涉及硬件的能力该如何实现。
本文为该系列文章的第 4 篇,将详细讲述在 Android 平台下影响音频路由相关的知识点。
咋回事,怎么听不到对方的声音了? 我这明明播放了音乐啊,怎么什么声音都没有?
相信做过音视频业务的同学都遇到过类似的问题,当然出现此类问题的原因比较多,例如:音频设备故障,网络、音频路由等,其他的我们先暂时搁置一旁,今天着重讲讲音频路由相关的知识点。
音频路由所产生的音频采集、播放异常(故障) 对业务产生的影响持续时间比较差,且难于排查。主要原因是开发者不仅需要对 Android 平台音频路由相关的知识点非常熟悉,还需要很多一些特殊机型、特殊场景下的经验值。本文会详细介绍下影响 Android 音频路由相关的应用层知识点。
在 Android 平台中有三个非常重要的参数,audioSource、streamType、audioMode 这三种参数分别表示 采集音频源、播放音频流类型、音频场景,他们很大程度上决定着系统路由(指的是输入设备和输出设备)的选择和优先级。除了在 commuication 的模式下,可以允许开发者控制扬声器和听筒的切换,大部分场景音频路由都是由系统根据这三个参数来决定的。
PS: 如果您阅读过之前有关音频采集的文章,应该对这个参数很熟悉。
表示采集源类型,通常会影响到系统选择采集设备的优先级,常见的类型值:
从上面的描述来看,audioSource 不仅会影响优先级,还会根据音频源的用途做一些额外的音频处理,但这些通常和路由没什么关系,普通开发者这里只需要知道就行。
以下是 audiosource 为 VOICE_COMMUNICATION 的情况下,Android 音频路由选择的代码逻辑。
case AUDIO_SOURCE_VOICE_COMMUNICATION:
//如果是 in_call 状态,指定可用设备列表 (primary)。
if ((getPhoneState() == AUDIO_MODE_IN_CALL) &&
(availableOutputDevices.getDevice(AUDIO_DEVICE_OUT_TELEPHONY_TX,
String8(""), AUDIO_FORMAT_DEFAULT)) == nullptr) {
LOG_ALWAYS_FATAL_IF(availablePrimaryDevices.isEmpty(), "Primary devices not found");
availableDevices = availablePrimaryDevices;
}
//优先选择带有语音通信的蓝牙
if (audio_is_bluetooth_out_sco_device(commDeviceType)) {
// if SCO device is requested but no SCO device is available, fall back to default case
device = availableDevices.getDevice(
AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET, String8(""), AUDIO_FORMAT_DEFAULT);
if (device != nullptr) {
break;
}
}
switch (commDeviceType) {
//支持低功耗协议的蓝牙设备
case AUDIO_DEVICE_OUT_BLE_HEADSET:
device = availableDevices.getDevice(
AUDIO_DEVICE_IN_BLE_HEADSET, String8(""), AUDIO_FORMAT_DEFAULT);
break;
//外放(喇叭)
case AUDIO_DEVICE_OUT_SPEAKER:
device = availableDevices.getFirstExistingDevice({
AUDIO_DEVICE_IN_BACK_MIC, AUDIO_DEVICE_IN_BUILTIN_MIC,
AUDIO_DEVICE_IN_USB_DEVICE, AUDIO_DEVICE_IN_USB_HEADSET});
break;
default: // FORCE_NONE
//如果是其他的 audiosource,则按照以下顺序来选择
device = availableDevices.getFirstExistingDevice({
AUDIO_DEVICE_IN_WIRED_HEADSET, AUDIO_DEVICE_IN_USB_HEADSET,
AUDIO_DEVICE_IN_USB_DEVICE, AUDIO_DEVICE_IN_BLUETOOTH_BLE,
AUDIO_DEVICE_IN_BUILTIN_MIC});
break;
}
break;streamType 用于对播放的音频流进行分类,主要包括以下几种常见类型:
streamType 的作用就是标识每条播放音频流的类型,用途如下:

以下是通话模式下,Android 音频输出设备选择的代码逻辑。
//系统底层会对 streamType 再一次的分类,
//STRATEGY_PHONE 是 STREAM_VOICE_CALL 和 STREAM_BLUETOOTH_SCO 的集合
//这里在不赘述
case STRATEGY_PHONE: {
//为了用户体验,会优先选择最后使用过的可插拔音频输出设备。
devices = availableOutputDevices.getFirstDevicesFromTypes(
getLastRemovableMediaDevices(GROUP_NONE, {
//要排除这两种设备,因为 Dialer 系统应用会显示的使用它们
AUDIO_DEVICE_OUT_HEARING_AID,
AUDIO_DEVICE_OUT_BLE_HEADSET
}));
if (!devices.isEmpty()) break;
//如果最近没有使用过可插拔的音频输出设备,按照以下顺序来选择输出设备。
devices = availableOutputDevices.getFirstDevicesFromTypes({
AUDIO_DEVICE_OUT_DGTL_DOCK_HEADSET, AUDIO_DEVICE_OUT_EARPIECE,
AUDIO_DEVICE_OUT_SPEAKER});
} break;刚才前文提了,audioMode 会和音频路由相关。但更为准确的说法是,audioMode 是一种全局的设置,不仅仅会影响到音频路由。接下来,我们举个例子来说明这个参数实际发挥的作用。
比如,你正带着蓝牙耳机接收 phone A 的音乐,这时候你用 phone B 拨打 phone A 的电话。这时候系统会切入到 MODE_RINGTONE 模式 (大概率应该是系统应用电话设置的),这时候的变化是,播放的音乐突然暂停了,然后来电的响铃声从耳机和扬声器同时播放出来了。这时候你按物理按键只能调节铃声的音量了 (对应着 STREAM_RING 这种类型的 streamType)。
从这个例子的现象,我们可以从中 Get 到:
当两个或更多个应用向同一输出流播放音频,系统会将所有的音频流混合在一起播放。这会为用户带来一定的烦恼,因为有时用户并不希望如此。所以,Android 引入了 “音频焦点” 的概念,它是官方为应用设计的一个协商机制。同一个时刻,只有一个应用才可以获取音频焦点,获取音频焦点之后你才可以播放音频流。
在 Android 12 之前,这个协商机制并不是强制的。虽然比较注重体验的应用都会遵守这个机制,但是还是会有很多应用不会遵守,这还多少还是会影响到用户体验,所以在 Android 12 之后,系统收回了这部分的权利,由系统来接管。
下面介绍下不同版本关于音频焦点的适配代码逻辑。
step1) 调用 requestAudioFocus 请求音频焦点,聊下三个参数的具体用法。
以下是 Android 官方提供的代码:
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
AudioManager.OnAudioFocusChangeListener afChangeListener;
...
// Request audio focus for playback
int result = audioManager.requestAudioFocus(afChangeListener,
// Use the music stream.
AudioManager.STREAM_MUSIC,
// Request permanent focus.
AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// Start playback
}
private Handler handler = new Handler();
AudioManager.OnAudioFocusChangeListener afChangeListener =
new AudioManager.OnAudioFocusChangeListener() {
public void onAudioFocusChange(int focusChange) {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
// Permanent loss of audio focus
// Pause playback immediately
mediaController.getTransportControls().pause();
// Wait 30 seconds before stopping playback
handler.postDelayed(delayedStopRunnable,
TimeUnit.SECONDS.toMillis(30));
}
else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
// Pause playback
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// Lower the volume, keep playing
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// Your app has been granted audio focus again
// Raise volume to normal, restart playback if necessary
}
}
};step2) 放弃音频焦点
audioManager.abandonAudioFocus(afChangeListener);这个阶段的 Android 系统版本,官方做了一些变更,但是接管的权利还是在开发者的手里。首先初始化的方式有所变动,提供了 Builder 来构建这块的参数 (请阅读下代码)。 下面介绍下接口的用法:
// initializing variables for audio focus and playback management
audioManager = (AudioManager) Context.getSystemService(Context.AUDIO_SERVICE);
playbackAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(playbackAttributes)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(afChangeListener, handler)
.build();
final Object focusLock = new Object();
boolean playbackDelayed = false;
boolean playbackNowAuthorized = false;
// requesting audio focus and processing the response
int res = audioManager.requestAudioFocus(focusRequest);
synchronized(focusLock) {
if (res == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
playbackNowAuthorized = false;
} else if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
playbackNowAuthorized = true;
playbackNow();
} else if (res == AudioManager.AUDIOFOCUS_REQUEST_DELAYED) {
playbackDelayed = true;
playbackNowAuthorized = false;
}
}
// implementing OnAudioFocusChangeListener to react to focus changes
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
if (playbackDelayed || resumeOnFocusGain) {
synchronized(focusLock) {
playbackDelayed = false;
resumeOnFocusGain = false;
}
playbackNow();
}
break;
case AudioManager.AUDIOFOCUS_LOSS:
synchronized(focusLock) {
resumeOnFocusGain = false;
playbackDelayed = false;
}
pausePlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
synchronized(focusLock) {
// only resume if playback is being interrupted
resumeOnFocusGain = isPlaying();
playbackDelayed = false;
}
pausePlayback();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// ... pausing or ducking depends on your app
break;
}
}
}Android 12 之后,系统开始接管音频焦点这块的功能,也是为了用户更好的音频体验。下面列举下三种场景的音频焦点变更的变化。
A 应用当前获取了音频焦点,正在播放音频流。
B 应用通过 AudioManager.AUDIOFOCUS_GAIN 方式来抢占音频焦点。
那么 A 应用将被系统强制停止播放,如果你想增加一些淡出的效果,配置必须满足以下条件:
当前抢占音频焦点的应用 A 配置如下:
应用 B 通过 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK 来抢占音频焦点。
那么当应用 B 抢占了音频焦点之后,系统会自动的将应用 A 音量降低。
如果您的应用请求音频焦点的代码中里 setUsage 设置 AudioAttributes.USAGE_MEDIA 或者 AudioAttributes.USAGE_GAME 。来电时,系统会自动暂停应用的音频流,待通话结束后自动开启。
以上就是本文的所有内容了,介绍了影响 Android 平台音频路由的相关知识点,相信对您有所帮助。
本文是音视频基础能力 - Android 音频篇的第四篇,后续精彩内容,敬请期待。往期精彩内容,可参考:
# 音视频基础能力之 Android 音频篇 (一): 音频采集
# 音视频基础能力之 Android 音频篇 (三):高性能音频采集
打个广告,欢迎关注我们运营的公众号 声知视界,会定期推送移动端、音视频领域的相关的科普文章。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。