@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.
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/expo/modules/systemaudiocapture/VbanForegroundService.kt +122 -0
- package/android/src/main/java/expo/modules/systemaudiocapture/VbanReceiverModule.kt +12 -0
- package/android/src/main/java/expo/modules/systemaudiocapture/VbanSenderModule.kt +12 -0
- package/package.json +6 -3
- package/android/.gradle/8.9/checksums/checksums.lock +0 -0
- package/android/.gradle/8.9/dependencies-accessors/gc.properties +0 -0
- package/android/.gradle/8.9/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.9/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.9/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
- package/android/.gradle/vcs-1/gc.properties +0 -0
|
@@ -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.
|
|
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": [
|
|
34
|
+
"platforms": [
|
|
35
|
+
"android"
|
|
36
|
+
]
|
|
34
37
|
}
|
|
35
38
|
}
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|