Android TV 设备可以同时连接多个音频输出:电视扬声器、HDMI 连接的家庭影院、蓝牙耳机等等。这些音频输出设备可以支持不同的音频功能,例如编码(杜比数字+、DTS 和 PCM)、采样率和声道。例如,HDMI 连接的电视支持多种编码,而连接的蓝牙耳机通常只支持 PCM。
可用音频设备列表和路由的音频设备也可以通过热插拔 HDMI 设备、连接或断开蓝牙耳机或用户更改音频设置来更改。由于即使应用正在播放媒体,音频输出功能也会发生变化,因此应用需要优雅地适应这些变化,并继续在新的路由音频设备及其功能上播放。输出错误的音频格式会导致错误或没有声音播放。
应用能够以多种编码输出相同的内容,以便根据音频设备的功能为用户提供最佳的音频体验。例如,如果电视支持,则播放杜比数字编码的音频流;如果没有杜比数字支持,则选择更广泛支持的 PCM 音频流。可在 支持的媒体格式 中找到用于将音频流转换为 PCM 的内置 Android 解码器的列表。
在播放时,流式应用应使用输出音频设备支持的最佳 AudioFormat
创建一个 AudioTrack
。
创建具有正确格式的轨道
应用应创建一个 AudioTrack
,开始播放它,并调用 getRoutedDevice()
来确定从中播放声音的默认音频设备。例如,这可以是一个仅用于确定路由设备及其音频功能的安全简短静音 PCM 编码轨道。
获取支持的编码
使用 getAudioProfiles()
(API 级别 31 及更高版本)或 getEncodings()
(API 级别 23 及更高版本)来确定默认音频设备上可用的音频格式。
检查支持的音频配置文件和格式
使用 AudioProfile
(API 级别 31 及更高版本)或 isDirectPlaybackSupported()
(API 级别 29 及更高版本)来检查格式、声道数和采样率的支持组合。
某些 Android 设备能够支持输出音频设备支持的编码以外的编码。这些附加格式应通过 isDirectPlaybackSupported()
进行检测。在这些情况下,音频数据将重新编码为输出音频设备支持的格式。即使 getEncodings()
返回的列表中没有所需的格式,也应使用 isDirectPlaybackSupported()
正确检查对该格式的支持。
预期音频路由
Android 13(API 级别 33)引入了预期音频路由。您可以预期设备音频属性支持并为活动音频设备准备轨道。您可以使用 getDirectPlaybackSupport()
检查对于给定的格式和属性,当前路由的音频设备是否支持直接播放
Kotlin
val format = AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_E_AC3) .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) .setSampleRate(48000) .build() val attributes = AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .build() if (AudioManager.getDirectPlaybackSupport(format, attributes) != AudioManager.DIRECT_PLAYBACK_NOT_SUPPORTED ) { // The format and attributes are supported for direct playback // on the currently active routed audio path } else { // The format and attributes are NOT supported for direct playback // on the currently active routed audio path }
Java
AudioFormat format = new AudioFormat.Builder() .setEncoding(AudioFormat.ENCODING_E_AC3) .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) .setSampleRate(48000) .build(); AudioAttributes attributes = new AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_MEDIA) .build(); if (AudioManager.getDirectPlaybackSupport(format, attributes) != AudioManager.DIRECT_PLAYBACK_NOT_SUPPORTED) { // The format and attributes are supported for direct playback // on the currently active routed audio path } else { // The format and attributes are NOT supported for direct playback // on the currently active routed audio path }
或者,您可以查询当前路由音频设备支持哪些配置文件进行直接媒体播放。这排除了任何不受支持的配置文件,或者例如,会被 Android 框架转码的配置文件。
Kotlin
private fun findBestAudioFormat(audioAttributes: AudioAttributes): AudioFormat { val preferredFormats = listOf( AudioFormat.ENCODING_E_AC3, AudioFormat.ENCODING_AC3, AudioFormat.ENCODING_PCM_16BIT, AudioFormat.ENCODING_DEFAULT ) val audioProfiles = audioManager.getDirectProfilesForAttributes(audioAttributes) val bestAudioProfile = preferredFormats.firstNotNullOf { format -> audioProfiles.firstOrNull { it.format == format } } val sampleRate = findBestSampleRate(bestAudioProfile) val channelMask = findBestChannelMask(bestAudioProfile) return AudioFormat.Builder() .setEncoding(bestAudioProfile.format) .setSampleRate(sampleRate) .setChannelMask(channelMask) .build() }
Java
private AudioFormat findBestAudioFormat(AudioAttributes audioAttributes) { Stream<Integer> preferredFormats = Stream.<Integer>builder() .add(AudioFormat.ENCODING_E_AC3) .add(AudioFormat.ENCODING_AC3) .add(AudioFormat.ENCODING_PCM_16BIT) .add(AudioFormat.ENCODING_DEFAULT) .build(); Stream<AudioProfile> audioProfiles = audioManager.getDirectProfilesForAttributes(audioAttributes).stream(); AudioProfile bestAudioProfile = (AudioProfile) preferredFormats.map(format -> audioProfiles.filter(profile -> profile.getFormat() == format) .findFirst() .orElseThrow(NoSuchElementException::new) ); Integer sampleRate = findBestSampleRate(bestAudioProfile); Integer channelMask = findBestChannelMask(bestAudioProfile); return new AudioFormat.Builder() .setEncoding(bestAudioProfile.getFormat()) .setSampleRate(sampleRate) .setChannelMask(channelMask) .build(); }
在这个例子中,preferredFormats
是一个 AudioFormat
实例列表。它按优先级排序,列表中优先级最高的排在最前面,优先级最低的排在最后面。getDirectProfilesForAttributes()
返回当前路由音频设备支持的 AudioProfile
对象列表,并使用提供的 AudioAttributes
。系统会遍历首选 AudioFormat
项目列表,直到找到匹配的支持的 AudioProfile
。此 AudioProfile
将存储为 bestAudioProfile
。最佳采样率和声道掩码由 bestAudioProfile
确定。最后,创建一个合适的 AudioFormat
实例。
创建音频轨道
应用应使用此信息为默认音频设备支持的最高质量 AudioFormat
(以及所选内容可用的)创建 AudioTrack
。
拦截音频设备更改
要拦截并对音频设备更改做出反应,应用应:
- 对于 API 等级等于或大于 24,添加
OnRoutingChangedListener
来监控音频设备更改(HDMI、蓝牙等)。 - 对于 API 等级 23,注册
AudioDeviceCallback
以接收可用音频设备列表中的更改。 - 对于 API 等级 21 和 22,监控 HDMI 插拔事件 并使用广播中的额外数据。
- 对于低于 API 23 的设备,还要注册一个
BroadcastReceiver
来监控BluetoothDevice
状态更改,因为AudioDeviceCallback
尚未支持。
当检测到 AudioTrack
的音频设备更改时,应用应检查更新的音频功能,如果需要,则使用不同的 AudioFormat
重新创建 AudioTrack
。如果现在支持更高质量的编码或先前使用的编码不再支持,则执行此操作。
示例代码
Kotlin
// audioPlayer is a wrapper around an AudioTrack // which calls a callback for an AudioTrack write error audioPlayer.addAudioTrackWriteErrorListener { // error code can be checked here, // in case of write error try to recreate the audio track restartAudioTrack(findDefaultAudioDeviceInfo()) } audioPlayer.audioTrack.addOnRoutingChangedListener({ audioRouting -> audioRouting?.routedDevice?.let { audioDeviceInfo -> // use the updated audio routed device to determine // what audio format should be used if (needsAudioFormatChange(audioDeviceInfo)) { restartAudioTrack(audioDeviceInfo) } } }, handler)
Java
// audioPlayer is a wrapper around an AudioTrack // which calls a callback for an AudioTrack write error audioPlayer.addAudioTrackWriteErrorListener(new AudioTrackPlayer.AudioTrackWriteError() { @Override public void audioTrackWriteError(int errorCode) { // error code can be checked here, // in case of write error try to recreate the audio track restartAudioTrack(findDefaultAudioDeviceInfo()); } }); audioPlayer.getAudioTrack().addOnRoutingChangedListener(new AudioRouting.OnRoutingChangedListener() { @Override public void onRoutingChanged(AudioRouting audioRouting) { if (audioRouting != null && audioRouting.getRoutedDevice() != null) { AudioDeviceInfo audioDeviceInfo = audioRouting.getRoutedDevice(); // use the updated audio routed device to determine // what audio format should be used if (needsAudioFormatChange(audioDeviceInfo)) { restartAudioTrack(audioDeviceInfo); } } } }, handler);