@parastud/react-native-vban 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,20 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
2
 
3
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
3
4
  <!-- Required (API 34+) to run a mediaProjection foreground service; ignored on older versions. -->
4
5
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
6
+ <!-- Keep-alive service types for background receive (playback) and mic-send. -->
7
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
8
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
5
9
 
6
10
  <application>
7
11
  <service
8
12
  android:name=".SystemAudioCaptureService"
9
13
  android:exported="false"
10
14
  android:foregroundServiceType="mediaProjection" />
15
+ <service
16
+ android:name=".VbanForegroundService"
17
+ android:exported="false"
18
+ android:foregroundServiceType="mediaPlayback|microphone" />
11
19
  </application>
12
20
  </manifest>
@@ -0,0 +1,122 @@
1
+ package expo.modules.systemaudiocapture
2
+
3
+ import android.app.Notification
4
+ import android.app.NotificationChannel
5
+ import android.app.NotificationManager
6
+ import android.app.Service
7
+ import android.content.Context
8
+ import android.content.Intent
9
+ import android.content.pm.ServiceInfo
10
+ import android.os.Build
11
+ import android.os.IBinder
12
+
13
+ /**
14
+ * Keep-alive foreground service for the native receive + mic-send paths.
15
+ *
16
+ * Those paths run their audio loop on an in-process [Thread] (see
17
+ * [VbanReceiverModule] / [VbanSenderModule.startMic]). Without a foreground
18
+ * service Android freezes the process shortly after it's backgrounded, which
19
+ * stalls the audio. This service doesn't own the audio — it only holds an
20
+ * ongoing notification so the process stays unfrozen and keeps network access
21
+ * while receiving and/or mic-sending is active.
22
+ *
23
+ * State lives in static flags set by the modules; call [refresh] after changing
24
+ * one. The service raises the correct foreground-service type(s) and stops
25
+ * itself once nothing is active.
26
+ *
27
+ * (Device-audio send has its own [SystemAudioCaptureService] and does not use
28
+ * this one.)
29
+ */
30
+ class VbanForegroundService : Service() {
31
+ companion object {
32
+ private const val ACTION_UPDATE = "expo.modules.systemaudiocapture.KEEPALIVE_UPDATE"
33
+ private const val CHANNEL_ID = "vban_keepalive"
34
+ private const val NOTIF_ID = 8124
35
+
36
+ /** True while the native receiver is running. */
37
+ @Volatile var receiving = false
38
+
39
+ /** True while native mic-send is running. */
40
+ @Volatile var sending = false
41
+
42
+ /**
43
+ * Reconcile the running service with the current [receiving]/[sending]
44
+ * flags: start/raise the foreground notification if anything is active,
45
+ * or tear it down if nothing is. Safe to call repeatedly.
46
+ */
47
+ fun refresh(context: Context) {
48
+ val intent = Intent(context, VbanForegroundService::class.java).apply {
49
+ action = ACTION_UPDATE
50
+ }
51
+ if (receiving || sending) {
52
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
53
+ context.startForegroundService(intent)
54
+ } else {
55
+ context.startService(intent)
56
+ }
57
+ } else {
58
+ // Plain start (no startForeground obligation) — the service will
59
+ // observe the cleared flags and stop itself.
60
+ context.startService(intent)
61
+ }
62
+ }
63
+ }
64
+
65
+ override fun onBind(intent: Intent?): IBinder? = null
66
+
67
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
68
+ if (!receiving && !sending) {
69
+ stopForegroundCompat()
70
+ stopSelf()
71
+ return START_NOT_STICKY
72
+ }
73
+ startForegroundNotification()
74
+ // If the system kills us under pressure, don't restart with stale state —
75
+ // the audio threads would be gone with the process anyway.
76
+ return START_NOT_STICKY
77
+ }
78
+
79
+ private fun startForegroundNotification() {
80
+ val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
81
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
82
+ nm.createNotificationChannel(
83
+ NotificationChannel(CHANNEL_ID, "VBAN audio", NotificationManager.IMPORTANCE_LOW)
84
+ )
85
+ }
86
+
87
+ val text = when {
88
+ receiving && sending -> "Receiving and sending audio over the network"
89
+ receiving -> "Receiving audio over the network"
90
+ else -> "Sending microphone audio over the network"
91
+ }
92
+ val notification: Notification = Notification.Builder(this, CHANNEL_ID)
93
+ .setContentTitle("VBAN")
94
+ .setContentText(text)
95
+ .setSmallIcon(android.R.drawable.ic_lock_silent_mode_off)
96
+ .setOngoing(true)
97
+ .build()
98
+
99
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
100
+ var type = 0
101
+ if (receiving) type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
102
+ if (sending) type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
103
+ startForeground(NOTIF_ID, notification, type)
104
+ } else {
105
+ startForeground(NOTIF_ID, notification)
106
+ }
107
+ }
108
+
109
+ @Suppress("DEPRECATION")
110
+ private fun stopForegroundCompat() {
111
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
112
+ stopForeground(STOP_FOREGROUND_REMOVE)
113
+ } else {
114
+ stopForeground(true)
115
+ }
116
+ }
117
+
118
+ override fun onDestroy() {
119
+ stopForegroundCompat()
120
+ super.onDestroy()
121
+ }
122
+ }
@@ -1,9 +1,11 @@
1
1
  package expo.modules.systemaudiocapture
2
2
 
3
+ import android.content.Context
3
4
  import android.media.AudioAttributes
4
5
  import android.media.AudioFormat
5
6
  import android.media.AudioTrack
6
7
  import expo.modules.kotlin.Promise
8
+ import expo.modules.kotlin.exception.Exceptions
7
9
  import expo.modules.kotlin.modules.Module
8
10
  import expo.modules.kotlin.modules.ModuleDefinition
9
11
  import java.net.DatagramPacket
@@ -24,6 +26,9 @@ class VbanReceiverModule : Module() {
24
26
  private var thread: Thread? = null
25
27
  private var socket: DatagramSocket? = null
26
28
 
29
+ private val context: Context
30
+ get() = appContext.reactContext ?: throw Exceptions.ReactContextLost()
31
+
27
32
  companion object {
28
33
  private val SR = intArrayOf(
29
34
  6000, 12000, 24000, 48000, 96000, 192000, 384000,
@@ -204,6 +209,10 @@ class VbanReceiverModule : Module() {
204
209
  track?.let { try { it.stop(); it.release() } catch (_: Exception) {} }
205
210
  }
206
211
  }.also { it.start() }
212
+
213
+ // Keep the process alive in the background while receiving.
214
+ VbanForegroundService.receiving = true
215
+ try { VbanForegroundService.refresh(context) } catch (_: Exception) {}
207
216
  }
208
217
 
209
218
  private fun stopReceive() {
@@ -212,5 +221,8 @@ class VbanReceiverModule : Module() {
212
221
  socket = null
213
222
  try { thread?.join(500) } catch (_: Exception) {}
214
223
  thread = null
224
+
225
+ VbanForegroundService.receiving = false
226
+ try { VbanForegroundService.refresh(context) } catch (_: Exception) {}
215
227
  }
216
228
  }
@@ -73,6 +73,10 @@ class VbanSenderModule : Module() {
73
73
  AsyncFunction("stop") { promise: Promise ->
74
74
  engine?.stop()
75
75
  engine = null
76
+ if (VbanForegroundService.sending) {
77
+ VbanForegroundService.sending = false
78
+ try { VbanForegroundService.refresh(context) } catch (_: Exception) {}
79
+ }
76
80
  if (deviceActive) {
77
81
  deviceActive = false
78
82
  SystemAudioCaptureService.onStats = null
@@ -88,6 +92,10 @@ class VbanSenderModule : Module() {
88
92
  OnDestroy {
89
93
  engine?.stop()
90
94
  engine = null
95
+ if (VbanForegroundService.sending) {
96
+ VbanForegroundService.sending = false
97
+ try { VbanForegroundService.refresh(context) } catch (_: Exception) {}
98
+ }
91
99
  }
92
100
  }
93
101
 
@@ -103,6 +111,10 @@ class VbanSenderModule : Module() {
103
111
  onError = { msg -> sendEvent("onError", mapOf("message" to msg)) },
104
112
  )
105
113
  engine!!.start()
114
+
115
+ // Keep the process alive in the background while mic-sending.
116
+ VbanForegroundService.sending = true
117
+ try { VbanForegroundService.refresh(context) } catch (_: Exception) {}
106
118
  }
107
119
 
108
120
  /** Build an AudioRecord for the mic, falling back to mono if stereo isn't supported. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parastud/react-native-vban",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "VBAN (VB-Audio Network) audio-over-IP for React Native / Expo — native UDP send & receive, Android system-audio capture via MediaProjection, and a pure-TypeScript VBAN codec. Interoperates with Voicemeeter and other VBAN endpoints.",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -20,7 +20,8 @@
20
20
  ],
21
21
  "files": [
22
22
  "src",
23
- "android",
23
+ "android/src",
24
+ "android/build.gradle",
24
25
  "expo-module.config.json",
25
26
  "README.md"
26
27
  ],
@@ -30,6 +31,8 @@
30
31
  "react-native": "*"
31
32
  },
32
33
  "expo": {
33
- "platforms": ["android"]
34
+ "platforms": [
35
+ "android"
36
+ ]
34
37
  }
35
38
  }
File without changes
@@ -1,2 +0,0 @@
1
- #Thu Jul 02 16:24:57 IST 2026
2
- gradle.version=8.9
File without changes