Merge remote-tracking branch 'origin/main' into claude/fix-duplicate-mic-buttons-AKgfT
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
|
||||
|
||||
+9
-2
@@ -346,9 +346,16 @@ class ReconnectingNestsSpeakerTest {
|
||||
}
|
||||
assertTrue(first.handles[0].isMuted, "first handle should be muted by user toggle")
|
||||
|
||||
// Wait for refresh to swap to the second session.
|
||||
// Wait for refresh to swap to the second session AND
|
||||
// for the pump to replay mute intent on the new handle.
|
||||
// `startCount > 0` is published from inside
|
||||
// ScriptedSpeaker.startBroadcasting BEFORE the pump
|
||||
// gets to run `if (desiredMuted) handle.setMuted(true)`,
|
||||
// so polling startCount alone races the replay step
|
||||
// under load (observed flake on CI). Wait for the
|
||||
// post-condition the assertion is about instead.
|
||||
withTimeout(5_000L) {
|
||||
while (second.startCount == 0) delay(5)
|
||||
while (second.handles.isEmpty() || !second.handles[0].isMuted) delay(5)
|
||||
}
|
||||
|
||||
// Critical postcondition: the new underlying handle
|
||||
|
||||
Reference in New Issue
Block a user