@voipsdk/react-native-voipcloud 0.2.1 → 1.0.1
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/CHANGELOG.md +44 -0
- package/README.md +182 -117
- package/android/build.gradle +7 -7
- package/android/src/main/java/com/voipcloud/sdk/rn/VoipCloudModule.java +307 -208
- package/android/src/main/java/com/voipcloud/sdk/rn/internal/BridgeGuard.java +74 -0
- package/android/src/main/java/com/voipcloud/sdk/rn/internal/Events.java +7 -2
- package/android/src/main/java/com/voipcloud/sdk/rn/internal/ManagedAccount.java +49 -5
- package/android/src/main/java/com/voipcloud/sdk/rn/internal/ManagedCall.java +58 -2
- package/android/src/main/java/com/voipcloud/sdk/rn/internal/ManagedCallRegistry.java +4 -0
- package/android/src/main/libs/voipcloud-release.aar +0 -0
- package/ios/VoipCloud.mm +8 -8
- package/lib/commonjs/VoipCloud.js +33 -48
- package/lib/commonjs/VoipCloud.js.map +1 -1
- package/lib/commonjs/events.js +13 -7
- package/lib/commonjs/events.js.map +1 -1
- package/lib/commonjs/index.js +2 -3
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/nativeModule.js +18 -0
- package/lib/commonjs/nativeModule.js.map +1 -0
- package/lib/module/VoipCloud.js +34 -48
- package/lib/module/VoipCloud.js.map +1 -1
- package/lib/module/events.js +9 -8
- package/lib/module/events.js.map +1 -1
- package/lib/module/index.js +1 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/nativeModule.js +13 -0
- package/lib/module/nativeModule.js.map +1 -0
- package/lib/typescript/VoipCloud.d.ts +7 -31
- package/lib/typescript/events.d.ts +6 -2
- package/lib/typescript/index.d.ts +2 -3
- package/lib/typescript/nativeModule.d.ts +7 -0
- package/lib/typescript/types.d.ts +5 -17
- package/package.json +7 -5
- package/react-native-voipcloud.podspec +7 -5
- package/src/VoipCloud.ts +42 -49
- package/src/events.ts +9 -8
- package/src/index.ts +1 -3
- package/src/nativeModule.ts +17 -0
- package/src/types.ts +5 -18
- package/lib/commonjs/License.js +0 -51
- package/lib/commonjs/License.js.map +0 -1
- package/lib/module/License.js +0 -45
- package/lib/module/License.js.map +0 -1
- package/lib/typescript/License.d.ts +0 -31
- package/src/License.ts +0 -52
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
package com.voipcloud.sdk.rn;
|
|
2
2
|
|
|
3
|
-
import android.provider.Settings;
|
|
4
3
|
import androidx.annotation.NonNull;
|
|
5
4
|
|
|
6
5
|
import com.facebook.react.bridge.Arguments;
|
|
@@ -17,345 +16,434 @@ import com.voipcloud.sdk.CallOpParam;
|
|
|
17
16
|
import com.voipcloud.sdk.CallSendDtmfParam;
|
|
18
17
|
import com.voipcloud.sdk.Endpoint;
|
|
19
18
|
import com.voipcloud.sdk.EpConfig;
|
|
20
|
-
import com.voipcloud.sdk.License;
|
|
21
|
-
import com.voipcloud.sdk.LicenseRefresher;
|
|
22
19
|
import com.voipcloud.sdk.TransportConfig;
|
|
23
20
|
import com.voipcloud.sdk.pjsip_status_code;
|
|
24
21
|
import com.voipcloud.sdk.pjsip_transport_type_e;
|
|
22
|
+
import com.voipcloud.sdk.pjsua_call_flag;
|
|
25
23
|
import com.voipcloud.sdk.pjsua_destroy_flag;
|
|
24
|
+
import com.voipcloud.sdk.AudDevManager;
|
|
26
25
|
|
|
26
|
+
import com.voipcloud.sdk.rn.internal.BridgeGuard;
|
|
27
27
|
import com.voipcloud.sdk.rn.internal.Events;
|
|
28
28
|
import com.voipcloud.sdk.rn.internal.ManagedAccount;
|
|
29
29
|
import com.voipcloud.sdk.rn.internal.ManagedCall;
|
|
30
30
|
import com.voipcloud.sdk.rn.internal.ManagedCallRegistry;
|
|
31
31
|
|
|
32
32
|
import java.util.concurrent.ConcurrentHashMap;
|
|
33
|
-
import java.util.concurrent.atomic.AtomicInteger;
|
|
34
33
|
|
|
35
34
|
/**
|
|
36
|
-
* React Native bridge for the VoipCloud SDK
|
|
37
|
-
*
|
|
35
|
+
* React Native bridge for the VoipCloud public SDK (PJSIP, no license gate).
|
|
36
|
+
* All failures are returned as rejected Promises with VC_* codes — never crash
|
|
37
|
+
* the host app for API misuse or recoverable PJSIP errors.
|
|
38
38
|
*/
|
|
39
39
|
public class VoipCloudModule extends ReactContextBaseJavaModule {
|
|
40
40
|
|
|
41
|
-
static {
|
|
42
|
-
// Load the prebuilt PJSIP + license-layer library. Names must
|
|
43
|
-
// match jniLibs path inside the AAR: jni/<abi>/libvoipcloud.so.
|
|
44
|
-
// libc++_shared is co-located and pulled in implicitly by the
|
|
45
|
-
// loader as a NEEDED dependency.
|
|
46
|
-
System.loadLibrary("voipcloud");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
41
|
private final ReactApplicationContext ctx;
|
|
50
42
|
private final ManagedCallRegistry calls = new ManagedCallRegistry();
|
|
51
43
|
private final ConcurrentHashMap<Integer, ManagedAccount> accounts = new ConcurrentHashMap<>();
|
|
52
|
-
private final AtomicInteger nextAccKey = new AtomicInteger(1);
|
|
53
44
|
|
|
54
45
|
private Endpoint endpoint;
|
|
55
46
|
private boolean inited = false;
|
|
56
|
-
/** Last token accepted by License.set; reused by the refresher. */
|
|
57
|
-
private volatile String licenseToken = null;
|
|
58
|
-
/** Refresh URL currently driving LicenseRefresher. Used to detect
|
|
59
|
-
* when a token rotation also rotates the refresh endpoint. */
|
|
60
|
-
private volatile String activeRefreshUrl = null;
|
|
61
47
|
|
|
62
48
|
public VoipCloudModule(ReactApplicationContext ctx) {
|
|
63
49
|
super(ctx);
|
|
64
50
|
this.ctx = ctx;
|
|
65
|
-
|
|
66
|
-
// Tell native the consumer app's package up front so subsequent
|
|
67
|
-
// License.set() can enforce the bundle_id binding embedded in
|
|
68
|
-
// the token. Also anchor the clock-skew detector.
|
|
69
|
-
License.setBundle(ctx.getPackageName());
|
|
70
|
-
License.setStateDir(ctx.getFilesDir().getAbsolutePath());
|
|
71
51
|
}
|
|
72
52
|
|
|
53
|
+
/** Must match NATIVE_MODULE_NAME in src/nativeModule.ts (@voipsdk/react-native-voipcloud). */
|
|
73
54
|
@NonNull
|
|
74
55
|
@Override
|
|
75
|
-
public String getName() { return "
|
|
76
|
-
|
|
77
|
-
// ============== License ============================================
|
|
78
|
-
|
|
79
|
-
@ReactMethod
|
|
80
|
-
public void setLicense(String token, Promise promise) {
|
|
81
|
-
try {
|
|
82
|
-
License.set(token);
|
|
83
|
-
licenseToken = token;
|
|
84
|
-
bootstrapRefresher(License.refreshUrl());
|
|
85
|
-
promise.resolve(null);
|
|
86
|
-
} catch (Throwable t) {
|
|
87
|
-
// Stop any refresher tied to the previous (now-rejected) token.
|
|
88
|
-
stopRefresherInternal();
|
|
89
|
-
promise.reject(mapLicenseError(t), t.getMessage(), t);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
56
|
+
public String getName() { return "VoipsdkReactNativeVoipcloud"; }
|
|
92
57
|
|
|
93
58
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
59
|
+
* PJSIP aborts (SIGABRT, "Calling pjlib from unknown/external thread")
|
|
60
|
+
* when pjlib is entered from a thread never passed to
|
|
61
|
+
* pj_thread_register(). SWIG finalizers run on the runtime's
|
|
62
|
+
* FinalizerDaemon thread and call native destructors that touch pjlib
|
|
63
|
+
* (CallOpParam, EpConfig, CallInfo, ... dropped to the GC), so that
|
|
64
|
+
* thread must be registered too. There is no API to run code on
|
|
65
|
+
* FinalizerDaemon directly — enqueue a sacrificial object whose
|
|
66
|
+
* finalizer registers it, and force a finalization pass now, while no
|
|
67
|
+
* other SDK garbage is pending yet.
|
|
97
68
|
*/
|
|
98
|
-
private void
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
private void stopRefresherInternal() {
|
|
116
|
-
try { LicenseRefresher.stop(); } catch (Throwable ignored) {}
|
|
117
|
-
activeRefreshUrl = null;
|
|
69
|
+
private static void registerFinalizerDaemonThread(final Endpoint ep) {
|
|
70
|
+
new Object() {
|
|
71
|
+
@Override
|
|
72
|
+
protected void finalize() throws Throwable {
|
|
73
|
+
try {
|
|
74
|
+
if (!ep.libIsThreadRegistered()) {
|
|
75
|
+
ep.libRegisterThread("FinalizerDaemon");
|
|
76
|
+
}
|
|
77
|
+
} catch (Throwable ignored) {
|
|
78
|
+
// Endpoint already destroyed or registration raced — the
|
|
79
|
+
// next init() enqueues a fresh registration.
|
|
80
|
+
}
|
|
81
|
+
super.finalize();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
System.gc();
|
|
85
|
+
System.runFinalization();
|
|
118
86
|
}
|
|
119
87
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
* Android 8+ the ANDROID_ID is scoped to package+signing key, so
|
|
123
|
-
* it survives reinstalls of the same APK but rotates if the app is
|
|
124
|
-
* sideloaded by another publisher — exactly the semantics a
|
|
125
|
-
* license server wants.
|
|
126
|
-
*/
|
|
127
|
-
private String autoDeviceId() {
|
|
88
|
+
private void destroyEndpointQuietly() {
|
|
89
|
+
if (endpoint == null) return;
|
|
128
90
|
try {
|
|
129
|
-
|
|
130
|
-
ctx.getContentResolver(), Settings.Secure.ANDROID_ID);
|
|
131
|
-
if (id != null && !id.isEmpty()) return id;
|
|
91
|
+
endpoint.libDestroy(pjsua_destroy_flag.PJSUA_DESTROY_NO_NETWORK);
|
|
132
92
|
} catch (Throwable ignored) {}
|
|
133
|
-
// Fall back to a manifest-derived label.
|
|
134
|
-
return ctx.getPackageName() + ":" + android.os.Build.FINGERPRINT;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Translate a PJSUA2 Error into the granular VC_* code surfaced to JS. */
|
|
138
|
-
private static String mapLicenseError(Throwable t) {
|
|
139
|
-
String msg = t.getMessage();
|
|
140
|
-
if (msg == null) return "VC_LICENSE";
|
|
141
|
-
if (msg.contains("license not set")) return "VC_NO_LICENSE";
|
|
142
|
-
if (msg.contains("malformed")) return "VC_BAD_FORMAT";
|
|
143
|
-
if (msg.contains("signature invalid")) return "VC_BAD_SIGNATURE";
|
|
144
|
-
if (msg.contains("license expired")) return "VC_EXPIRED";
|
|
145
|
-
if (msg.contains("not active")) return "VC_NOT_ACTIVE";
|
|
146
|
-
if (msg.contains("revoked")) return "VC_REVOKED";
|
|
147
|
-
if (msg.contains("bundle id mismatch")) return "VC_BUNDLE_MISMATCH";
|
|
148
|
-
if (msg.contains("concurrent calls")) return "VC_CCU_EXCEEDED";
|
|
149
|
-
if (msg.contains("payload too large")) return "VC_PAYLOAD_TOO_BIG";
|
|
150
|
-
if (msg.contains("clock rolled back")) return "VC_CLOCK_SKEW";
|
|
151
|
-
return "VC_LICENSE";
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
@ReactMethod
|
|
155
|
-
public void getLicenseStatus(Promise promise) {
|
|
156
93
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
m.putInt ("ccu", License.maxCcu());
|
|
162
|
-
m.putInt ("currentCalls",License.currentCalls());
|
|
163
|
-
m.putDouble ("expiry", License.expiryUnix());
|
|
164
|
-
m.putDouble ("serverTime", License.serverTime());
|
|
165
|
-
promise.resolve(m);
|
|
166
|
-
} catch (Throwable t) {
|
|
167
|
-
promise.reject("VC_STATUS", t.getMessage(), t);
|
|
168
|
-
}
|
|
94
|
+
endpoint.delete();
|
|
95
|
+
} catch (Throwable ignored) {}
|
|
96
|
+
endpoint = null;
|
|
97
|
+
inited = false;
|
|
169
98
|
}
|
|
170
99
|
|
|
171
|
-
/**
|
|
172
|
-
* The refresher bootstraps itself from token.ref during
|
|
173
|
-
* setLicense(); the consumer never starts it manually. We expose
|
|
174
|
-
* stop() only so apps can pause refresh during user logout / SDK
|
|
175
|
-
* shutdown.
|
|
176
|
-
*/
|
|
177
100
|
@ReactMethod
|
|
178
|
-
public void
|
|
179
|
-
|
|
180
|
-
promise.resolve(null);
|
|
181
|
-
}
|
|
101
|
+
public void init(ReadableMap config, Promise promise) {
|
|
102
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
103
|
+
if (inited) { promise.resolve(null); return; }
|
|
182
104
|
|
|
183
|
-
|
|
105
|
+
if (config == null) config = Arguments.createMap();
|
|
184
106
|
|
|
185
|
-
|
|
186
|
-
public void init(ReadableMap config, Promise promise) {
|
|
107
|
+
Endpoint ep = null;
|
|
187
108
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
109
|
+
ep = new Endpoint();
|
|
110
|
+
ep.libCreate();
|
|
111
|
+
registerFinalizerDaemonThread(ep);
|
|
191
112
|
|
|
192
113
|
EpConfig epCfg = new EpConfig();
|
|
193
|
-
if (config.hasKey("userAgent"))
|
|
114
|
+
if (config.hasKey("userAgent") && !config.isNull("userAgent"))
|
|
194
115
|
epCfg.getUaConfig().setUserAgent(config.getString("userAgent"));
|
|
195
|
-
if (config.hasKey("stunServer")) {
|
|
116
|
+
if (config.hasKey("stunServer") && !config.isNull("stunServer")) {
|
|
196
117
|
com.voipcloud.sdk.StringVector v = new com.voipcloud.sdk.StringVector();
|
|
197
118
|
v.add(config.getString("stunServer"));
|
|
198
119
|
epCfg.getUaConfig().setStunServer(v);
|
|
199
120
|
}
|
|
200
|
-
if (config.hasKey("logLevel"))
|
|
121
|
+
if (config.hasKey("logLevel") && !config.isNull("logLevel"))
|
|
201
122
|
epCfg.getLogConfig().setLevel(config.getInt("logLevel"));
|
|
202
123
|
|
|
203
|
-
|
|
124
|
+
// Disable PJSIP's software echo canceller: it is CPU-heavy on
|
|
125
|
+
// mobile and a common cause of crackling/choppy audio. Android
|
|
126
|
+
// applies its hardware AEC to VOICE_COMMUNICATION streams.
|
|
127
|
+
try {
|
|
128
|
+
epCfg.getMedConfig().setEcTailLen(0);
|
|
129
|
+
} catch (Throwable ignored) {}
|
|
130
|
+
|
|
131
|
+
// Audio quality: run the sound device at the Android native rate
|
|
132
|
+
// (48 kHz) so the HAL does not resample a 16 kHz stream a second
|
|
133
|
+
// time (double resampling on the JNI audio backend is the main
|
|
134
|
+
// source of crackling/harsh speaker audio). The conference
|
|
135
|
+
// bridge stays at 16 kHz; PJSIP resamples once with the
|
|
136
|
+
// high-quality (large filter) resampler. VAD is disabled so
|
|
137
|
+
// silence suppression cannot chop word edges.
|
|
138
|
+
try {
|
|
139
|
+
epCfg.getMedConfig().setSndClockRate(48000);
|
|
140
|
+
epCfg.getMedConfig().setQuality(10);
|
|
141
|
+
epCfg.getMedConfig().setNoVad(true);
|
|
142
|
+
} catch (Throwable ignored) {}
|
|
143
|
+
|
|
144
|
+
ep.libInit(epCfg);
|
|
204
145
|
|
|
205
|
-
// Default transports — TLS handled per-account via registrar URI.
|
|
206
146
|
TransportConfig udp = new TransportConfig();
|
|
207
|
-
|
|
147
|
+
ep.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_UDP, udp);
|
|
208
148
|
TransportConfig tcp = new TransportConfig();
|
|
209
|
-
|
|
149
|
+
ep.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_TCP, tcp);
|
|
210
150
|
|
|
211
|
-
|
|
151
|
+
ep.libStart();
|
|
152
|
+
endpoint = ep;
|
|
212
153
|
inited = true;
|
|
213
154
|
promise.resolve(null);
|
|
214
155
|
} catch (Throwable t) {
|
|
215
|
-
|
|
156
|
+
if (ep != null) {
|
|
157
|
+
try { ep.libDestroy(pjsua_destroy_flag.PJSUA_DESTROY_NO_NETWORK); } catch (Throwable ignored) {}
|
|
158
|
+
try { ep.delete(); } catch (Throwable ignored) {}
|
|
159
|
+
}
|
|
160
|
+
endpoint = null;
|
|
161
|
+
inited = false;
|
|
162
|
+
BridgeGuard.reject(promise, "VC_INIT", "Failed to start VoipCloud endpoint", t);
|
|
216
163
|
}
|
|
217
164
|
}
|
|
218
165
|
|
|
219
166
|
@ReactMethod
|
|
220
167
|
public void shutdown(Promise promise) {
|
|
168
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
221
169
|
try {
|
|
222
170
|
calls.clear();
|
|
223
|
-
for (ManagedAccount a : accounts.values())
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
endpoint.libDestroy(pjsua_destroy_flag.PJSUA_DESTROY_NO_NETWORK);
|
|
227
|
-
endpoint.delete();
|
|
228
|
-
endpoint = null;
|
|
171
|
+
for (ManagedAccount a : accounts.values()) {
|
|
172
|
+
try { a.setRegistration(false); } catch (Throwable ignored) {}
|
|
173
|
+
try { a.delete(); } catch (Throwable ignored) {}
|
|
229
174
|
}
|
|
230
|
-
|
|
175
|
+
accounts.clear();
|
|
176
|
+
destroyEndpointQuietly();
|
|
231
177
|
promise.resolve(null);
|
|
232
178
|
} catch (Throwable t) {
|
|
233
|
-
|
|
179
|
+
BridgeGuard.reject(promise, "VC_SHUTDOWN", "Failed to shut down VoipCloud", t);
|
|
234
180
|
}
|
|
235
181
|
}
|
|
236
182
|
|
|
237
|
-
// ============== Account ============================================
|
|
238
|
-
|
|
239
183
|
@ReactMethod
|
|
240
184
|
public void register(ReadableMap cfg, Promise promise) {
|
|
185
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
186
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
187
|
+
|
|
188
|
+
String uri = BridgeGuard.requireString(cfg, "uri", promise);
|
|
189
|
+
if (uri == null) return;
|
|
190
|
+
String registrar = BridgeGuard.requireString(cfg, "registrar", promise);
|
|
191
|
+
if (registrar == null) return;
|
|
192
|
+
String username = BridgeGuard.requireString(cfg, "username", promise);
|
|
193
|
+
if (username == null) return;
|
|
194
|
+
String password = BridgeGuard.requireString(cfg, "password", promise);
|
|
195
|
+
if (password == null) return;
|
|
196
|
+
|
|
241
197
|
try {
|
|
242
198
|
AccountConfig acfg = new AccountConfig();
|
|
243
|
-
acfg.setIdUri(
|
|
244
|
-
acfg.getRegConfig().setRegistrarUri(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
199
|
+
acfg.setIdUri(uri);
|
|
200
|
+
acfg.getRegConfig().setRegistrarUri(registrar);
|
|
201
|
+
|
|
202
|
+
// NAT survival: without keep-alives and contact rewrite the
|
|
203
|
+
// registration "succeeds" but the NAT mapping dies within a
|
|
204
|
+
// minute and inbound INVITEs never reach the device.
|
|
205
|
+
acfg.getNatConfig().setContactRewriteUse(1);
|
|
206
|
+
acfg.getNatConfig().setViaRewriteUse(1);
|
|
207
|
+
acfg.getNatConfig().setUdpKaIntervalSec(20);
|
|
208
|
+
// Short re-REGISTER interval keeps the binding fresh behind NAT.
|
|
209
|
+
acfg.getRegConfig().setTimeoutSec(120);
|
|
210
|
+
acfg.getRegConfig().setFirstRetryIntervalSec(5);
|
|
211
|
+
acfg.getRegConfig().setRetryIntervalSec(30);
|
|
212
|
+
|
|
213
|
+
AuthCredInfo cred = new AuthCredInfo("digest", "*", username, 0, password);
|
|
250
214
|
acfg.getSipConfig().getAuthCreds().add(cred);
|
|
251
215
|
|
|
252
|
-
ManagedAccount acc = new ManagedAccount(ctx, calls);
|
|
216
|
+
ManagedAccount acc = new ManagedAccount(ctx, calls, endpoint);
|
|
253
217
|
acc.create(acfg);
|
|
254
218
|
int id = acc.accountId();
|
|
219
|
+
if (id < 0) {
|
|
220
|
+
acc.delete();
|
|
221
|
+
promise.reject("VC_REGISTER", "Account created but returned invalid id");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
255
224
|
accounts.put(id, acc);
|
|
256
225
|
promise.resolve(id);
|
|
257
226
|
} catch (Throwable t) {
|
|
258
|
-
|
|
227
|
+
BridgeGuard.reject(promise, "VC_REGISTER", "Failed to register SIP account", t);
|
|
259
228
|
}
|
|
260
229
|
}
|
|
261
230
|
|
|
262
231
|
@ReactMethod
|
|
263
232
|
public void unregister(int accountId, Promise promise) {
|
|
233
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
264
234
|
try {
|
|
265
235
|
ManagedAccount acc = accounts.remove(accountId);
|
|
266
236
|
if (acc != null) {
|
|
267
|
-
acc.setRegistration(false);
|
|
268
|
-
acc.delete();
|
|
237
|
+
try { acc.setRegistration(false); } catch (Throwable ignored) {}
|
|
238
|
+
try { acc.delete(); } catch (Throwable ignored) {}
|
|
269
239
|
}
|
|
270
240
|
promise.resolve(null);
|
|
271
241
|
} catch (Throwable t) {
|
|
272
|
-
|
|
242
|
+
BridgeGuard.reject(promise, "VC_UNREGISTER", "Failed to unregister account", t);
|
|
273
243
|
}
|
|
274
244
|
}
|
|
275
245
|
|
|
276
|
-
// ============== Call ===============================================
|
|
277
|
-
|
|
278
246
|
@ReactMethod
|
|
279
247
|
public void makeCall(int accountId, String uri, Promise promise) {
|
|
248
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
249
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
250
|
+
|
|
251
|
+
if (uri == null || uri.trim().isEmpty()) {
|
|
252
|
+
promise.reject("VC_INVALID_ARGUMENT", "destinationUri must not be empty");
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
280
256
|
try {
|
|
281
257
|
ManagedAccount acc = accounts.get(accountId);
|
|
282
|
-
if (acc == null) {
|
|
283
|
-
|
|
284
|
-
|
|
258
|
+
if (acc == null) {
|
|
259
|
+
promise.reject("VC_NO_ACCOUNT", "Unknown accountId " + accountId + ". Call register() first.");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
ManagedCall call = new ManagedCall(ctx, acc, calls);
|
|
263
|
+
call.bindEndpoint(endpoint);
|
|
264
|
+
call.makeCall(uri.trim(), new CallOpParam(true));
|
|
265
|
+
int callId = call.callId();
|
|
266
|
+
if (callId < 0) {
|
|
267
|
+
promise.reject("VC_MAKE_CALL", "Call started but returned invalid callId");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
285
270
|
calls.put(call);
|
|
286
|
-
promise.resolve(
|
|
271
|
+
promise.resolve(callId);
|
|
287
272
|
} catch (Throwable t) {
|
|
288
|
-
|
|
273
|
+
BridgeGuard.reject(promise, "VC_MAKE_CALL", "Failed to place call", t);
|
|
289
274
|
}
|
|
290
275
|
}
|
|
291
276
|
|
|
292
277
|
@ReactMethod
|
|
293
278
|
public void answer(int callId, Promise promise) {
|
|
279
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
280
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
294
281
|
try {
|
|
295
282
|
ManagedCall call = calls.get(callId);
|
|
296
|
-
if (call == null) {
|
|
283
|
+
if (call == null) {
|
|
284
|
+
promise.reject("VC_NO_CALL", "Unknown callId " + callId);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
297
287
|
CallOpParam op = new CallOpParam();
|
|
298
288
|
op.setStatusCode(pjsip_status_code.PJSIP_SC_OK);
|
|
299
289
|
call.answer(op);
|
|
300
290
|
promise.resolve(null);
|
|
301
291
|
} catch (Throwable t) {
|
|
302
|
-
|
|
292
|
+
BridgeGuard.reject(promise, "VC_ANSWER", "Failed to answer call", t);
|
|
303
293
|
}
|
|
304
294
|
}
|
|
305
295
|
|
|
306
296
|
@ReactMethod
|
|
307
|
-
public void hangup(int callId, Promise promise) {
|
|
297
|
+
public void hangup(int callId, String mode, Promise promise) {
|
|
298
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
308
299
|
try {
|
|
309
300
|
ManagedCall call = calls.get(callId);
|
|
310
301
|
if (call == null) { promise.resolve(null); return; }
|
|
311
|
-
CallOpParam op = new CallOpParam();
|
|
312
|
-
|
|
302
|
+
CallOpParam op = new CallOpParam(true);
|
|
303
|
+
if ("busy".equals(mode)) {
|
|
304
|
+
op.setStatusCode(pjsip_status_code.PJSIP_SC_BUSY_HERE);
|
|
305
|
+
} else if ("cancel".equals(mode)) {
|
|
306
|
+
op.setStatusCode(pjsip_status_code.PJSIP_SC_REQUEST_TERMINATED);
|
|
307
|
+
}
|
|
313
308
|
call.hangup(op);
|
|
314
|
-
calls.remove(callId);
|
|
315
309
|
promise.resolve(null);
|
|
316
310
|
} catch (Throwable t) {
|
|
317
|
-
|
|
311
|
+
BridgeGuard.reject(promise, "VC_HANGUP", "Failed to hang up call", t);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Synchronous answer for native notification / Telecom UI when JS is inactive. */
|
|
316
|
+
public boolean answerCallSync(int callId) {
|
|
317
|
+
if (!inited) return false;
|
|
318
|
+
try {
|
|
319
|
+
ManagedCall call = calls.get(callId);
|
|
320
|
+
if (call == null) return false;
|
|
321
|
+
CallOpParam op = new CallOpParam();
|
|
322
|
+
op.setStatusCode(pjsip_status_code.PJSIP_SC_OK);
|
|
323
|
+
call.answer(op);
|
|
324
|
+
return true;
|
|
325
|
+
} catch (Throwable t) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Synchronous hangup for native notification / Telecom UI when JS is inactive. */
|
|
331
|
+
public boolean hangupCallSync(int callId, String mode) {
|
|
332
|
+
try {
|
|
333
|
+
ManagedCall call = calls.get(callId);
|
|
334
|
+
if (call == null) return false;
|
|
335
|
+
CallOpParam op = new CallOpParam(true);
|
|
336
|
+
if ("busy".equals(mode)) {
|
|
337
|
+
op.setStatusCode(pjsip_status_code.PJSIP_SC_BUSY_HERE);
|
|
338
|
+
} else if ("cancel".equals(mode)) {
|
|
339
|
+
op.setStatusCode(pjsip_status_code.PJSIP_SC_REQUEST_TERMINATED);
|
|
340
|
+
}
|
|
341
|
+
call.hangup(op);
|
|
342
|
+
return true;
|
|
343
|
+
} catch (Throwable t) {
|
|
344
|
+
return false;
|
|
318
345
|
}
|
|
319
346
|
}
|
|
320
347
|
|
|
321
348
|
@ReactMethod
|
|
322
349
|
public void hold(int callId, Promise promise) {
|
|
350
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
351
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
323
352
|
try {
|
|
324
|
-
ManagedCall call =
|
|
325
|
-
if (call == null)
|
|
353
|
+
ManagedCall call = requireCall(callId, promise);
|
|
354
|
+
if (call == null) return;
|
|
326
355
|
call.setHold(new CallOpParam(true));
|
|
356
|
+
// Do NOT reconnect audio here: the hold re-INVITE is still in
|
|
357
|
+
// flight, so reconnecting emits a media-state snapshot with the
|
|
358
|
+
// OLD hold flag and desyncs the JS hold state. onCallMediaState
|
|
359
|
+
// rewires the bridge once the media status actually changes.
|
|
327
360
|
WritableMap ev = Arguments.createMap();
|
|
328
361
|
ev.putInt("callId", callId);
|
|
329
362
|
Events.emit(getReactApplicationContext(), Events.CALL_HOLD, ev);
|
|
330
363
|
promise.resolve(null);
|
|
331
364
|
} catch (Throwable t) {
|
|
332
|
-
|
|
365
|
+
BridgeGuard.reject(promise, "VC_HOLD", "Failed to hold call", t);
|
|
333
366
|
}
|
|
334
367
|
}
|
|
335
368
|
|
|
336
369
|
@ReactMethod
|
|
337
370
|
public void unhold(int callId, Promise promise) {
|
|
371
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
372
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
338
373
|
try {
|
|
339
|
-
ManagedCall call =
|
|
340
|
-
if (call == null)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
call.reinvite(
|
|
374
|
+
ManagedCall call = requireCall(callId, promise);
|
|
375
|
+
if (call == null) return;
|
|
376
|
+
CallOpParam prm = new CallOpParam(true);
|
|
377
|
+
prm.getOpt().setFlag(pjsua_call_flag.PJSUA_CALL_UNHOLD);
|
|
378
|
+
call.reinvite(prm);
|
|
379
|
+
// See hold(): audio is rewired by onCallMediaState when the
|
|
380
|
+
// unhold re-INVITE completes and media becomes ACTIVE again.
|
|
344
381
|
WritableMap ev = Arguments.createMap();
|
|
345
382
|
ev.putInt("callId", callId);
|
|
346
383
|
Events.emit(getReactApplicationContext(), Events.CALL_UNHOLD, ev);
|
|
347
384
|
promise.resolve(null);
|
|
348
385
|
} catch (Throwable t) {
|
|
349
|
-
|
|
386
|
+
BridgeGuard.reject(promise, "VC_UNHOLD", "Failed to resume call", t);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private void refreshPjsipPlaybackDevices() {
|
|
391
|
+
if (endpoint == null) return;
|
|
392
|
+
try {
|
|
393
|
+
AudDevManager adm = endpoint.audDevManager();
|
|
394
|
+
adm.refreshDevs();
|
|
395
|
+
} catch (Throwable ignored) {}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Reconnect PJSIP audio bridge after Android route changes (speaker/earpiece). */
|
|
399
|
+
public boolean reconnectCallAudioSync(int callId) {
|
|
400
|
+
if (!inited) return false;
|
|
401
|
+
ManagedCall call = calls.get(callId);
|
|
402
|
+
if (call == null) return false;
|
|
403
|
+
try {
|
|
404
|
+
refreshPjsipPlaybackDevices();
|
|
405
|
+
call.reconnectAudioMedia();
|
|
406
|
+
return true;
|
|
407
|
+
} catch (Throwable t) {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
public int reconnectAllCallsAudioSync() {
|
|
413
|
+
if (!inited) return 0;
|
|
414
|
+
refreshPjsipPlaybackDevices();
|
|
415
|
+
int count = 0;
|
|
416
|
+
for (ManagedCall call : calls.snapshot()) {
|
|
417
|
+
try {
|
|
418
|
+
call.reconnectAudioMedia();
|
|
419
|
+
count++;
|
|
420
|
+
} catch (Throwable ignored) {}
|
|
421
|
+
}
|
|
422
|
+
return count;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
@ReactMethod
|
|
426
|
+
public void reconnectCallAudio(int callId, Promise promise) {
|
|
427
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
428
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
429
|
+
if (reconnectCallAudioSync(callId)) {
|
|
430
|
+
promise.resolve(null);
|
|
431
|
+
} else {
|
|
432
|
+
promise.reject("VC_AUDIO", "Failed to reconnect call audio");
|
|
350
433
|
}
|
|
351
434
|
}
|
|
352
435
|
|
|
353
436
|
@ReactMethod
|
|
354
437
|
public void mute(int callId, boolean on, Promise promise) {
|
|
438
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
439
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
355
440
|
try {
|
|
356
|
-
ManagedCall call =
|
|
357
|
-
if (call == null)
|
|
358
|
-
if (endpoint == null) {
|
|
441
|
+
ManagedCall call = requireCall(callId, promise);
|
|
442
|
+
if (call == null) return;
|
|
443
|
+
if (endpoint == null) {
|
|
444
|
+
promise.reject("VC_NOT_INITIALIZED", "VoipCloud.init() must succeed before mute()");
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
359
447
|
call.setMuted(endpoint, on);
|
|
360
448
|
WritableMap ev = Arguments.createMap();
|
|
361
449
|
ev.putInt("callId", callId);
|
|
@@ -363,15 +451,21 @@ public class VoipCloudModule extends ReactContextBaseJavaModule {
|
|
|
363
451
|
Events.emit(getReactApplicationContext(), Events.CALL_MUTE, ev);
|
|
364
452
|
promise.resolve(null);
|
|
365
453
|
} catch (Throwable t) {
|
|
366
|
-
|
|
454
|
+
BridgeGuard.reject(promise, "VC_MUTE", "Failed to change mute state", t);
|
|
367
455
|
}
|
|
368
456
|
}
|
|
369
457
|
|
|
370
458
|
@ReactMethod
|
|
371
459
|
public void sendDtmf(int callId, String digits, Promise promise) {
|
|
460
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
461
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
462
|
+
if (digits == null || digits.isEmpty()) {
|
|
463
|
+
promise.reject("VC_INVALID_ARGUMENT", "digits must not be empty");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
372
466
|
try {
|
|
373
|
-
ManagedCall call =
|
|
374
|
-
if (call == null)
|
|
467
|
+
ManagedCall call = requireCall(callId, promise);
|
|
468
|
+
if (call == null) return;
|
|
375
469
|
CallSendDtmfParam p = new CallSendDtmfParam();
|
|
376
470
|
p.setDigits(digits);
|
|
377
471
|
call.sendDtmf(p);
|
|
@@ -381,48 +475,53 @@ public class VoipCloudModule extends ReactContextBaseJavaModule {
|
|
|
381
475
|
Events.emit(getReactApplicationContext(), Events.CALL_DTMF_SENT, ev);
|
|
382
476
|
promise.resolve(null);
|
|
383
477
|
} catch (Throwable t) {
|
|
384
|
-
|
|
478
|
+
BridgeGuard.reject(promise, "VC_DTMF", "Failed to send DTMF", t);
|
|
385
479
|
}
|
|
386
480
|
}
|
|
387
481
|
|
|
388
|
-
/**
|
|
389
|
-
* Blind-transfer a call (REFER). The Promise resolves once the
|
|
390
|
-
* REFER is *sent* — progress of the remote-side INVITE comes
|
|
391
|
-
* through `callTransferStatus` events (statusCode + finalNotify).
|
|
392
|
-
*/
|
|
393
482
|
@ReactMethod
|
|
394
483
|
public void transferCall(int callId, String destinationUri, Promise promise) {
|
|
484
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
485
|
+
if (!BridgeGuard.ensureInited(inited, promise)) return;
|
|
486
|
+
if (destinationUri == null || destinationUri.trim().isEmpty()) {
|
|
487
|
+
promise.reject("VC_INVALID_ARGUMENT", "destinationUri must not be empty");
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
395
490
|
try {
|
|
396
|
-
ManagedCall call =
|
|
397
|
-
if (call == null)
|
|
398
|
-
call.xfer(destinationUri, new CallOpParam(true));
|
|
491
|
+
ManagedCall call = requireCall(callId, promise);
|
|
492
|
+
if (call == null) return;
|
|
493
|
+
call.xfer(destinationUri.trim(), new CallOpParam(true));
|
|
399
494
|
WritableMap ev = Arguments.createMap();
|
|
400
495
|
ev.putInt("callId", callId);
|
|
401
|
-
ev.putString("destinationUri", destinationUri);
|
|
496
|
+
ev.putString("destinationUri", destinationUri.trim());
|
|
402
497
|
Events.emit(getReactApplicationContext(), Events.CALL_TRANSFER, ev);
|
|
403
498
|
promise.resolve(null);
|
|
404
499
|
} catch (Throwable t) {
|
|
405
|
-
|
|
500
|
+
BridgeGuard.reject(promise, "VC_TRANSFER", "Failed to transfer call", t);
|
|
406
501
|
}
|
|
407
502
|
}
|
|
408
503
|
|
|
409
|
-
/**
|
|
410
|
-
* End every active call in one shot. Useful for SDK shutdown or
|
|
411
|
-
* a global "end all" UI action. Each call still emits its own
|
|
412
|
-
* DISCONNECTED callState.
|
|
413
|
-
*/
|
|
414
504
|
@ReactMethod
|
|
415
505
|
public void hangupAllCalls(Promise promise) {
|
|
506
|
+
if (!BridgeGuard.ensureNative(promise)) return;
|
|
416
507
|
try {
|
|
417
|
-
if (endpoint != null) endpoint.hangupAllCalls();
|
|
508
|
+
if (endpoint != null && inited) endpoint.hangupAllCalls();
|
|
509
|
+
calls.clear();
|
|
418
510
|
promise.resolve(null);
|
|
419
511
|
} catch (Throwable t) {
|
|
420
|
-
|
|
512
|
+
BridgeGuard.reject(promise, "VC_HANGUP_ALL", "Failed to hang up all calls", t);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private ManagedCall requireCall(int callId, Promise promise) {
|
|
517
|
+
ManagedCall call = calls.get(callId);
|
|
518
|
+
if (call == null) {
|
|
519
|
+
promise.reject("VC_NO_CALL", "Unknown callId " + callId);
|
|
520
|
+
return null;
|
|
421
521
|
}
|
|
522
|
+
return call;
|
|
422
523
|
}
|
|
423
524
|
|
|
424
|
-
// RN event-emitter contract — required on RN 0.65+ when used with
|
|
425
|
-
// NativeEventEmitter on the JS side.
|
|
426
525
|
@ReactMethod public void addListener(String eventName) { /* no-op */ }
|
|
427
526
|
@ReactMethod public void removeListeners(int count) { /* no-op */ }
|
|
428
527
|
}
|