Merge pull request #2693 from vitorpamplona/claude/fix-nests-audio-issue-a8sFv
Switch audio playback to USAGE_MEDIA for hands-free rooms
This commit is contained in:
+6
-1
@@ -74,6 +74,11 @@ class NestForegroundService : Service() {
|
|||||||
* call lowers the room volume cleanly instead of mixing two
|
* call lowers the room volume cleanly instead of mixing two
|
||||||
* voices on top of each other. Acquired once per service
|
* voices on top of each other. Acquired once per service
|
||||||
* lifetime; released in [onDestroy].
|
* lifetime; released in [onDestroy].
|
||||||
|
*
|
||||||
|
* Matches the playback `AudioAttributes` we set on `AudioTrack`
|
||||||
|
* in `AudioTrackPlayer` (USAGE_MEDIA + CONTENT_TYPE_SPEECH) so
|
||||||
|
* the focus request applies to the same stream the audio
|
||||||
|
* actually renders on.
|
||||||
*/
|
*/
|
||||||
private fun requestAudioFocus() {
|
private fun requestAudioFocus() {
|
||||||
if (audioFocusRequest != null) return
|
if (audioFocusRequest != null) return
|
||||||
@@ -81,7 +86,7 @@ class NestForegroundService : Service() {
|
|||||||
val attrs =
|
val attrs =
|
||||||
android.media.AudioAttributes
|
android.media.AudioAttributes
|
||||||
.Builder()
|
.Builder()
|
||||||
.setUsage(android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
|
||||||
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_SPEECH)
|
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||||
.build()
|
.build()
|
||||||
val request =
|
val request =
|
||||||
|
|||||||
+18
-8
@@ -21,23 +21,36 @@
|
|||||||
package com.vitorpamplona.nestsclient.audio
|
package com.vitorpamplona.nestsclient.audio
|
||||||
|
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.media.AudioManager
|
|
||||||
import android.media.AudioTrack
|
import android.media.AudioTrack
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import android.media.AudioFormat as AndroidAudioFormat
|
import android.media.AudioFormat as AndroidAudioFormat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [AudioPlayer] backed by Android's [AudioTrack] in `MODE_STREAM`. Targets the
|
* [AudioPlayer] backed by Android's [AudioTrack] in `MODE_STREAM`.
|
||||||
* voice-call usage stream so the OS treats audio-room playback like a phone
|
*
|
||||||
* call (volume rocker controls call volume, ducks notifications, etc.).
|
* Routes through `USAGE_MEDIA` + `CONTENT_TYPE_SPEECH` (= `STREAM_MUSIC`)
|
||||||
|
* so audio-room playback comes out of the loudspeaker by default and the
|
||||||
|
* volume rocker controls media volume — same approach Twitter/X Spaces
|
||||||
|
* and Clubhouse use for hands-free audio rooms.
|
||||||
|
*
|
||||||
|
* Originally this used `USAGE_VOICE_COMMUNICATION` to get call-style
|
||||||
|
* volume / ducking, but that routes through `STREAM_VOICE_CALL`, which
|
||||||
|
* Android only services audibly while the device is in
|
||||||
|
* `MODE_IN_COMMUNICATION`. Nests doesn't drive `AudioManager.mode`
|
||||||
|
* (only the NIP-100 `CallAudioManager` does), so the playback either
|
||||||
|
* dropped to the earpiece at near-zero volume or produced no audio at
|
||||||
|
* all depending on the device — making rooms appear silent on both
|
||||||
|
* phones. Echo cancellation still works on the capture side via
|
||||||
|
* `MediaRecorder.AudioSource.VOICE_COMMUNICATION` regardless of the
|
||||||
|
* playback usage.
|
||||||
*
|
*
|
||||||
* Buffer sizing: 4× minimum so the producer can fall behind by ~80 ms before
|
* Buffer sizing: 4× minimum so the producer can fall behind by ~80 ms before
|
||||||
* dropouts, which roughly matches the jitter the WebTransport datagram path
|
* dropouts, which roughly matches the jitter the WebTransport datagram path
|
||||||
* introduces over typical mobile networks.
|
* introduces over typical mobile networks.
|
||||||
*/
|
*/
|
||||||
class AudioTrackPlayer(
|
class AudioTrackPlayer(
|
||||||
private val usage: Int = AudioAttributes.USAGE_VOICE_COMMUNICATION,
|
private val usage: Int = AudioAttributes.USAGE_MEDIA,
|
||||||
private val contentType: Int = AudioAttributes.CONTENT_TYPE_SPEECH,
|
private val contentType: Int = AudioAttributes.CONTENT_TYPE_SPEECH,
|
||||||
) : AudioPlayer {
|
) : AudioPlayer {
|
||||||
private var track: AudioTrack? = null
|
private var track: AudioTrack? = null
|
||||||
@@ -144,9 +157,6 @@ class AudioTrackPlayer(
|
|||||||
runCatching { t.release() }
|
runCatching { t.release() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
val voiceCallUsage: Int get() = AudioManager.STREAM_VOICE_CALL // kept for documentation
|
|
||||||
|
|
||||||
private fun applyMuteVolume(track: AudioTrack) {
|
private fun applyMuteVolume(track: AudioTrack) {
|
||||||
// setVolume is preferred over pause(): it keeps the streaming pipeline
|
// setVolume is preferred over pause(): it keeps the streaming pipeline
|
||||||
// running so unmute is sample-accurate and there's no AudioTrack-restart
|
// running so unmute is sample-accurate and there's no AudioTrack-restart
|
||||||
|
|||||||
Reference in New Issue
Block a user