อุปกรณ์ Android TV สามารถเชื่อมต่อเอาต์พุตเสียงได้หลายรายการพร้อมกัน เช่น ลำโพงทีวี โรงภาพยนตร์ในบ้านที่เชื่อมต่อผ่าน HDMI, หูฟังบลูทูธ และอื่นๆ อุปกรณ์เอาต์พุตเสียงเหล่านี้รองรับความสามารถด้านเสียงที่หลากหลาย เช่น การเข้ารหัส (Dolby Digital+, DTS และ PCM), อัตราตัวอย่าง และช่อง ตัวอย่างเช่น ทีวีที่เชื่อมต่อ HDMI รองรับการเข้ารหัสหลายรูปแบบ ในขณะที่หูฟังบลูทูธที่เชื่อมต่อมักจะรองรับเฉพาะ PCM
รายการอุปกรณ์เสียงที่ใช้ได้และอุปกรณ์เสียงที่กำหนดเส้นทางยังเปลี่ยนแปลงได้ด้วยการเสียบอุปกรณ์ HDMI ร้อนๆ การเชื่อมต่อหรือยกเลิกการเชื่อมต่อหูฟังบลูทูธ หรือผู้ใช้เปลี่ยนการตั้งค่าเสียง เนื่องจากความสามารถของเอาต์พุตเสียงอาจเปลี่ยนแปลงได้แม้ในขณะที่แอปเล่นสื่ออยู่ แอปจึงต้องปรับตัวให้เข้ากับการเปลี่ยนแปลงเหล่านี้และเล่นต่อในอุปกรณ์เสียงที่กำหนดเส้นทางใหม่และความสามารถของอุปกรณ์ การส่งออกรูปแบบเสียงที่ไม่ถูกต้องอาจทำให้เกิดข้อผิดพลาดหรือเสียงไม่เล่น
แอปสามารถส่งออกเนื้อหาเดียวกันได้หลายรูปแบบการเข้ารหัสเพื่อมอบประสบการณ์เสียงที่ดีที่สุดให้แก่ผู้ใช้ โดยขึ้นอยู่กับความสามารถของอุปกรณ์เสียง ตัวอย่างเช่น ระบบจะเล่นสตรีมเสียงที่เข้ารหัส Dolby Digital หากทีวีรองรับ ส่วนสตรีมเสียง PCM ที่รองรับได้กว้างกว่าจะถูกเลือกเมื่อไม่รองรับ Dolby Digital ดูรายการโปรแกรมถอดรหัสในตัวของ 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, บลูทูธ และอื่นๆ) - สำหรับ 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);