fix(nests): play audio-room output through STREAM_MUSIC, not STREAM_VOICE_CALL
`AudioTrackPlayer` was constructing its `AudioTrack` with `USAGE_VOICE_COMMUNICATION` + `CONTENT_TYPE_SPEECH` so the room would behave like a phone call (volume rocker controls call volume, ducks notifications). The `AudioTrack` then renders on `STREAM_VOICE_CALL`, which Android only services audibly while the device is in `MODE_IN_COMMUNICATION`. Nests never drives `AudioManager.mode` (only NIP-100's `CallAudioManager` does), so on most devices the playback either dropped to the earpiece at near-zero volume or produced no audio at all — making rooms appear silent on both phones. Fix: route through `USAGE_MEDIA` + `CONTENT_TYPE_SPEECH` (= `STREAM_MUSIC`) so audio-room playback comes out of the loudspeaker by default and the volume rocker controls it any time. Same approach Twitter/X Spaces and Clubhouse take for hands-free audio rooms. Echo cancellation still works on the capture side via `MediaRecorder.AudioSource.VOICE_COMMUNICATION` regardless of the playback usage. Also update `NestForegroundService.requestAudioFocus` to request focus under the matching `USAGE_MEDIA` attributes so the focus claim and the playback attributes line up.
This commit is contained in:
+6
-1
@@ -74,6 +74,11 @@ class NestForegroundService : Service() {
|
||||
* call lowers the room volume cleanly instead of mixing two
|
||||
* voices on top of each other. Acquired once per service
|
||||
* 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() {
|
||||
if (audioFocusRequest != null) return
|
||||
@@ -81,7 +86,7 @@ class NestForegroundService : Service() {
|
||||
val attrs =
|
||||
android.media.AudioAttributes
|
||||
.Builder()
|
||||
.setUsage(android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
|
||||
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.build()
|
||||
val request =
|
||||
|
||||
+18
-8
@@ -21,23 +21,36 @@
|
||||
package com.vitorpamplona.nestsclient.audio
|
||||
|
||||
import android.media.AudioAttributes
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioTrack
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import android.media.AudioFormat as AndroidAudioFormat
|
||||
|
||||
/**
|
||||
* [AudioPlayer] backed by Android's [AudioTrack] in `MODE_STREAM`. Targets the
|
||||
* voice-call usage stream so the OS treats audio-room playback like a phone
|
||||
* call (volume rocker controls call volume, ducks notifications, etc.).
|
||||
* [AudioPlayer] backed by Android's [AudioTrack] in `MODE_STREAM`.
|
||||
*
|
||||
* 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
|
||||
* dropouts, which roughly matches the jitter the WebTransport datagram path
|
||||
* introduces over typical mobile networks.
|
||||
*/
|
||||
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,
|
||||
) : AudioPlayer {
|
||||
private var track: AudioTrack? = null
|
||||
@@ -144,9 +157,6 @@ class AudioTrackPlayer(
|
||||
runCatching { t.release() }
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
val voiceCallUsage: Int get() = AudioManager.STREAM_VOICE_CALL // kept for documentation
|
||||
|
||||
private fun applyMuteVolume(track: AudioTrack) {
|
||||
// setVolume is preferred over pause(): it keeps the streaming pipeline
|
||||
// running so unmute is sample-accurate and there's no AudioTrack-restart
|
||||
|
||||
Reference in New Issue
Block a user