Android TV 支援多個並行音訊輸出:電視喇叭、HDMI 家庭劇院、藍牙等。這些裝置支援不同的編碼 (例如 Dolby Digital+、DTS、PCM)、取樣率和聲道。透過 HDMI 連接的電視支援多種格式,但藍牙僅支援 PCM。
播放期間,可用的音訊裝置和音訊路徑可能會變更:使用者可能會熱插拔 HDMI、連線藍牙或修改設定。應用程式必須妥善調整,確保能使用新裝置的功能持續播放內容。不支援的格式可能會導致錯誤或無聲。
根據裝置功能提供多種編碼,以獲得最佳體驗。 舉例來說,如果裝置支援 Dolby Digital,就會使用這項技術;否則應用程式會改用 PCM。如要瞭解可將串流轉換為 PCM 的 Android 解碼器,請參閱「支援的媒體格式」。
播放時,串流應用程式應建立 AudioTrack,並使用輸出音訊裝置支援的最佳 AudioFormat。
建立格式正確的音軌
應用程式應建立 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 外掛程式事件,並使用廣播中的額外資料。
- 此外,請註冊
BroadcastReceiver,監控 API 23 以下裝置的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);