@voipsdk/react-native-voipcloud 1.0.2 → 1.0.4
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 +35 -0
- package/LICENSE +21 -0
- package/README.md +13 -14
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/voipcloud/sdk/rn/VoipCloudModule.java +113 -3
- package/android/src/main/java/com/voipcloud/sdk/rn/internal/ManagedAccount.java +12 -6
- package/android/src/main/java/com/voipcloud/sdk/rn/internal/ManagedCall.java +33 -1
- package/ios/VoipCloud.xcframework/ios-arm64/Headers/module.modulemap +13 -0
- package/ios/VoipCloud.xcframework/ios-arm64_x86_64-simulator/Headers/module.modulemap +13 -0
- package/ios/VoipCloudEngine.mm +89 -5
- package/lib/typescript/types.d.ts +14 -0
- package/package.json +3 -2
- package/react-native-voipcloud.podspec +18 -4
- package/src/types.ts +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ All notable changes to `@voipsdk/react-native-voipcloud` are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.3] - 2026-07-17
|
|
9
|
+
|
|
10
|
+
Folds in the NAT / media / call-routing fixes that were living as app-side
|
|
11
|
+
`patch-package` patches, so a fresh install of the SDK behaves correctly
|
|
12
|
+
without any patches.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `AccountConfig.proxy` — outbound proxy URI (e.g. `sip:proxy.example.com:5060;lr`).
|
|
17
|
+
Routes every SIP request, including in-dialog re-INVITE / BYE, through the proxy
|
|
18
|
+
so in-dialog SIP survives NAT (a NAT'd client no longer sends its BYE / unhold
|
|
19
|
+
re-INVITE straight to the peer's private address). Wired on both iOS and Android.
|
|
20
|
+
- `AccountConfig.srtp` — `'disabled' | 'optional' | 'mandatory'` media policy,
|
|
21
|
+
defaulting to `'disabled'` so a plain-UDP trunk never offers `a=crypto`
|
|
22
|
+
(an optional-SRTP offer breaks media on some PBX / rtpengine setups → one-way audio).
|
|
23
|
+
|
|
24
|
+
### Changed / Fixed
|
|
25
|
+
|
|
26
|
+
- **iOS:** transports are now pinned per account (REGISTER + INVITE + in-dialog
|
|
27
|
+
re-INVITE / BYE share one socket/port for a stable NAT binding across hold/unhold);
|
|
28
|
+
media-only STUN (`mediaStunUse = RETRY_ON_FAILURE`, SIP STUN disabled) so the SDP
|
|
29
|
+
offer carries a routable address; endpoint codec allow-list pinned to PCMU/PCMA
|
|
30
|
+
(+G722 fallback) to stop the proxy rewriting the `m=` line; session timers disabled
|
|
31
|
+
on-device; hold / unhold / makeCall forced audio-only (`audioCount=1, videoCount=0`).
|
|
32
|
+
- **iOS podspec:** `HEADER_SEARCH_PATHS` points at each slice's `Headers` root instead
|
|
33
|
+
of a recursive `/**` glob, so `pj/compat/` no longer shadows the C stdlib.
|
|
34
|
+
- **Android:** accounts pinned to a single transport (`transportIdForRegistrar`);
|
|
35
|
+
`contactUseSrcPort` kept in the rewritten Contact; RFC 4028 session timers as a
|
|
36
|
+
safety net for a lost in-dialog BYE; unhold re-INVITE forces every audio stream back
|
|
37
|
+
to `sendrecv` so the PBX stops streaming MOH after resume.
|
|
38
|
+
- **Android:** native call-end hook (`VoipCallEndNotifier.onCallEnded`) clears the
|
|
39
|
+
incoming notification / ringtone even when JS is gone (headless FCM); `sipCallId`
|
|
40
|
+
now forwarded to the native incoming-call dispatch.
|
|
41
|
+
- **Android:** `minSdkVersion` lowered from 31 to 24.
|
|
42
|
+
|
|
8
43
|
## [1.1.0] - 2026-07-06
|
|
9
44
|
|
|
10
45
|
### Added
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VoipCloud
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @voipsdk/react-native-voipcloud
|
|
2
2
|
|
|
3
|
-
> Public
|
|
4
|
-
> Package: `@voipsdk/react-native-voipcloud@1.
|
|
3
|
+
> Public VoIP SDK for React Native — Android **and** iOS (RN 0.70+).
|
|
4
|
+
> Package: `@voipsdk/react-native-voipcloud@1.0.4`
|
|
5
5
|
|
|
6
6
|
Android and iOS expose the **same** JS API, events and `VC_*` error codes.
|
|
7
7
|
|
|
@@ -41,7 +41,7 @@ packages.add(new com.voipcloud.sdk.rn.VoipCloudPackage());
|
|
|
41
41
|
|
|
42
42
|
### iOS
|
|
43
43
|
|
|
44
|
-
Autolinking installs the pod (which vendors the prebuilt `VoipCloud.xcframework` —
|
|
44
|
+
Autolinking installs the pod (which vendors the prebuilt `VoipCloud.xcframework` — native SIP engine, self-contained). Just run:
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
cd ios && pod install
|
|
@@ -79,7 +79,7 @@ Request at runtime: `RECORD_AUDIO`, `POST_NOTIFICATIONS` (Android 13+), `BLUETOO
|
|
|
79
79
|
```ts
|
|
80
80
|
import { VoipCloud } from '@voipsdk/react-native-voipcloud';
|
|
81
81
|
|
|
82
|
-
// 1. Start
|
|
82
|
+
// 1. Start the VoIP endpoint (required before any SIP call).
|
|
83
83
|
await VoipCloud.init({ userAgent: 'my-app/1.0' });
|
|
84
84
|
|
|
85
85
|
// 2. Register a SIP account → returns numeric accountId.
|
|
@@ -142,7 +142,7 @@ export function useVoipEvents() {
|
|
|
142
142
|
|
|
143
143
|
| Method | Description |
|
|
144
144
|
|--------|-------------|
|
|
145
|
-
| `init(config?)` | Create and start
|
|
145
|
+
| `init(config?)` | Create and start the VoIP endpoint |
|
|
146
146
|
| `shutdown()` | Hang up all, destroy endpoint |
|
|
147
147
|
| `register(account)` | Register SIP account → `accountId` |
|
|
148
148
|
| `unregister(accountId)` | Unregister account |
|
|
@@ -163,7 +163,7 @@ export function useVoipEvents() {
|
|
|
163
163
|
|-------|------|-------------|
|
|
164
164
|
| `userAgent` | `string` | SIP User-Agent header |
|
|
165
165
|
| `stunServer` | `string` | STUN server URI |
|
|
166
|
-
| `logLevel` | `number` |
|
|
166
|
+
| `logLevel` | `number` | SIP log level (0–6) |
|
|
167
167
|
|
|
168
168
|
### `AccountConfig`
|
|
169
169
|
|
|
@@ -196,7 +196,7 @@ export function useVoipEvents() {
|
|
|
196
196
|
|
|
197
197
|
## Error handling
|
|
198
198
|
|
|
199
|
-
Every method returns a `Promise`. Failures reject with `error.code` (string). The bridge **never crashes** the host app for API misuse or recoverable
|
|
199
|
+
Every method returns a `Promise`. Failures reject with `error.code` (string). The bridge **never crashes** the host app for API misuse or recoverable SIP errors.
|
|
200
200
|
|
|
201
201
|
```ts
|
|
202
202
|
try {
|
|
@@ -239,13 +239,12 @@ try {
|
|
|
239
239
|
|
|
240
240
|
| Artifact | Name |
|
|
241
241
|
|----------|------|
|
|
242
|
-
| Prebuilt framework | `VoipCloud.xcframework` —
|
|
243
|
-
|
|
|
242
|
+
| Prebuilt framework | `VoipCloud.xcframework` — native SIP engine, static, self-contained |
|
|
243
|
+
| Native SIP wrapper | `VoipCloudEngine` (Objective-C++) |
|
|
244
244
|
| RN bridge module | `VoipsdkReactNativeVoipcloud` (`RCTEventEmitter`, JS: `VoipCloud`) |
|
|
245
245
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
→ `build-xcframework.sh` (the last step also copies it into `ios/`).
|
|
246
|
+
Both platforms ship their native engine prebuilt and self-contained — no extra
|
|
247
|
+
native toolchain or SIP stack is required in the host app.
|
|
249
248
|
|
|
250
249
|
---
|
|
251
250
|
|
|
@@ -265,7 +264,7 @@ android { packagingOptions { pickFirst '**/libc++_shared.so' } }
|
|
|
265
264
|
```
|
|
266
265
|
|
|
267
266
|
**Registration fails (`VC_REGISTER`)**
|
|
268
|
-
Check `registrar`, credentials, and network. TLS requires a valid server certificate or appropriate
|
|
267
|
+
Check `registrar`, credentials, and network. TLS requires a valid server certificate or appropriate TLS settings on the PBX side.
|
|
269
268
|
|
|
270
269
|
**No audio**
|
|
271
270
|
Android: grant `RECORD_AUDIO` at runtime before placing a call.
|
|
@@ -280,4 +279,4 @@ and links `libc++` + the required system frameworks.
|
|
|
280
279
|
|
|
281
280
|
## License
|
|
282
281
|
|
|
283
|
-
|
|
282
|
+
[MIT](./LICENSE) © VoipCloud. Free for personal **and** commercial use.
|
package/android/build.gradle
CHANGED
|
@@ -10,17 +10,26 @@ import com.facebook.react.bridge.ReactMethod;
|
|
|
10
10
|
import com.facebook.react.bridge.ReadableMap;
|
|
11
11
|
import com.facebook.react.bridge.WritableMap;
|
|
12
12
|
|
|
13
|
+
import android.util.Log;
|
|
14
|
+
|
|
13
15
|
import com.voipcloud.sdk.AccountConfig;
|
|
14
16
|
import com.voipcloud.sdk.AuthCredInfo;
|
|
17
|
+
import com.voipcloud.sdk.CallInfo;
|
|
18
|
+
import com.voipcloud.sdk.CallMediaInfo;
|
|
15
19
|
import com.voipcloud.sdk.CallOpParam;
|
|
20
|
+
import com.voipcloud.sdk.CallSetting;
|
|
16
21
|
import com.voipcloud.sdk.CallSendDtmfParam;
|
|
17
22
|
import com.voipcloud.sdk.Endpoint;
|
|
18
23
|
import com.voipcloud.sdk.EpConfig;
|
|
24
|
+
import com.voipcloud.sdk.IntVector;
|
|
19
25
|
import com.voipcloud.sdk.TransportConfig;
|
|
26
|
+
import com.voipcloud.sdk.pjmedia_dir;
|
|
27
|
+
import com.voipcloud.sdk.pjmedia_type;
|
|
20
28
|
import com.voipcloud.sdk.pjsip_status_code;
|
|
21
29
|
import com.voipcloud.sdk.pjsip_transport_type_e;
|
|
22
30
|
import com.voipcloud.sdk.pjsua_call_flag;
|
|
23
31
|
import com.voipcloud.sdk.pjsua_destroy_flag;
|
|
32
|
+
import com.voipcloud.sdk.pjsua_sip_timer_use;
|
|
24
33
|
import com.voipcloud.sdk.AudDevManager;
|
|
25
34
|
|
|
26
35
|
import com.voipcloud.sdk.rn.internal.BridgeGuard;
|
|
@@ -44,6 +53,14 @@ public class VoipCloudModule extends ReactContextBaseJavaModule {
|
|
|
44
53
|
|
|
45
54
|
private Endpoint endpoint;
|
|
46
55
|
private boolean inited = false;
|
|
56
|
+
/**
|
|
57
|
+
* Transport IDs captured at init() so accounts can be PINNED to one
|
|
58
|
+
* transport (see register()). Both UDP and TCP transports are created; an
|
|
59
|
+
* account bound to neither lets pjsip auto-pick a transport per request,
|
|
60
|
+
* which can switch mid-dialog and break in-dialog routing.
|
|
61
|
+
*/
|
|
62
|
+
private int udpTransportId = -1;
|
|
63
|
+
private int tcpTransportId = -1;
|
|
47
64
|
|
|
48
65
|
public VoipCloudModule(ReactApplicationContext ctx) {
|
|
49
66
|
super(ctx);
|
|
@@ -95,6 +112,8 @@ public class VoipCloudModule extends ReactContextBaseJavaModule {
|
|
|
95
112
|
} catch (Throwable ignored) {}
|
|
96
113
|
endpoint = null;
|
|
97
114
|
inited = false;
|
|
115
|
+
udpTransportId = -1;
|
|
116
|
+
tcpTransportId = -1;
|
|
98
117
|
}
|
|
99
118
|
|
|
100
119
|
@ReactMethod
|
|
@@ -143,10 +162,15 @@ public class VoipCloudModule extends ReactContextBaseJavaModule {
|
|
|
143
162
|
|
|
144
163
|
ep.libInit(epCfg);
|
|
145
164
|
|
|
165
|
+
// Capture the transport IDs so register() can pin each account to
|
|
166
|
+
// exactly ONE transport. Without a binding pjsip is free to choose
|
|
167
|
+
// a different transport for a mid-dialog re-INVITE/UPDATE than the
|
|
168
|
+
// one that established the dialog — the source ip:port then changes
|
|
169
|
+
// and in-dialog routing breaks (see register()).
|
|
146
170
|
TransportConfig udp = new TransportConfig();
|
|
147
|
-
ep.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_UDP, udp);
|
|
171
|
+
udpTransportId = ep.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_UDP, udp);
|
|
148
172
|
TransportConfig tcp = new TransportConfig();
|
|
149
|
-
ep.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_TCP, tcp);
|
|
173
|
+
tcpTransportId = ep.transportCreate(pjsip_transport_type_e.PJSIP_TRANSPORT_TCP, tcp);
|
|
150
174
|
|
|
151
175
|
ep.libStart();
|
|
152
176
|
endpoint = ep;
|
|
@@ -205,11 +229,50 @@ public class VoipCloudModule extends ReactContextBaseJavaModule {
|
|
|
205
229
|
acfg.getNatConfig().setContactRewriteUse(1);
|
|
206
230
|
acfg.getNatConfig().setViaRewriteUse(1);
|
|
207
231
|
acfg.getNatConfig().setUdpKaIntervalSec(20);
|
|
232
|
+
// Keep the learned NAT source PORT in the rewritten Contact (not just
|
|
233
|
+
// the source IP). The remote target our peer routes in-dialog requests
|
|
234
|
+
// to must match the exact ip:port our transport sends from, or replies
|
|
235
|
+
// to our re-INVITE stall and the peer's BYE misses the NAT pinhole.
|
|
236
|
+
acfg.getNatConfig().setContactUseSrcPort(1);
|
|
208
237
|
// Short re-REGISTER interval keeps the binding fresh behind NAT.
|
|
209
238
|
acfg.getRegConfig().setTimeoutSec(120);
|
|
210
239
|
acfg.getRegConfig().setFirstRetryIntervalSec(5);
|
|
211
240
|
acfg.getRegConfig().setRetryIntervalSec(30);
|
|
212
241
|
|
|
242
|
+
// RFC 4028 session timers — safety net for a LOST in-dialog BYE.
|
|
243
|
+
// Behind NAT (kamailio+asterisk, UDP) the remote party's BYE is an
|
|
244
|
+
// inbound in-dialog request; if it never reaches the device the
|
|
245
|
+
// call would otherwise stay "up" forever (remote hangs up, our
|
|
246
|
+
// side never disconnects). With session timers the dialog must be
|
|
247
|
+
// refreshed (re-INVITE/UPDATE) every session-expires seconds — when
|
|
248
|
+
// the peer is gone the refresh gets no answer (or none arrives) and
|
|
249
|
+
// PJSIP tears the call down locally, firing onCallState=DISCONNECTED
|
|
250
|
+
// so the JS/UI ends the call. ALWAYS advertises "timer" in Supported
|
|
251
|
+
// on every INVITE (graceful: peers without RFC 4028 just ignore it;
|
|
252
|
+
// Asterisk negotiates it). Min-SE 90 is the RFC floor; 120s expiry
|
|
253
|
+
// bounds the worst-case stuck-call window (~60s typical — refresh at
|
|
254
|
+
// half). This does NOT fix the root cause (see kamailio record-route
|
|
255
|
+
// NAT config); it guarantees the call cannot hang indefinitely.
|
|
256
|
+
acfg.getCallConfig().setTimerUse(pjsua_sip_timer_use.PJSUA_SIP_TIMER_ALWAYS);
|
|
257
|
+
acfg.getCallConfig().setTimerMinSESec(90);
|
|
258
|
+
acfg.getCallConfig().setTimerSessExpiresSec(120);
|
|
259
|
+
|
|
260
|
+
// PIN the account to a single transport so EVERY request in the call
|
|
261
|
+
// — the initial INVITE, the hold/unhold re-INVITE and any session-timer
|
|
262
|
+
// UPDATE, and the BYE — originates from the SAME local socket/port.
|
|
263
|
+
// Both UDP and TCP transports exist; if the account is bound to neither
|
|
264
|
+
// (transportId = -1 / auto), pjsip may pick a different transport for a
|
|
265
|
+
// mid-dialog re-INVITE than the one that set up the dialog (observed:
|
|
266
|
+
// hold over TCP, unhold over UDP). The source ip:port then changes,
|
|
267
|
+
// kamailio/asterisk can no longer match the request to the dialog, the
|
|
268
|
+
// re-INVITE hangs at 100 Trying, and the call effectively forks into a
|
|
269
|
+
// second dialog — so hold/unhold and hangup act on the wrong one.
|
|
270
|
+
// Binding to one transport keeps the SIP 5-tuple stable for the whole call.
|
|
271
|
+
int boundTransport = transportIdForRegistrar(registrar);
|
|
272
|
+
if (boundTransport >= 0) {
|
|
273
|
+
acfg.getSipConfig().setTransportId(boundTransport);
|
|
274
|
+
}
|
|
275
|
+
|
|
213
276
|
AuthCredInfo cred = new AuthCredInfo("digest", "*", username, 0, password);
|
|
214
277
|
acfg.getSipConfig().getAuthCreds().add(cred);
|
|
215
278
|
|
|
@@ -374,7 +437,38 @@ public class VoipCloudModule extends ReactContextBaseJavaModule {
|
|
|
374
437
|
ManagedCall call = requireCall(callId, promise);
|
|
375
438
|
if (call == null) return;
|
|
376
439
|
CallOpParam prm = new CallOpParam(true);
|
|
377
|
-
|
|
440
|
+
// PJSUA_CALL_UNHOLD alone regenerates the offer, but on this build
|
|
441
|
+
// the audio m-line can stay a=sendonly, so the remote PBX never sees
|
|
442
|
+
// the resume and keeps streaming MOH to the caller. Explicitly force
|
|
443
|
+
// every audio stream back to sendrecv (PJMEDIA_DIR_ENCODING_DECODING)
|
|
444
|
+
// via PJSUA_CALL_SET_MEDIA_DIR so the re-INVITE offer is unambiguous.
|
|
445
|
+
// Build the CallSetting explicitly and attach it with setOpt() —
|
|
446
|
+
// getOpt() can return a detached copy whose mutations are dropped.
|
|
447
|
+
CallSetting opt = new CallSetting(true);
|
|
448
|
+
long flags = pjsua_call_flag.PJSUA_CALL_UNHOLD;
|
|
449
|
+
int mediaCount = 0;
|
|
450
|
+
try {
|
|
451
|
+
IntVector dirs = new IntVector();
|
|
452
|
+
CallInfo info = call.getInfo();
|
|
453
|
+
for (int i = 0; i < info.getMedia().size(); i++) {
|
|
454
|
+
CallMediaInfo mi = info.getMedia().get(i);
|
|
455
|
+
dirs.add(mi.getType() == pjmedia_type.PJMEDIA_TYPE_AUDIO
|
|
456
|
+
? pjmedia_dir.PJMEDIA_DIR_ENCODING_DECODING
|
|
457
|
+
: pjmedia_dir.PJMEDIA_DIR_NONE);
|
|
458
|
+
}
|
|
459
|
+
if (dirs.isEmpty()) {
|
|
460
|
+
dirs.add(pjmedia_dir.PJMEDIA_DIR_ENCODING_DECODING);
|
|
461
|
+
}
|
|
462
|
+
mediaCount = dirs.size();
|
|
463
|
+
opt.setMediaDir(dirs);
|
|
464
|
+
flags |= pjsua_call_flag.PJSUA_CALL_SET_MEDIA_DIR;
|
|
465
|
+
} catch (Throwable ignored) {
|
|
466
|
+
// Fall back to a plain UNHOLD re-INVITE if media enumeration fails.
|
|
467
|
+
}
|
|
468
|
+
opt.setFlag(flags);
|
|
469
|
+
prm.setOpt(opt);
|
|
470
|
+
Log.i("VoipCloudUnhold", "unhold callId=" + callId
|
|
471
|
+
+ " flags=" + flags + " forcedSendrecvOn=" + mediaCount + " media");
|
|
378
472
|
call.reinvite(prm);
|
|
379
473
|
// See hold(): audio is rewired by onCallMediaState when the
|
|
380
474
|
// unhold re-INVITE completes and media becomes ACTIVE again.
|
|
@@ -513,6 +607,22 @@ public class VoipCloudModule extends ReactContextBaseJavaModule {
|
|
|
513
607
|
}
|
|
514
608
|
}
|
|
515
609
|
|
|
610
|
+
/**
|
|
611
|
+
* Choose the transport to bind an account to, based on the {@code transport=}
|
|
612
|
+
* param in its registrar URI (built by mapAccountToVoipCloud on the JS side).
|
|
613
|
+
* Defaults to UDP — the app's standard transport, whose 20s keep-alive holds
|
|
614
|
+
* the NAT pinhole open for the whole call. TCP/TLS accounts bind to the TCP
|
|
615
|
+
* transport. Returns -1 (auto) only if the matching transport was never
|
|
616
|
+
* created, leaving pjsip's default behaviour untouched as a safe fallback.
|
|
617
|
+
*/
|
|
618
|
+
private int transportIdForRegistrar(String registrar) {
|
|
619
|
+
String t = registrar == null ? "" : registrar.toLowerCase();
|
|
620
|
+
if (t.contains("transport=tcp") || t.contains("transport=tls")) {
|
|
621
|
+
return tcpTransportId;
|
|
622
|
+
}
|
|
623
|
+
return udpTransportId;
|
|
624
|
+
}
|
|
625
|
+
|
|
516
626
|
private ManagedCall requireCall(int callId, Promise promise) {
|
|
517
627
|
ManagedCall call = calls.get(callId);
|
|
518
628
|
if (call == null) {
|
|
@@ -60,19 +60,23 @@ public final class ManagedAccount extends Account {
|
|
|
60
60
|
|
|
61
61
|
String from = "";
|
|
62
62
|
String to = "";
|
|
63
|
+
// SIP Call-ID header — stable across systems (PBX CDR, CRM sync, and the
|
|
64
|
+
// FCM push `callId`), unlike the numeric PJSUA call slot which is
|
|
65
|
+
// recycled. Captured here so the native dispatch below can map it to the
|
|
66
|
+
// PJSUA slot (answer the correct call from a push).
|
|
67
|
+
String sipCallId = "";
|
|
63
68
|
try {
|
|
64
69
|
AccountInfo accInfo = getInfo();
|
|
65
70
|
com.voipcloud.sdk.CallInfo callInfo = call.getInfo();
|
|
66
71
|
from = callInfo.getRemoteUri();
|
|
67
72
|
to = accInfo.getUri();
|
|
73
|
+
sipCallId = callInfo.getCallIdString();
|
|
68
74
|
WritableMap payload = Arguments.createMap();
|
|
69
75
|
payload.putInt("accountId", accountId());
|
|
70
76
|
payload.putInt("callId", call.callId());
|
|
71
77
|
payload.putString("from", from);
|
|
72
78
|
payload.putString("to", to);
|
|
73
|
-
|
|
74
|
-
// unlike the numeric PJSUA call slot which is recycled.
|
|
75
|
-
payload.putString("sipCallId", callInfo.getCallIdString());
|
|
79
|
+
payload.putString("sipCallId", sipCallId);
|
|
76
80
|
Events.emit(ctx, Events.INCOMING_CALL, payload);
|
|
77
81
|
} catch (Exception ignored) {
|
|
78
82
|
WritableMap payload = Arguments.createMap();
|
|
@@ -80,12 +84,12 @@ public final class ManagedAccount extends Account {
|
|
|
80
84
|
payload.putInt("callId", call.callId());
|
|
81
85
|
Events.emit(ctx, Events.INCOMING_CALL, payload);
|
|
82
86
|
}
|
|
83
|
-
dispatchIncomingToApp(ctx, accountId(), call.callId(), from, to);
|
|
87
|
+
dispatchIncomingToApp(ctx, accountId(), call.callId(), from, to, sipCallId);
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
/** Notify VoicePBX native layer before JS (background incoming calls). */
|
|
87
91
|
private static void dispatchIncomingToApp(
|
|
88
|
-
ReactContext ctx, int accountId, int callId, String from, String to) {
|
|
92
|
+
ReactContext ctx, int accountId, int callId, String from, String to, String sipCallId) {
|
|
89
93
|
try {
|
|
90
94
|
Class<?> cls = Class.forName("com.voicepbx.voip.VoipIncomingNotifier");
|
|
91
95
|
cls.getMethod(
|
|
@@ -94,6 +98,7 @@ public final class ManagedAccount extends Account {
|
|
|
94
98
|
int.class,
|
|
95
99
|
int.class,
|
|
96
100
|
String.class,
|
|
101
|
+
String.class,
|
|
97
102
|
String.class)
|
|
98
103
|
.invoke(
|
|
99
104
|
null,
|
|
@@ -101,7 +106,8 @@ public final class ManagedAccount extends Account {
|
|
|
101
106
|
accountId,
|
|
102
107
|
callId,
|
|
103
108
|
from != null ? from : "",
|
|
104
|
-
to != null ? to : ""
|
|
109
|
+
to != null ? to : "",
|
|
110
|
+
sipCallId != null ? sipCallId : "");
|
|
105
111
|
} catch (Throwable ignored) {}
|
|
106
112
|
}
|
|
107
113
|
}
|
|
@@ -132,6 +132,12 @@ public final class ManagedCall extends Call {
|
|
|
132
132
|
p.putInt("code", 0);
|
|
133
133
|
p.putString("reason", "");
|
|
134
134
|
Events.emit(ctx, Events.CALL_STATE, p);
|
|
135
|
+
// Native call-end hook: clear the incoming notification / ringing
|
|
136
|
+
// even when JS (React Native) is gone — a headless FCM task may have
|
|
137
|
+
// torn down its RN instance while PJSIP stays alive to receive the
|
|
138
|
+
// CANCEL/BYE. Without this a cancelled call keeps ringing on next
|
|
139
|
+
// app open.
|
|
140
|
+
if (cachedId >= 0) dispatchCallEndToApp(ctx, cachedId, "");
|
|
135
141
|
if (cachedId >= 0) registry.remove(cachedId);
|
|
136
142
|
return;
|
|
137
143
|
}
|
|
@@ -142,15 +148,41 @@ public final class ManagedCall extends Call {
|
|
|
142
148
|
p.putString("reason", info.getLastReason());
|
|
143
149
|
// SIP Call-ID header — lets JS attach the dialog id to outgoing
|
|
144
150
|
// calls too (makeCall only returns the recycled PJSUA slot).
|
|
145
|
-
|
|
151
|
+
String sipCallId = "";
|
|
152
|
+
try { sipCallId = info.getCallIdString(); } catch (Exception ignored) {}
|
|
153
|
+
p.putString("sipCallId", sipCallId);
|
|
146
154
|
Events.emit(ctx, Events.CALL_STATE, p);
|
|
147
155
|
int state = info.getState();
|
|
148
156
|
if (state == pjsip_inv_state.PJSIP_INV_STATE_DISCONNECTED
|
|
149
157
|
|| state == pjsip_inv_state.PJSIP_INV_STATE_NULL) {
|
|
158
|
+
dispatchCallEndToApp(ctx, callId(), sipCallId);
|
|
150
159
|
registry.remove(callId());
|
|
151
160
|
}
|
|
152
161
|
}
|
|
153
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Notify the VoicePBX native layer that a call ended (background hangup /
|
|
165
|
+
* remote cancel), so it can cancel the incoming notification, stop the
|
|
166
|
+
* ringtone and clear the pending open-call id even with no live JS. Mirror
|
|
167
|
+
* of ManagedAccount.dispatchIncomingToApp. Best-effort via reflection so
|
|
168
|
+
* the SDK has no compile dependency on the app package.
|
|
169
|
+
*/
|
|
170
|
+
private static void dispatchCallEndToApp(ReactContext ctx, int callId, String sipCallId) {
|
|
171
|
+
try {
|
|
172
|
+
Class<?> cls = Class.forName("com.voicepbx.voip.VoipCallEndNotifier");
|
|
173
|
+
cls.getMethod(
|
|
174
|
+
"onCallEnded",
|
|
175
|
+
android.content.Context.class,
|
|
176
|
+
int.class,
|
|
177
|
+
String.class)
|
|
178
|
+
.invoke(
|
|
179
|
+
null,
|
|
180
|
+
ctx.getApplicationContext(),
|
|
181
|
+
callId,
|
|
182
|
+
sipCallId != null ? sipCallId : "");
|
|
183
|
+
} catch (Throwable ignored) {}
|
|
184
|
+
}
|
|
185
|
+
|
|
154
186
|
@Override
|
|
155
187
|
public void onCallMediaState(OnCallMediaStateParam prm) {
|
|
156
188
|
connectAudioMedia();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Minimal module map for the VoipCloud static xcframework.
|
|
2
|
+
//
|
|
3
|
+
// PJSIP headers cannot be safely modularised — they include C standard headers
|
|
4
|
+
// (<stdint.h>, <ctype.h>, <string.h>) in a way that creates cyclic module
|
|
5
|
+
// dependencies under Xcode 26+'s _DarwinFoundation2 / _Builtin_stdint system.
|
|
6
|
+
// Declaring no public headers here prevents Xcode from auto-generating a module
|
|
7
|
+
// map that would drag all PJSIP headers into the module system.
|
|
8
|
+
//
|
|
9
|
+
// The ObjC++ bridge files (VoipCloudEngine.mm / VoipCloud.mm) include PJSIP
|
|
10
|
+
// headers textually via HEADER_SEARCH_PATHS, which works correctly without modules.
|
|
11
|
+
module VoipCloud {
|
|
12
|
+
link library "VoipCloud"
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Minimal module map for the VoipCloud static xcframework.
|
|
2
|
+
//
|
|
3
|
+
// PJSIP headers cannot be safely modularised — they include C standard headers
|
|
4
|
+
// (<stdint.h>, <ctype.h>, <string.h>) in a way that creates cyclic module
|
|
5
|
+
// dependencies under Xcode 26+'s _DarwinFoundation2 / _Builtin_stdint system.
|
|
6
|
+
// Declaring no public headers here prevents Xcode from auto-generating a module
|
|
7
|
+
// map that would drag all PJSIP headers into the module system.
|
|
8
|
+
//
|
|
9
|
+
// The ObjC++ bridge files (VoipCloudEngine.mm / VoipCloud.mm) include PJSIP
|
|
10
|
+
// headers textually via HEADER_SEARCH_PATHS, which works correctly without modules.
|
|
11
|
+
module VoipCloud {
|
|
12
|
+
link library "VoipCloud"
|
|
13
|
+
}
|
package/ios/VoipCloudEngine.mm
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
#include <pjsua-lib/pjsua.h>
|
|
9
9
|
#include <string>
|
|
10
10
|
#include <map>
|
|
11
|
+
#include <algorithm>
|
|
12
|
+
#include <cctype>
|
|
11
13
|
|
|
12
14
|
using namespace pj;
|
|
13
15
|
|
|
@@ -270,6 +272,12 @@ void VCCall::onDtmfDigit(OnDtmfDigitParam &prm) {
|
|
|
270
272
|
VCLogWriter *_logWriter;
|
|
271
273
|
std::map<int, VCAccount*> _accounts;
|
|
272
274
|
std::map<int, VCCall*> _calls;
|
|
275
|
+
// Transport ids returned by transportCreate, so an account can be pinned to
|
|
276
|
+
// the SAME socket/port for REGISTER + INVITE + in-dialog re-INVITE/BYE
|
|
277
|
+
// (stable NAT binding across hold/unhold). -1 = that transport not created.
|
|
278
|
+
int _udpTransportId;
|
|
279
|
+
int _tcpTransportId;
|
|
280
|
+
int _tlsTransportId;
|
|
273
281
|
}
|
|
274
282
|
|
|
275
283
|
+ (instancetype)shared {
|
|
@@ -315,17 +323,42 @@ void VCCall::onDtmfDigit(OnDtmfDigitParam &prm) {
|
|
|
315
323
|
_ep->libInit(epCfg);
|
|
316
324
|
|
|
317
325
|
// Ephemeral ports (0) avoid "Address already in use" for SIP clients.
|
|
318
|
-
|
|
319
|
-
|
|
326
|
+
// Keep each transport id so registerAccountWithConfig can pin the
|
|
327
|
+
// account to the matching one (0 is a valid id → init to -1).
|
|
328
|
+
_udpTransportId = _tcpTransportId = _tlsTransportId = -1;
|
|
329
|
+
{ TransportConfig t; t.port = 0; _udpTransportId = _ep->transportCreate(PJSIP_TRANSPORT_UDP, t); }
|
|
330
|
+
{ TransportConfig t; t.port = 0; _tcpTransportId = _ep->transportCreate(PJSIP_TRANSPORT_TCP, t); }
|
|
320
331
|
{
|
|
321
332
|
TransportConfig t; t.port = 0;
|
|
322
333
|
t.tlsConfig.verifyServer = false;
|
|
323
334
|
t.tlsConfig.verifyClient = false;
|
|
324
335
|
t.tlsConfig.method = PJSIP_TLSV1_2_METHOD;
|
|
325
|
-
_ep->transportCreate(PJSIP_TRANSPORT_TLS, t);
|
|
336
|
+
_tlsTransportId = _ep->transportCreate(PJSIP_TRANSPORT_TLS, t);
|
|
326
337
|
}
|
|
327
338
|
|
|
328
339
|
_ep->libStart();
|
|
340
|
+
|
|
341
|
+
// Codec allow-list. The PBX/proxy speaks G.711 (+G722 fallback); PJSIP's
|
|
342
|
+
// full default set (speex 8k/16k/32k, iLBC, …) makes the proxy rewrite
|
|
343
|
+
// the m= line and shift dynamic payload-type numbers → wrong codec map →
|
|
344
|
+
// broken audio. Priority 0 = codec not offered at all. Order: PCMU/PCMA
|
|
345
|
+
// first, G722 as fallback, so the far end answers G.711. Codec priority
|
|
346
|
+
// is endpoint-global in PJSIP, so it is configured once here.
|
|
347
|
+
try {
|
|
348
|
+
CodecInfoVector2 codecs = _ep->codecEnum2();
|
|
349
|
+
for (size_t i = 0; i < codecs.size(); i++) {
|
|
350
|
+
std::string cid = codecs[i].codecId; // e.g. "PCMU/8000/1"
|
|
351
|
+
std::string fam = cid.substr(0, cid.find('/'));
|
|
352
|
+
std::transform(fam.begin(), fam.end(), fam.begin(),
|
|
353
|
+
[](unsigned char c){ return (char)std::toupper(c); });
|
|
354
|
+
pj_uint8_t prio = 0;
|
|
355
|
+
if (fam == "PCMU") prio = 255;
|
|
356
|
+
else if (fam == "PCMA") prio = 254;
|
|
357
|
+
else if (fam == "G722") prio = 200;
|
|
358
|
+
_ep->codecSetPriority(codecs[i].codecId, prio);
|
|
359
|
+
}
|
|
360
|
+
} catch (...) {}
|
|
361
|
+
|
|
329
362
|
[self configureAudioSessionCategory];
|
|
330
363
|
_isStarted = YES;
|
|
331
364
|
return YES;
|
|
@@ -372,6 +405,15 @@ void VCCall::onDtmfDigit(OnDtmfDigitParam &prm) {
|
|
|
372
405
|
acfg.idUri = std::string(uri.UTF8String);
|
|
373
406
|
acfg.regConfig.registrarUri = std::string(registrar.UTF8String);
|
|
374
407
|
|
|
408
|
+
// Outbound proxy (Kamailio). Route EVERY SIP request — including
|
|
409
|
+
// in-dialog re-INVITE / BYE — through the proxy so a NAT'd client's
|
|
410
|
+
// teardown/unhold doesn't go straight to the peer's private address and
|
|
411
|
+
// get lost (peer stuck on MOH after unhold, call never ends).
|
|
412
|
+
NSString *proxy = cfg[@"proxy"];
|
|
413
|
+
if ([proxy isKindOfClass:[NSString class]] && proxy.length) {
|
|
414
|
+
acfg.sipConfig.proxies.push_back(std::string(proxy.UTF8String));
|
|
415
|
+
}
|
|
416
|
+
|
|
375
417
|
// NAT survival — keep the registration binding alive behind NAT so
|
|
376
418
|
// inbound INVITEs still reach the device (matches Android).
|
|
377
419
|
acfg.natConfig.contactRewriteUse = 1;
|
|
@@ -381,11 +423,43 @@ void VCCall::onDtmfDigit(OnDtmfDigitParam &prm) {
|
|
|
381
423
|
acfg.regConfig.firstRetryIntervalSec = 5;
|
|
382
424
|
acfg.regConfig.retryIntervalSec = 30;
|
|
383
425
|
|
|
426
|
+
// STUN for MEDIA only: without it the SDP offer carries the LAN IP
|
|
427
|
+
// (c=192.168.x) and the far end sends RTP to an unroutable address →
|
|
428
|
+
// no 2-way audio. Falls back to the host addr if STUN is unreachable.
|
|
429
|
+
// SIP keeps rport/contactRewrite — do NOT STUN it (would disturb
|
|
430
|
+
// REGISTER). STUN server itself is set at endpoint level (stunServer).
|
|
431
|
+
acfg.natConfig.mediaStunUse = PJSUA_STUN_RETRY_ON_FAILURE;
|
|
432
|
+
acfg.natConfig.sipStunUse = PJSUA_STUN_USE_DISABLED;
|
|
433
|
+
|
|
384
434
|
AuthCredInfo cred("digest", "*", std::string(username.UTF8String), 0,
|
|
385
435
|
std::string(password.UTF8String));
|
|
386
436
|
acfg.sipConfig.authCreds.push_back(cred);
|
|
387
437
|
|
|
388
|
-
|
|
438
|
+
// Pin REGISTER + INVITE + in-dialog re-INVITE/BYE to the SAME transport
|
|
439
|
+
// (socket/port) so the NAT binding is stable across hold/unhold. Without
|
|
440
|
+
// this PJSIP may auto-pick a different transport per request. Falls back
|
|
441
|
+
// to auto-select if the requested transport failed to create (-1).
|
|
442
|
+
NSString *transport = cfg[@"transport"];
|
|
443
|
+
int pinnedTransportId = -1;
|
|
444
|
+
if ([transport isEqualToString:@"TLS"]) pinnedTransportId = _tlsTransportId;
|
|
445
|
+
else if ([transport isEqualToString:@"TCP"]) pinnedTransportId = _tcpTransportId;
|
|
446
|
+
else if ([transport isEqualToString:@"UDP"]) pinnedTransportId = _udpTransportId;
|
|
447
|
+
if (pinnedTransportId >= 0) acfg.sipConfig.transportId = pinnedTransportId;
|
|
448
|
+
|
|
449
|
+
// No session timers on the device — the VoIP server owns session
|
|
450
|
+
// refresh. With them on, the device (refresher) proposes Session-Expires
|
|
451
|
+
// and periodically sends a chatty UPDATE after ACK.
|
|
452
|
+
acfg.callConfig.timerUse = PJSUA_SIP_TIMER_INACTIVE;
|
|
453
|
+
|
|
454
|
+
// SRTP is opt-in per account. A plain-UDP trunk with srtp='disabled'
|
|
455
|
+
// must NOT offer a=crypto (an optional-SRTP offer breaks media on some
|
|
456
|
+
// PBX/rtpengine setups → one-way / no audio on outgoing). Default
|
|
457
|
+
// DISABLED when the account doesn't ask for it.
|
|
458
|
+
NSString *srtp = cfg[@"srtp"];
|
|
459
|
+
pjmedia_srtp_use srtpUse = PJMEDIA_SRTP_DISABLED;
|
|
460
|
+
if ([srtp isEqualToString:@"mandatory"]) srtpUse = PJMEDIA_SRTP_MANDATORY;
|
|
461
|
+
else if ([srtp isEqualToString:@"optional"]) srtpUse = PJMEDIA_SRTP_OPTIONAL;
|
|
462
|
+
acfg.mediaConfig.srtpUse = srtpUse;
|
|
389
463
|
acfg.mediaConfig.srtpSecureSignaling = 0;
|
|
390
464
|
|
|
391
465
|
VCAccount *acc = new VCAccount();
|
|
@@ -425,6 +499,8 @@ void VCCall::onDtmfDigit(OnDtmfDigitParam &prm) {
|
|
|
425
499
|
VCCall *call = new VCCall(*it->second);
|
|
426
500
|
call->owner = self;
|
|
427
501
|
CallOpParam prm(true);
|
|
502
|
+
prm.opt.audioCount = 1;
|
|
503
|
+
prm.opt.videoCount = 0; // audio-only: no stray video m-line in the offer
|
|
428
504
|
call->makeCall(std::string(uri.UTF8String), prm);
|
|
429
505
|
int cid = call->getId();
|
|
430
506
|
call->cachedId = cid;
|
|
@@ -470,7 +546,13 @@ void VCCall::onDtmfDigit(OnDtmfDigitParam &prm) {
|
|
|
470
546
|
- (BOOL)holdCall:(int)callId error:(NSError **)error {
|
|
471
547
|
VCCall *call = [self callForId:callId];
|
|
472
548
|
if (!call) { if (error) *error = VCMakeError(-1, [NSString stringWithFormat:@"Unknown callId %d", callId]); return NO; }
|
|
473
|
-
try {
|
|
549
|
+
try {
|
|
550
|
+
CallOpParam prm(true);
|
|
551
|
+
prm.opt.audioCount = 1;
|
|
552
|
+
prm.opt.videoCount = 0; // audio-only re-INVITE
|
|
553
|
+
call->setHold(prm);
|
|
554
|
+
return YES;
|
|
555
|
+
}
|
|
474
556
|
catch (Error &err) { if (error) *error = VCMakeError(err.status, VCStr(err.info())); return NO; }
|
|
475
557
|
}
|
|
476
558
|
|
|
@@ -480,6 +562,8 @@ void VCCall::onDtmfDigit(OnDtmfDigitParam &prm) {
|
|
|
480
562
|
try {
|
|
481
563
|
CallOpParam prm(true);
|
|
482
564
|
prm.opt.flag |= PJSUA_CALL_UNHOLD;
|
|
565
|
+
prm.opt.audioCount = 1;
|
|
566
|
+
prm.opt.videoCount = 0; // audio-only re-INVITE
|
|
483
567
|
call->reinvite(prm);
|
|
484
568
|
return YES;
|
|
485
569
|
} catch (Error &err) { if (error) *error = VCMakeError(err.status, VCStr(err.info())); return NO; }
|
|
@@ -31,6 +31,20 @@ export interface AccountConfig {
|
|
|
31
31
|
* the two in sync to avoid surprises.
|
|
32
32
|
*/
|
|
33
33
|
transport: Transport;
|
|
34
|
+
/**
|
|
35
|
+
* Outbound proxy URI, e.g. "sip:proxy.example.com:5060;lr". When set,
|
|
36
|
+
* every SIP request — including in-dialog re-INVITE / BYE — is routed
|
|
37
|
+
* through this proxy (`proxies.push_back`). Required for in-dialog SIP
|
|
38
|
+
* to survive NAT: without a proxy Route a NAT'd client sends its BYE /
|
|
39
|
+
* unhold re-INVITE straight to the peer's private address and it is lost.
|
|
40
|
+
*/
|
|
41
|
+
proxy?: string;
|
|
42
|
+
/**
|
|
43
|
+
* SRTP media policy. Defaults to 'disabled' when omitted so a plain-UDP
|
|
44
|
+
* trunk never offers a=crypto (an optional-SRTP offer breaks media on some
|
|
45
|
+
* PBX / rtpengine setups → one-way / no audio).
|
|
46
|
+
*/
|
|
47
|
+
srtp?: 'disabled' | 'optional' | 'mandatory';
|
|
34
48
|
}
|
|
35
49
|
export interface RegistrationStateEvent {
|
|
36
50
|
accountId: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voipsdk/react-native-voipcloud",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Public React Native VoIP SDK backed by PJSIP (VoipCloud).",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"react-native.config.js",
|
|
17
17
|
"README.md",
|
|
18
18
|
"CHANGELOG.md",
|
|
19
|
+
"LICENSE",
|
|
19
20
|
"!**/__tests__",
|
|
20
21
|
"!**/.*"
|
|
21
22
|
],
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"name": "VoipCloud",
|
|
42
43
|
"email": "htt.bkap@gmail.com"
|
|
43
44
|
},
|
|
44
|
-
"license": "
|
|
45
|
+
"license": "MIT",
|
|
45
46
|
"repository": {
|
|
46
47
|
"type": "git",
|
|
47
48
|
"url": "git+https://github.com/hoangdx/react-native-voipcloud.git"
|
|
@@ -28,16 +28,30 @@ Pod::Spec.new do |s|
|
|
|
28
28
|
s.pod_target_xcconfig = {
|
|
29
29
|
"CLANG_CXX_LANGUAGE_STANDARD" => "gnu++17",
|
|
30
30
|
"CLANG_CXX_LIBRARY" => "libc++",
|
|
31
|
+
# Xcode 26+ introduced a stricter modular C header system (_DarwinFoundation2)
|
|
32
|
+
# that creates a cyclic dependency when PJSIP headers include <stdint.h> inside
|
|
33
|
+
# a module compilation context:
|
|
34
|
+
# _DarwinFoundation2 -> _Builtin_stdint -> _DarwinFoundation2
|
|
35
|
+
# This leaves isalnum/strlen/memset/jmp_buf etc. undeclared inside the PJSIP
|
|
36
|
+
# compat headers. Disabling modules for this pod target makes all #import/#include
|
|
37
|
+
# directives textual — the PJSIP headers then see the full stdlib without any
|
|
38
|
+
# module cycle, and all C std-library identifiers are declared as expected.
|
|
39
|
+
"CLANG_ENABLE_MODULES" => "NO",
|
|
31
40
|
# PJSIP was built via autoconf, so pj/config.h only pulls the generated
|
|
32
41
|
# machine config (endianness, sizes, feature flags in m_auto.h/os_auto.h)
|
|
33
42
|
# when PJ_AUTOCONF is defined. Consumers MUST compile with it too — both to
|
|
34
43
|
# fix arm64 endianness detection and to keep struct layouts ABI-compatible
|
|
35
44
|
# with the prebuilt libVoipCloud.a.
|
|
36
45
|
"GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) PJ_AUTOCONF=1",
|
|
37
|
-
# pjsua2.hpp and
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
46
|
+
# pjsua2.hpp and the pj/ pjsip/ pjmedia/ trees live at each slice's Headers/
|
|
47
|
+
# root, and PJSIP includes are all rooted there (<pjsua2.hpp>, <pj/xxx.h>).
|
|
48
|
+
# Point at that root directly — one path per slice, only the matching one
|
|
49
|
+
# exists per build. Do NOT use a recursive /** glob: it drags every subdir,
|
|
50
|
+
# including pj/compat/, onto the search path. pj/compat/ ships files named
|
|
51
|
+
# ctype.h, string.h, setjmp.h, etc. that shadow the C stdlib, so PJSIP's own
|
|
52
|
+
# `#include <ctype.h>` resolves to the shadow (self, guard already defined)
|
|
53
|
+
# instead of libc — leaving isalnum/strlen/memset/jmp_buf undeclared.
|
|
54
|
+
"HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)/ios/VoipCloud.xcframework/ios-arm64/Headers\" \"$(PODS_TARGET_SRCROOT)/ios/VoipCloud.xcframework/ios-arm64_x86_64-simulator/Headers\""
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
s.dependency "React-Core"
|
package/src/types.ts
CHANGED
|
@@ -34,6 +34,20 @@ export interface AccountConfig {
|
|
|
34
34
|
* the two in sync to avoid surprises.
|
|
35
35
|
*/
|
|
36
36
|
transport: Transport;
|
|
37
|
+
/**
|
|
38
|
+
* Outbound proxy URI, e.g. "sip:proxy.example.com:5060;lr". When set,
|
|
39
|
+
* every SIP request — including in-dialog re-INVITE / BYE — is routed
|
|
40
|
+
* through this proxy (`proxies.push_back`). Required for in-dialog SIP
|
|
41
|
+
* to survive NAT: without a proxy Route a NAT'd client sends its BYE /
|
|
42
|
+
* unhold re-INVITE straight to the peer's private address and it is lost.
|
|
43
|
+
*/
|
|
44
|
+
proxy?: string;
|
|
45
|
+
/**
|
|
46
|
+
* SRTP media policy. Defaults to 'disabled' when omitted so a plain-UDP
|
|
47
|
+
* trunk never offers a=crypto (an optional-SRTP offer breaks media on some
|
|
48
|
+
* PBX / rtpengine setups → one-way / no audio).
|
|
49
|
+
*/
|
|
50
|
+
srtp?: 'disabled' | 'optional' | 'mandatory';
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
export interface RegistrationStateEvent {
|