Skip to content

Commit 1686ded

Browse files
Fix VolumeProvider: add logging, metadata, and system volume observer fallback
- Added MediaMetadata to MediaSession so system treats it as real session - Added MediaSession.Callback for media button events - Added logging to VolumeProvider onSetVolumeTo/onAdjustVolume callbacks - Added ContentObserver fallback on Settings.System.CONTENT_URI: if Android 12+ bug prevents VolumeProvider callbacks from firing, we detect STREAM_MUSIC volume changes and map them to app volume - Volume observer maps system vol (0..maxVol) to app vol (0..100) and emits volume_change events for JS slider sync Co-Authored-By: Raymond <20248577+javaarchive@users.noreply.github.com>
1 parent bc4595f commit 1686ded

1 file changed

Lines changed: 86 additions & 12 deletions

File tree

app/android/app/src/main/kotlin/com/audiostream/AudioStreamService.kt

Lines changed: 86 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,24 @@ import android.app.Service
77
import android.content.Context
88
import android.content.Intent
99
import android.content.pm.ServiceInfo
10+
import android.database.ContentObserver
1011
import android.media.AudioAttributes
1112
import android.media.AudioFormat
1213
import android.media.AudioManager
1314
import android.media.AudioPlaybackCaptureConfiguration
1415
import android.media.AudioRecord
16+
import android.media.MediaMetadata
1517
import android.media.VolumeProvider
1618
import android.media.projection.MediaProjection
1719
import android.media.projection.MediaProjectionManager
1820
import android.media.session.MediaSession
1921
import android.media.session.PlaybackState
2022
import android.net.wifi.WifiManager
23+
import android.os.Handler
2124
import android.os.IBinder
25+
import android.os.Looper
2226
import android.os.PowerManager
27+
import android.provider.Settings
2328
import java.net.DatagramPacket
2429
import java.net.DatagramSocket
2530
import java.net.InetAddress
@@ -45,13 +50,11 @@ class AudioStreamService : Service() {
4550
const val EXTRA_LOG_SCALING = "logScaling"
4651
const val EXTRA_MUTE_LOCAL = "muteLocal"
4752

48-
// Airwire protocol signature bytes
4953
const val SIGNATURE_BYTE_1: Byte = 13
5054
const val SIGNATURE_BYTE_2: Byte = 37
5155
const val SIGNATURE_SIZE = 2
5256
const val PACKET_ID_SIZE = 8
5357

54-
// Static instance so AudioCaptureModule can call updateVolume
5558
@Volatile
5659
var instance: AudioStreamService? = null
5760
private set
@@ -69,26 +72,30 @@ class AudioStreamService : Service() {
6972
@Volatile var currentGain: Float = 1.0f
7073
@Volatile var currentLogScaling: Boolean = false
7174

72-
// Locks
7375
private var wakeLock: PowerManager.WakeLock? = null
7476
private var wifiLock: WifiManager.WifiLock? = null
7577

76-
// Mute
7778
private var audioManager: AudioManager? = null
7879
private var savedVolume: Int = -1
7980
private var didMute = false
8081

81-
// MediaSession
8282
private var mediaSession: MediaSession? = null
8383
private var volumeProvider: VolumeProvider? = null
8484

85+
// Fallback: watch system volume changes in case VolumeProvider callbacks
86+
// are not fired (known Android 12+ issue)
87+
private var volumeObserver: ContentObserver? = null
88+
private var lastObservedSystemVol: Int = -1
89+
private var systemMaxVol: Int = 15
90+
8591
override fun onBind(intent: Intent?): IBinder? = null
8692

8793
override fun onCreate() {
8894
super.onCreate()
8995
instance = this
9096
createNotificationChannel()
9197
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
98+
systemMaxVol = audioManager?.getStreamMaxVolume(AudioManager.STREAM_MUSIC) ?: 15
9299
log("info", "[Service] onCreate")
93100
}
94101

@@ -128,7 +135,6 @@ class AudioStreamService : Service() {
128135
return START_NOT_STICKY
129136
}
130137

131-
// Set up MediaSession before startForeground so the notification can reference it
132138
setupMediaSession(volume, logScaling)
133139

134140
try {
@@ -165,6 +171,9 @@ class AudioStreamService : Service() {
165171
muteLocalAudio()
166172
}
167173

174+
// Start fallback volume observer
175+
startVolumeObserver(volume)
176+
168177
startStreaming(host, port, sampleRate, channels, frameSize, packetPacing, repeatPackets, verboseLogging, volume, logScaling)
169178

170179
return START_NOT_STICKY
@@ -224,20 +233,47 @@ class AudioStreamService : Service() {
224233
private fun setupMediaSession(initialVolume: Int, logScaling: Boolean) {
225234
try {
226235
mediaSession = MediaSession(this, "AirwireStreamer").apply {
236+
// Set metadata so the system treats this as a real media session
237+
setMetadata(
238+
MediaMetadata.Builder()
239+
.putString(MediaMetadata.METADATA_KEY_TITLE, "Airwire Streamer")
240+
.putString(MediaMetadata.METADATA_KEY_ARTIST, "Streaming audio")
241+
.putLong(MediaMetadata.METADATA_KEY_DURATION, -1L)
242+
.build()
243+
)
244+
227245
setPlaybackState(
228246
PlaybackState.Builder()
229247
.setState(PlaybackState.STATE_PLAYING, 0, 1.0f)
230-
.setActions(PlaybackState.ACTION_PLAY or PlaybackState.ACTION_STOP)
248+
.setActions(
249+
PlaybackState.ACTION_PLAY or
250+
PlaybackState.ACTION_STOP or
251+
PlaybackState.ACTION_PAUSE
252+
)
231253
.build()
232254
)
233255

256+
// Set a callback for media button events
257+
setCallback(object : MediaSession.Callback() {
258+
override fun onPlay() {
259+
log("info", "[MediaSession] onPlay callback")
260+
}
261+
override fun onStop() {
262+
log("info", "[MediaSession] onStop callback")
263+
}
264+
override fun onPause() {
265+
log("info", "[MediaSession] onPause callback")
266+
}
267+
}, Handler(Looper.getMainLooper()))
268+
234269
val vp = object : VolumeProvider(
235270
VOLUME_CONTROL_ABSOLUTE,
236271
100,
237272
initialVolume.coerceIn(0, 100)
238273
) {
239274
override fun onSetVolumeTo(volume: Int) {
240275
val clamped = volume.coerceIn(0, 100)
276+
log("info", "[VolumeProvider] onSetVolumeTo: $clamped")
241277
setCurrentVolume(clamped)
242278
applyVolume(clamped, currentLogScaling)
243279
AudioStreamEventEmitter.emit("volume_change", clamped.toString())
@@ -252,6 +288,7 @@ class AudioStreamService : Service() {
252288
AudioManager.ADJUST_UNMUTE -> current
253289
else -> current
254290
}
291+
log("info", "[VolumeProvider] onAdjustVolume: direction=$direction, $current->$newVol")
255292
setCurrentVolume(newVol)
256293
applyVolume(newVol, currentLogScaling)
257294
AudioStreamEventEmitter.emit("volume_change", newVol.toString())
@@ -267,7 +304,42 @@ class AudioStreamService : Service() {
267304
}
268305
}
269306

270-
/** Called from AudioCaptureModule.updateVolume or from MediaSession VolumeProvider */
307+
/**
308+
* Fallback: If VolumeProvider callbacks don't fire (Android 12+ bug),
309+
* observe system STREAM_MUSIC volume changes and map them to our gain.
310+
* When the user drags the system "cast" slider, on broken Android versions
311+
* it changes STREAM_MUSIC directly instead of calling VolumeProvider.
312+
*/
313+
private fun startVolumeObserver(initialAppVolume: Int) {
314+
val am = audioManager ?: return
315+
lastObservedSystemVol = am.getStreamVolume(AudioManager.STREAM_MUSIC)
316+
317+
val handler = Handler(Looper.getMainLooper())
318+
volumeObserver = object : ContentObserver(handler) {
319+
override fun onChange(selfChange: Boolean) {
320+
val newSysVol = am.getStreamVolume(AudioManager.STREAM_MUSIC)
321+
if (newSysVol != lastObservedSystemVol) {
322+
val delta = newSysVol - lastObservedSystemVol
323+
lastObservedSystemVol = newSysVol
324+
325+
// Map system volume (0..systemMaxVol) to app volume (0..100)
326+
val appVol = ((newSysVol.toFloat() / systemMaxVol.toFloat()) * 100).toInt().coerceIn(0, 100)
327+
log("info", "[VolumeObserver] System vol changed: $newSysVol/$systemMaxVol -> appVol=$appVol (delta=$delta)")
328+
applyVolume(appVol, currentLogScaling)
329+
syncMediaSessionVolume(appVol)
330+
AudioStreamEventEmitter.emit("volume_change", appVol.toString())
331+
}
332+
}
333+
}
334+
335+
contentResolver.registerContentObserver(
336+
Settings.System.CONTENT_URI,
337+
true,
338+
volumeObserver!!
339+
)
340+
log("info", "[Service] Volume observer registered (sysVol=$lastObservedSystemVol/$systemMaxVol)")
341+
}
342+
271343
fun applyVolume(volume: Int, logScaling: Boolean) {
272344
val clamped = volume.coerceIn(0, 100)
273345
currentLogScaling = logScaling
@@ -279,7 +351,6 @@ class AudioStreamService : Service() {
279351
}
280352
}
281353

282-
/** Called from AudioCaptureModule to sync MediaSession's volume display */
283354
fun syncMediaSessionVolume(volume: Int) {
284355
volumeProvider?.setCurrentVolume(volume.coerceIn(0, 100))
285356
}
@@ -303,7 +374,6 @@ class AudioStreamService : Service() {
303374
.setSmallIcon(android.R.drawable.ic_media_play)
304375
.setOngoing(true)
305376

306-
// Attach MediaSession token so system volume UI routes here
307377
val session = mediaSession
308378
if (session != null) {
309379
builder.setStyle(
@@ -399,7 +469,6 @@ class AudioStreamService : Service() {
399469
val recState = audioRecord?.recordingState
400470
log("info", "[Stream] AudioRecord recordingState=$recState (3=recording)")
401471

402-
// Set initial gain
403472
applyVolume(volume, logScaling)
404473
log("info", "[Stream] Initial volume=$volume%, logScaling=$logScaling, gain=$currentGain")
405474

@@ -429,7 +498,6 @@ class AudioStreamService : Service() {
429498
}
430499
if (shortsRead == 0) continue
431500

432-
// Read the volatile gain (may be updated by slider or MediaSession at any time)
433501
val gain = currentGain
434502
if (gain < 1.0f) {
435503
for (s in 0 until shortsRead) {
@@ -514,6 +582,12 @@ class AudioStreamService : Service() {
514582
mediaSession = null
515583
volumeProvider = null
516584

585+
// Unregister volume observer
586+
try {
587+
volumeObserver?.let { contentResolver.unregisterContentObserver(it) }
588+
} catch (_: Exception) {}
589+
volumeObserver = null
590+
517591
restoreLocalAudio()
518592

519593
try {

0 commit comments

Comments
 (0)