Android TV поддерживает несколько одновременных аудиовыходов: динамики телевизора, домашний кинотеатр HDMI, Bluetooth и т. д. Эти устройства поддерживают разные кодеки (например, Dolby Digital+, DTS, PCM), частоту дискретизации и каналы. Телевизоры, подключенные через HDMI, поддерживают множество форматов, а Bluetooth – только PCM.
Доступные аудиоустройства и маршрутизация могут меняться во время воспроизведения: пользователи могут подключать HDMI, Bluetooth или изменять настройки. Приложения должны корректно адаптироваться к новым возможностям устройства, чтобы воспроизведение продолжалось. Неподдерживаемые форматы могут вызывать ошибки или тишину.
Предлагайте несколько вариантов кодирования, чтобы обеспечить оптимальное качество воспроизведения на разных устройствах. Например, если поддерживается Dolby Digital, используется этот формат, а если нет, то приложение переключается на PCM. Список декодеров Android, которые преобразуют потоки в PCM, приведен в разделе Поддерживаемые форматы медиаконтента.
Во время воспроизведения потоковый сервис должен создать AudioTrack с лучшим AudioFormat, поддерживаемым выходным аудиоустройством.
Создайте трек в подходящем формате
Приложения должны создать AudioTrack, начать его воспроизведение и вызвать getRoutedDevice(), чтобы определить аудиоустройство по умолчанию, на котором будет воспроизводиться звук. Например, это может быть короткая безопасная дорожка с тишиной, закодированная в формате PCM, которая используется только для определения подключенного устройства и его аудиовозможностей.
Как получить список поддерживаемых кодировок
Используйте getAudioProfiles() (уровень API 31 и выше) или getEncodings() (уровень API 23 и выше), чтобы определить аудиоформаты, доступные на аудиоустройстве по умолчанию.
Проверьте поддерживаемые аудиопрофили и форматы
Используйте AudioProfile (уровень API 31 и выше) или isDirectPlaybackSupported() (уровень API 29 и выше), чтобы проверить поддерживаемые сочетания формата, количества каналов и частоты дискретизации.
Некоторые устройства Android поддерживают кодировки, которые не поддерживаются выходным аудиоустройством. Дополнительные форматы должны быть обнаружены с помощью isDirectPlaybackSupported(). В таких случаях аудиоданные перекодируются в формат, поддерживаемый выходным аудиоустройством. Используйте isDirectPlaybackSupported(), чтобы проверить, поддерживается ли выбранный формат, даже если его нет в списке, возвращенном getEncodings().
Устройство вывода звука для предварительного прослушивания
В 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.
Как создать аудиодорожку
Приложения должны использовать эту информацию, чтобы создать AudioTrack для AudioFormat самого высокого качества, поддерживаемого аудиоустройством по умолчанию (и доступного для выбранного контента).
Перехват изменений аудиоустройства
Чтобы перехватывать и обрабатывать изменения аудиоустройств, приложения должны:
- Для уровней API 24 и выше добавьте
OnRoutingChangedListener, чтобы отслеживать изменения аудиоустройств (HDMI, Bluetooth и т. д.). - Для уровня API 23 зарегистрируйте
AudioDeviceCallback, чтобы получать уведомления об изменениях в списке доступных аудиоустройств. - Для уровней API 21 и 22 отслеживайте события подключения HDMI и используйте дополнительные данные из трансляций.
- Также зарегистрируйте
BroadcastReceiver, чтобы отслеживать изменения состоянияBluetoothDeviceна устройствах с уровнем API ниже 23, посколькуAudioDeviceCallbackещё не поддерживается.
Когда для объекта AudioTrack обнаруживается изменение аудиоустройства, приложение должно проверить обновленные аудиовозможности и, если необходимо, воссоздать объект AudioTrack с другим объектом AudioFormat. Это нужно сделать, если теперь поддерживается более качественное кодирование или если ранее использовавшееся кодирование больше не поддерживается.
Пример кода
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);