@dialstack/sdk-native 0.1.0 → 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/README.md +166 -36
- package/dist/index.d.ts +670 -54
- package/dist/index.js +3139 -604
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +361 -0
- package/dist/testing.js +606 -0
- package/dist/testing.js.map +1 -0
- package/package.json +6 -1
package/dist/index.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ interface PaginatedList<P extends Page<unknown>> extends Promise<P> {
|
|
|
41
41
|
* rather than depending on the ambient `lib.dom.d.ts` names directly.
|
|
42
42
|
*/
|
|
43
43
|
type RTCPeerConnection = globalThis.RTCPeerConnection;
|
|
44
|
-
type MediaStream = globalThis.MediaStream;
|
|
44
|
+
type MediaStream$1 = globalThis.MediaStream;
|
|
45
45
|
type RTCIceServer$1 = globalThis.RTCIceServer;
|
|
46
46
|
type RTCSessionDescriptionInit = globalThis.RTCSessionDescriptionInit;
|
|
47
47
|
/**
|
|
@@ -81,7 +81,7 @@ interface Ringback {
|
|
|
81
81
|
setSinkId?(deviceId: string | null): void;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
type PhoneErrorCode = 'auth_failed' | 'auth_expired' | 'invalid_message' | 'call_failed' | 'call_not_found' | 'emergency_address_required' | 'session_limit' | 'session_replaced' | 'session_revoked' | 'rate_limited' | 'presence_unavailable' | 'reachability_unavailable' | 'internal_error' | 'going_away' | 'idle_timeout' | 'slow_consumer' | 'transport_closed' | 'ice_fetch_failed' | 'mic_permission_denied' | 'audio_device_unavailable' | 'audio_device_in_use';
|
|
84
|
+
type PhoneErrorCode = 'auth_failed' | 'auth_expired' | 'invalid_message' | 'call_failed' | 'call_not_found' | 'emergency_address_required' | 'session_limit' | 'session_replaced' | 'session_revoked' | 'rate_limited' | 'call_limit' | 'presence_unavailable' | 'reachability_unavailable' | 'internal_error' | 'going_away' | 'idle_timeout' | 'slow_consumer' | 'transport_closed' | 'ice_fetch_failed' | 'token_refresh_failed' | 'mic_permission_denied' | 'audio_device_unavailable' | 'audio_device_in_use';
|
|
85
85
|
declare class PhoneError extends Error {
|
|
86
86
|
readonly code: PhoneErrorCode;
|
|
87
87
|
readonly callId: string | null;
|
|
@@ -161,6 +161,17 @@ interface PhoneOptions {
|
|
|
161
161
|
* processed the socket's close), reconnecting if it's dead.
|
|
162
162
|
*/
|
|
163
163
|
onAppResume?: AppResumeSubscribe;
|
|
164
|
+
/**
|
|
165
|
+
* Acquire an inbound call's microphone when it is ANSWERED rather than when it
|
|
166
|
+
* arrives. Set by platforms whose OS owns the call audio session: CallKit and
|
|
167
|
+
* Telecom activate it only on answer, and capturing before then fails — on
|
|
168
|
+
* Android the device is still in `MODE_RINGTONE`, where WebRTC cannot start its
|
|
169
|
+
* recorder, so it releases it and the answered call has no mic at all.
|
|
170
|
+
*
|
|
171
|
+
* Off by default: a browser can capture during ringing, and doing so surfaces a
|
|
172
|
+
* microphone problem while the user can still see it.
|
|
173
|
+
*/
|
|
174
|
+
deferInboundCapture?: boolean;
|
|
164
175
|
/**
|
|
165
176
|
* Microphone `deviceId` for new calls; omitted means the OS default. Held for the
|
|
166
177
|
* phone's lifetime and NOT persisted — remembering a device is the host's call, so
|
|
@@ -299,25 +310,32 @@ type ClientMessage = {
|
|
|
299
310
|
sdp: string;
|
|
300
311
|
} | {
|
|
301
312
|
type: 'call.answer';
|
|
313
|
+
req_id?: string;
|
|
302
314
|
call_id: string;
|
|
303
315
|
} | {
|
|
304
316
|
type: 'call.reject';
|
|
317
|
+
req_id?: string;
|
|
305
318
|
call_id: string;
|
|
306
319
|
reason?: RejectReason;
|
|
307
320
|
} | {
|
|
308
321
|
type: 'call.hangup';
|
|
322
|
+
req_id?: string;
|
|
309
323
|
call_id: string;
|
|
310
324
|
} | {
|
|
311
325
|
type: 'call.hold';
|
|
326
|
+
req_id?: string;
|
|
312
327
|
call_id: string;
|
|
313
328
|
} | {
|
|
314
329
|
type: 'call.resume';
|
|
330
|
+
req_id?: string;
|
|
315
331
|
call_id: string;
|
|
316
332
|
} | {
|
|
317
333
|
type: 'call.mute';
|
|
334
|
+
req_id?: string;
|
|
318
335
|
call_id: string;
|
|
319
336
|
} | {
|
|
320
337
|
type: 'call.transfer';
|
|
338
|
+
req_id?: string;
|
|
321
339
|
call_id: string;
|
|
322
340
|
destination: string;
|
|
323
341
|
} | {
|
|
@@ -329,6 +347,7 @@ type ClientMessage = {
|
|
|
329
347
|
sdp?: string;
|
|
330
348
|
} | {
|
|
331
349
|
type: 'call.unmute';
|
|
350
|
+
req_id?: string;
|
|
332
351
|
call_id: string;
|
|
333
352
|
} | {
|
|
334
353
|
type: 'sdp.offer';
|
|
@@ -476,6 +495,7 @@ type TransportEvents = {
|
|
|
476
495
|
error?: PhoneError;
|
|
477
496
|
}) => void;
|
|
478
497
|
reconnecting: (attempt: number, delayMs: number) => void;
|
|
498
|
+
wake: () => void;
|
|
479
499
|
};
|
|
480
500
|
declare class Transport {
|
|
481
501
|
private ws;
|
|
@@ -507,6 +527,11 @@ declare class Transport {
|
|
|
507
527
|
*/
|
|
508
528
|
trySend(msg: ClientMessage): void;
|
|
509
529
|
close(): void;
|
|
530
|
+
/**
|
|
531
|
+
* Abandon the current socket without ending the transport: its close runs the
|
|
532
|
+
* normal backoff reconnect. For an attempt that must not authenticate.
|
|
533
|
+
*/
|
|
534
|
+
dropSocket(): void;
|
|
510
535
|
private openSocket;
|
|
511
536
|
private scheduleReconnect;
|
|
512
537
|
private onWake;
|
|
@@ -525,6 +550,16 @@ type CallEventMap = {
|
|
|
525
550
|
audioInputLost: () => void;
|
|
526
551
|
};
|
|
527
552
|
type Listener$1<K extends keyof CallEventMap> = CallEventMap[K];
|
|
553
|
+
/**
|
|
554
|
+
* The slice of a local conference a merged `Call` needs. Declared structurally
|
|
555
|
+
* here (rather than importing `LocalConference`) so the core has no dependency
|
|
556
|
+
* on the mixer — `conference.ts` imports `Call`, and the reverse import would
|
|
557
|
+
* make that a cycle.
|
|
558
|
+
*/
|
|
559
|
+
interface ConferenceSink {
|
|
560
|
+
/** Apply mute across the merged legs' mixed uplinks. */
|
|
561
|
+
setMuted(muted: boolean): void;
|
|
562
|
+
}
|
|
528
563
|
interface CallInit {
|
|
529
564
|
id: string;
|
|
530
565
|
direction: CallDirection;
|
|
@@ -538,6 +573,8 @@ interface CallInit {
|
|
|
538
573
|
ringback?: Ringback;
|
|
539
574
|
audioInputDeviceId?: string;
|
|
540
575
|
audioOutputDeviceId?: string;
|
|
576
|
+
deferInboundCapture?: boolean;
|
|
577
|
+
nextReqId: () => string;
|
|
541
578
|
}
|
|
542
579
|
declare class Call {
|
|
543
580
|
id: string;
|
|
@@ -571,6 +608,9 @@ declare class Call {
|
|
|
571
608
|
*/
|
|
572
609
|
get canSendDtmf(): boolean;
|
|
573
610
|
private transport;
|
|
611
|
+
private readonly nextReqId;
|
|
612
|
+
private readonly issuedReqIds;
|
|
613
|
+
private readonly hangupReqIds;
|
|
574
614
|
private readonly startConsult;
|
|
575
615
|
private localStream;
|
|
576
616
|
private remoteStream;
|
|
@@ -580,6 +620,7 @@ declare class Call {
|
|
|
580
620
|
private listeners;
|
|
581
621
|
private endedSettled;
|
|
582
622
|
private pendingAnswerSdp;
|
|
623
|
+
private buildingAnswer;
|
|
583
624
|
private answerSent;
|
|
584
625
|
private wantsAnswer;
|
|
585
626
|
private localMediaReady;
|
|
@@ -589,8 +630,25 @@ declare class Call {
|
|
|
589
630
|
private readonly ringback;
|
|
590
631
|
private remoteAudioFlowing;
|
|
591
632
|
private audioInputDeviceId_;
|
|
633
|
+
private deferredAudioInput;
|
|
634
|
+
private conference;
|
|
592
635
|
private switchChain;
|
|
636
|
+
private captureDeferred;
|
|
593
637
|
constructor(init: CallInit);
|
|
638
|
+
/**
|
|
639
|
+
* Take the mic this call deliberately skipped on arrival, then produce the
|
|
640
|
+
* answer SDP from it.
|
|
641
|
+
*
|
|
642
|
+
* `buildAnswer` is what needs the track: setRemoteDescription created a
|
|
643
|
+
* track-less sender per m-line, and answering without attaching a real track
|
|
644
|
+
* yields `a=recvonly` — the far end hears nothing, which is the failure this
|
|
645
|
+
* whole path exists to avoid. It runs only once the offer has been set;
|
|
646
|
+
* otherwise prepareAnswerForOffer does it when the offer lands.
|
|
647
|
+
*/
|
|
648
|
+
private acquireDeferredCapture;
|
|
649
|
+
/** Set by the owner to surface a late mic failure, since `Call` has no error event. */
|
|
650
|
+
private reportCaptureFailure;
|
|
651
|
+
onCaptureFailure(report: (e: unknown) => void): void;
|
|
594
652
|
whenLocalMediaReady(): Promise<void>;
|
|
595
653
|
private acquireLocalMedia;
|
|
596
654
|
/** The selected mic, or null for the OS default. */
|
|
@@ -607,6 +665,7 @@ declare class Call {
|
|
|
607
665
|
setAudioOutputDevice(deviceId: string | null): void;
|
|
608
666
|
private watchCaptureTrack;
|
|
609
667
|
private get isFinished();
|
|
668
|
+
private issueReqId;
|
|
610
669
|
on<K extends keyof CallEventMap>(event: K, handler: Listener$1<K>): void;
|
|
611
670
|
off<K extends keyof CallEventMap>(event: K, handler?: Listener$1<K>): void;
|
|
612
671
|
answer(): void;
|
|
@@ -614,9 +673,40 @@ declare class Call {
|
|
|
614
673
|
reject(reason?: RejectReason): void;
|
|
615
674
|
hangup(): void;
|
|
616
675
|
hold(): void;
|
|
676
|
+
/**
|
|
677
|
+
* Hold or resume this leg as part of holding the whole conference.
|
|
678
|
+
*
|
|
679
|
+
* `hold()` refuses while merged because holding ONE party silences it to the
|
|
680
|
+
* other. Holding every leg together is the coherent version, so the caller
|
|
681
|
+
* that does that reaches the frames through here.
|
|
682
|
+
*/
|
|
683
|
+
setConferenceHold(held: boolean): void;
|
|
684
|
+
/**
|
|
685
|
+
* Re-hold a call `resume()` ran on in this same tick. `hold()`'s
|
|
686
|
+
* `state !== 'active'` guard would swallow it, since the state only flips on
|
|
687
|
+
* the server echo. Only for that unwind; use `hold()` everywhere else.
|
|
688
|
+
*/
|
|
689
|
+
holdAfterResume(): void;
|
|
617
690
|
resume(): void;
|
|
618
691
|
mute(): void;
|
|
619
692
|
unmute(): void;
|
|
693
|
+
/**
|
|
694
|
+
* Mute has two layers, and a merged call needs both handled differently.
|
|
695
|
+
*
|
|
696
|
+
* Normally: flip the mic track (instant local feedback) AND send `call.mute`,
|
|
697
|
+
* which the server turns into a re-INVITE narrowing the leg's SDP direction to
|
|
698
|
+
* `recvonly` — defence in depth against a peer that ignores the local disable.
|
|
699
|
+
*
|
|
700
|
+
* While MERGED neither of those works:
|
|
701
|
+
* - the wire carries the conference's mixed track, not this call's mic track,
|
|
702
|
+
* so toggling `localStream` no longer reaches the far end (the mic is an
|
|
703
|
+
* input to the mixer now). The conference toggles the mixed uplinks instead.
|
|
704
|
+
* - the server frame must be withheld: `recvonly` would stop this leg's
|
|
705
|
+
* upstream media at the far end regardless of what the graph does, which is
|
|
706
|
+
* silent one-way audio for the other conference parties. Muting a merged
|
|
707
|
+
* call is a purely local mix decision.
|
|
708
|
+
*/
|
|
709
|
+
private setMuted;
|
|
620
710
|
sendDtmf(digits: string, duration?: number, interToneGap?: number): void;
|
|
621
711
|
/**
|
|
622
712
|
* Blind transfer: immediately redirect the remote party to `destination`.
|
|
@@ -640,10 +730,25 @@ declare class Call {
|
|
|
640
730
|
*/
|
|
641
731
|
completeTransfer(): void;
|
|
642
732
|
private assertTransferable;
|
|
643
|
-
get remoteMediaStream(): MediaStream;
|
|
733
|
+
get remoteMediaStream(): MediaStream$1;
|
|
734
|
+
/**
|
|
735
|
+
* The local capture (mic) stream. A local conference mixes against this rather
|
|
736
|
+
* than opening a second `getUserMedia`, so every merged leg shares the one
|
|
737
|
+
* capture the call already owns.
|
|
738
|
+
*/
|
|
739
|
+
get localMediaStream(): MediaStream$1;
|
|
740
|
+
/**
|
|
741
|
+
* Attach (or detach, with null) the local conference this call is merged into.
|
|
742
|
+
*
|
|
743
|
+
* While attached, the wire carries the conference's MIXED track rather than
|
|
744
|
+
* this call's mic track, which changes what mute has to do — see `mute()`.
|
|
745
|
+
* Set by the conference owner (the softphone's merge action), not by the core.
|
|
746
|
+
*/
|
|
747
|
+
attachConference(conference: ConferenceSink | null): void;
|
|
644
748
|
startOutbound(): Promise<RTCSessionDescriptionInit>;
|
|
645
749
|
prepareAnswerForOffer(sdp: string): Promise<void>;
|
|
646
750
|
private buildAnswer;
|
|
751
|
+
private buildAnswerOnce;
|
|
647
752
|
acceptRemoteProvisionalAnswer(sdp: string): Promise<boolean>;
|
|
648
753
|
acceptRemoteAnswer(sdp: string): Promise<void>;
|
|
649
754
|
addRemoteCandidate(candidate: string, sdpMid: string | null, sdpMLineIndex: number | null): Promise<void>;
|
|
@@ -680,6 +785,11 @@ type Listener<K extends keyof PhoneEventMap> = PhoneEventMap[K];
|
|
|
680
785
|
declare class DialStackPhone {
|
|
681
786
|
isConnected: boolean;
|
|
682
787
|
readonly activeCalls: Call[];
|
|
788
|
+
/**
|
|
789
|
+
* A `connect()` is in flight but not yet connected. Consult alongside
|
|
790
|
+
* `isConnected`; a concurrent `connect()` joins the one in flight.
|
|
791
|
+
*/
|
|
792
|
+
get isConnecting(): boolean;
|
|
683
793
|
private token;
|
|
684
794
|
private apiBaseUrl;
|
|
685
795
|
private signalingUrl;
|
|
@@ -689,8 +799,12 @@ declare class DialStackPhone {
|
|
|
689
799
|
private ringback;
|
|
690
800
|
private createSignalingSocket;
|
|
691
801
|
private onAppResume;
|
|
802
|
+
private deferInboundCapture;
|
|
692
803
|
private transport;
|
|
693
804
|
private readonly handshake;
|
|
805
|
+
private connectInFlight;
|
|
806
|
+
private tokenMint;
|
|
807
|
+
private tokenMintedAt;
|
|
694
808
|
private iceServers;
|
|
695
809
|
private listeners;
|
|
696
810
|
private pendingOutbound;
|
|
@@ -749,6 +863,14 @@ declare class DialStackPhone {
|
|
|
749
863
|
*/
|
|
750
864
|
private syncDeviceChangeSubscription;
|
|
751
865
|
connect(): Promise<void>;
|
|
866
|
+
private openSession;
|
|
867
|
+
private tokenNeedsRefresh;
|
|
868
|
+
private tokenMsLeft;
|
|
869
|
+
private freshToken;
|
|
870
|
+
private mintToken;
|
|
871
|
+
private authenticateAfterMint;
|
|
872
|
+
private sendAuthenticate;
|
|
873
|
+
private awaitReconnect;
|
|
752
874
|
disconnect(): void;
|
|
753
875
|
/**
|
|
754
876
|
* Tear down and reconnect, re-running the `authenticate` handshake. The new
|
|
@@ -768,6 +890,8 @@ declare class DialStackPhone {
|
|
|
768
890
|
call(destination: string): Promise<Call>;
|
|
769
891
|
private startConsult;
|
|
770
892
|
private placeOutbound;
|
|
893
|
+
private endCallsOfLostSession;
|
|
894
|
+
private track;
|
|
771
895
|
getCall(callId: string): Call | undefined;
|
|
772
896
|
/**
|
|
773
897
|
* Subscribe to presence (BLF) for an explicit, bounded set of users. The
|
|
@@ -847,6 +971,15 @@ declare class DialStackPhone {
|
|
|
847
971
|
*/
|
|
848
972
|
clearEmergencyAddressRegisteredIp(id: string): Promise<void>;
|
|
849
973
|
private nextReqId;
|
|
974
|
+
/**
|
|
975
|
+
* Replace the token for the NEXT connect, off the connected path — for a host
|
|
976
|
+
* that mints its own tokens and must (re)connect with a fresh one (boot,
|
|
977
|
+
* push-wake), which `onTokenExpiring` (live-session only) can't cover. Refuses
|
|
978
|
+
* while connected/connecting: swapping there would desync the local token from
|
|
979
|
+
* the one the server enforces and the session would die silently at expiry — to
|
|
980
|
+
* swap a live session's token, `disconnect()` then `setToken()` before `connect()`.
|
|
981
|
+
*/
|
|
982
|
+
setToken(token: string): void;
|
|
850
983
|
private adoptToken;
|
|
851
984
|
private scheduleTokenRefresh;
|
|
852
985
|
private fireTokenRefresh;
|
|
@@ -1095,37 +1228,19 @@ interface AppearanceOptions$1 {
|
|
|
1095
1228
|
* the `appearance` prop flows through identically on both platforms.
|
|
1096
1229
|
*/
|
|
1097
1230
|
|
|
1098
|
-
/** Flat color set the Softphone paints from, after appearance is resolved. */
|
|
1099
1231
|
interface SoftphonePalette {
|
|
1100
|
-
/** Component background. */
|
|
1101
1232
|
background: string;
|
|
1102
|
-
/** Dial-pad key / control surface fill. */
|
|
1103
1233
|
surface: string;
|
|
1104
|
-
/** Surface fill while pressed/active. */
|
|
1105
1234
|
surfaceActive: string;
|
|
1106
|
-
/** Primary text (digits, destination, peer name). */
|
|
1107
1235
|
text: string;
|
|
1108
|
-
/** Secondary text (letter sub-labels, call state, hints). */
|
|
1109
1236
|
textSecondary: string;
|
|
1110
|
-
/** Hairline borders/dividers. */
|
|
1111
1237
|
border: string;
|
|
1112
|
-
/** Accent (links, focus ring, keypad/secondary control highlight). */
|
|
1113
1238
|
accent: string;
|
|
1114
|
-
/** Call / Answer (green) action color. */
|
|
1115
1239
|
success: string;
|
|
1116
|
-
/** Hang up / Decline (red) action color. */
|
|
1117
1240
|
danger: string;
|
|
1118
|
-
/** Warning / caution (amber) — used by the E911 banner. */
|
|
1119
1241
|
warning: string;
|
|
1120
|
-
/** Foreground used on top of accent/success/danger fills. */
|
|
1121
1242
|
onAccent: string;
|
|
1122
1243
|
}
|
|
1123
|
-
/**
|
|
1124
|
-
* Resolve the final palette from appearance: theme picks the base set, then any
|
|
1125
|
-
* `appearance.variables` overrides (the same keys other SDK components honor)
|
|
1126
|
-
* are layered on top. Unrecognized/absent variables fall back to the theme
|
|
1127
|
-
* default, so a bare `{ theme: 'dark' }` and a fully-specified palette both work.
|
|
1128
|
-
*/
|
|
1129
1244
|
declare function resolveSoftphonePalette(appearance?: AppearanceOptions$1): SoftphonePalette;
|
|
1130
1245
|
|
|
1131
1246
|
/**
|
|
@@ -1180,6 +1295,9 @@ declare const en: {
|
|
|
1180
1295
|
addCall: string;
|
|
1181
1296
|
addCallPlaceholder: string;
|
|
1182
1297
|
addCallSend: string;
|
|
1298
|
+
merge: string;
|
|
1299
|
+
split: string;
|
|
1300
|
+
conferenceLabel: string;
|
|
1183
1301
|
cancel: string;
|
|
1184
1302
|
stateTrying: string;
|
|
1185
1303
|
stateRinging: string;
|
|
@@ -2031,6 +2149,7 @@ declare const en: {
|
|
|
2031
2149
|
perUser: string;
|
|
2032
2150
|
perPhoneNumber: string;
|
|
2033
2151
|
perVoiceAiLocation: string;
|
|
2152
|
+
voiceAiPerLocationNote: string;
|
|
2034
2153
|
perMonth: string;
|
|
2035
2154
|
rateNotSet: string;
|
|
2036
2155
|
pendingRate: string;
|
|
@@ -2179,6 +2298,19 @@ interface SoftphoneContextBase {
|
|
|
2179
2298
|
completeAttendedTransfer: () => void;
|
|
2180
2299
|
cancelAttendedTransfer: () => void;
|
|
2181
2300
|
placeCall: (destination: string) => Promise<void>;
|
|
2301
|
+
/** The calls bridged into a local three-way conference (empty when not merged). */
|
|
2302
|
+
mergedCalls: Call[];
|
|
2303
|
+
isMerged: boolean;
|
|
2304
|
+
/** While merged, every remote party mixed — what the local user hears. */
|
|
2305
|
+
conferenceAudio: MediaStream | null;
|
|
2306
|
+
/** Whether merging is possible: two connected calls, no transfer, WebAudio present. */
|
|
2307
|
+
canMerge: boolean;
|
|
2308
|
+
mergeCalls: () => void;
|
|
2309
|
+
splitMerge: () => void;
|
|
2310
|
+
/** Hang up every leg of the conference (the merged UI shows one Hang up). */
|
|
2311
|
+
hangupConference: () => void;
|
|
2312
|
+
/** Hold or resume every leg of the conference together. */
|
|
2313
|
+
holdConference: (held: boolean) => void;
|
|
2182
2314
|
emergency: UseEmergencyBinding;
|
|
2183
2315
|
emergencyManagedByHost: boolean;
|
|
2184
2316
|
lastError: {
|
|
@@ -2193,52 +2325,476 @@ interface SoftphoneContextBase {
|
|
|
2193
2325
|
}
|
|
2194
2326
|
|
|
2195
2327
|
/**
|
|
2196
|
-
*
|
|
2197
|
-
*
|
|
2198
|
-
*
|
|
2199
|
-
|
|
2200
|
-
|
|
2328
|
+
* Its own module so `createPhone()` can use it without importing the provider
|
|
2329
|
+
* (and React with it): a push-woken headless runtime has no renderer, yet it
|
|
2330
|
+
* needs the same header or the ingress 403s its handshake.
|
|
2331
|
+
*/
|
|
2332
|
+
declare const nativeSignalingSocket: SignalingSocketFactory;
|
|
2333
|
+
|
|
2334
|
+
type AudioOutputKind = 'earpiece' | 'speaker' | 'wired' | 'bluetooth' | 'car' | 'airplay' | 'other';
|
|
2335
|
+
interface AudioOutput {
|
|
2336
|
+
id: string;
|
|
2337
|
+
kind: AudioOutputKind;
|
|
2338
|
+
name: string;
|
|
2339
|
+
}
|
|
2340
|
+
/**
|
|
2341
|
+
* Call-audio routing, implemented by a host that owns call audio
|
|
2342
|
+
* (`callAudio="host"`) over whatever its OS integration exposes.
|
|
2343
|
+
*
|
|
2344
|
+
* `current()` must be what the OS reports, never what was last requested: the
|
|
2345
|
+
* route also changes from the CallKit/Telecom UI, a headset, or a car, and the
|
|
2346
|
+
* softphone shows only this. It must return the same object until the route
|
|
2347
|
+
* changes.
|
|
2348
|
+
*/
|
|
2349
|
+
interface AudioOutputController {
|
|
2350
|
+
current(): AudioOutput | null;
|
|
2351
|
+
onChange(listener: (output: AudioOutput | null) => void): () => void;
|
|
2352
|
+
setSpeaker(on: boolean): Promise<void>;
|
|
2353
|
+
}
|
|
2354
|
+
interface UseAudioOutput {
|
|
2355
|
+
supported: boolean;
|
|
2356
|
+
current: AudioOutput | null;
|
|
2357
|
+
speakerOn: boolean;
|
|
2358
|
+
toggleSpeaker: () => void;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
type CallAudioOwner = 'sdk' | 'host';
|
|
2362
|
+
|
|
2363
|
+
/**
|
|
2364
|
+
* The OS call surface (CallKit on iOS, Telecom on Android) as the bridge needs
|
|
2365
|
+
* it — shaped by the bridge, not by any library. See the contract suite for
|
|
2366
|
+
* what an adapter must guarantee.
|
|
2367
|
+
*/
|
|
2368
|
+
/** OS-side call identity: the UUID CallKit/Telecom track the call under. */
|
|
2369
|
+
type OsSessionId = string;
|
|
2370
|
+
interface OsIncomingCall {
|
|
2371
|
+
callId: string;
|
|
2372
|
+
/** Caller handle as the OS should display/dial it. */
|
|
2373
|
+
from: string;
|
|
2374
|
+
displayName?: string | null;
|
|
2375
|
+
}
|
|
2376
|
+
interface OsOutgoingCall {
|
|
2377
|
+
/** The recipient handle the OS should display/dial. */
|
|
2378
|
+
to: string;
|
|
2379
|
+
displayName?: string | null;
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* Why the call ended, in the OS's vocabulary. `answeredElsewhere` /
|
|
2383
|
+
* `declinedElsewhere` make the OS dismiss the ring silently instead of logging
|
|
2384
|
+
* a missed call.
|
|
2385
|
+
*/
|
|
2386
|
+
type OsEndReason = 'remoteEnded' | 'unanswered' | 'answeredElsewhere' | 'declinedElsewhere' | 'failed' | 'unknown';
|
|
2387
|
+
/** Where the OS thinks a session is. `connecting` is answered but not yet reported connected. */
|
|
2388
|
+
type OsSessionStatus = 'ringing' | 'connecting' | 'connected' | 'ended';
|
|
2389
|
+
interface OsActiveSession {
|
|
2390
|
+
sessionId: OsSessionId;
|
|
2391
|
+
callId: string | null;
|
|
2392
|
+
/**
|
|
2393
|
+
* The OS's own view, for reconciling against the SDK call. Optional: an
|
|
2394
|
+
* adapter that can't read it leaves it out and the bridge doesn't reconcile
|
|
2395
|
+
* that field.
|
|
2396
|
+
*/
|
|
2397
|
+
status?: OsSessionStatus;
|
|
2398
|
+
held?: boolean;
|
|
2399
|
+
}
|
|
2400
|
+
interface OsCallEvents {
|
|
2401
|
+
/**
|
|
2402
|
+
* A call was reported to the OS — by native code before JS existed, or by
|
|
2403
|
+
* `reportIncoming`. `callId` is the call_id the session was created with (a
|
|
2404
|
+
* wake push's, or the one passed to `reportIncoming`): the bridge pairs the
|
|
2405
|
+
* session with its call by it, and ends a session that has none.
|
|
2406
|
+
*/
|
|
2407
|
+
incomingReported: (e: OsActiveSession) => void;
|
|
2408
|
+
/** The OS wants to PLACE a call — the user dialled from the platform's own UI
|
|
2409
|
+
* (Recents, Siri, dialer intent), not from the app. */
|
|
2410
|
+
callIntent: (e: {
|
|
2411
|
+
handle: string;
|
|
2412
|
+
}) => void;
|
|
2413
|
+
answer: (e: {
|
|
2414
|
+
sessionId: OsSessionId;
|
|
2415
|
+
}) => void;
|
|
2416
|
+
end: (e: {
|
|
2417
|
+
sessionId: OsSessionId;
|
|
2418
|
+
}) => void;
|
|
2419
|
+
setHeld: (e: {
|
|
2420
|
+
sessionId: OsSessionId;
|
|
2421
|
+
held: boolean;
|
|
2422
|
+
}) => void;
|
|
2423
|
+
setMuted: (e: {
|
|
2424
|
+
sessionId: OsSessionId;
|
|
2425
|
+
muted: boolean;
|
|
2426
|
+
}) => void;
|
|
2427
|
+
dtmf: (e: {
|
|
2428
|
+
sessionId: OsSessionId;
|
|
2429
|
+
digits: string;
|
|
2430
|
+
}) => void;
|
|
2431
|
+
/** iOS only in practice: CallKit gates audio, so WebRTC audio starts here. */
|
|
2432
|
+
audioSessionActivated: (e: {
|
|
2433
|
+
sessionId: OsSessionId | null;
|
|
2434
|
+
}) => void;
|
|
2435
|
+
}
|
|
2436
|
+
type OsCallEventName = keyof OsCallEvents;
|
|
2437
|
+
interface OsCallAdapter {
|
|
2438
|
+
/** Resolves to the OS-assigned session id (the OS assigns, never the caller). */
|
|
2439
|
+
reportIncoming(call: OsIncomingCall): Promise<OsSessionId>;
|
|
2440
|
+
/**
|
|
2441
|
+
* Report an outbound call so the OS holds a live session — on Android that
|
|
2442
|
+
* starts the foreground service keeping the process and call alive when
|
|
2443
|
+
* backgrounded, which an outbound call otherwise lacks.
|
|
2444
|
+
*/
|
|
2445
|
+
reportOutgoing(call: OsOutgoingCall): Promise<OsSessionId>;
|
|
2446
|
+
/** The call the OS is showing as incoming/dialling is now connected. */
|
|
2447
|
+
reportConnected(sessionId: OsSessionId): Promise<void>;
|
|
2448
|
+
/** Must not re-emit `end` — the bridge is telling the OS, not the other way round. */
|
|
2449
|
+
reportEnded(sessionId: OsSessionId, reason: OsEndReason): Promise<void>;
|
|
2450
|
+
/** Remote-initiated state only; the OS already knows about its own actions. */
|
|
2451
|
+
setHeld(sessionId: OsSessionId, held: boolean): Promise<void>;
|
|
2452
|
+
setMuted(sessionId: OsSessionId, muted: boolean): Promise<void>;
|
|
2453
|
+
/** The session the OS currently treats as active, if any. */
|
|
2454
|
+
getActiveSession(): Promise<OsActiveSession | null>;
|
|
2455
|
+
/**
|
|
2456
|
+
* The app's own Answer/End buttons go through the OS too, so every answer —
|
|
2457
|
+
* lock screen, notification, car, watch, in-app — arrives as the same
|
|
2458
|
+
* `answer`/`end` event and there is one code path, not two.
|
|
2459
|
+
*/
|
|
2460
|
+
answer(sessionId: OsSessionId): Promise<void>;
|
|
2461
|
+
end(sessionId: OsSessionId): Promise<void>;
|
|
2462
|
+
on<K extends OsCallEventName>(event: K, listener: OsCallEvents[K]): () => void;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
/**
|
|
2466
|
+
* Where the bridge schedules its deadlines and retries. The JS timers are the
|
|
2467
|
+
* default, but React Native on Android pauses them in an app that is
|
|
2468
|
+
* backgrounded without being killed, even while a headless task runs. A wake
|
|
2469
|
+
* whose call never arrives (answered or cancelled before the app re-registered)
|
|
2470
|
+
* then waits on a deadline that never fires, and the OS keeps ringing. An
|
|
2471
|
+
* integration that can schedule natively passes timers here.
|
|
2472
|
+
*/
|
|
2473
|
+
interface BridgeTimers {
|
|
2474
|
+
setTimeout(fn: () => void, ms: number): unknown;
|
|
2475
|
+
clearTimeout(handle: unknown): void;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
/**
|
|
2479
|
+
* "Keep this JS runtime alive" as a promise the platform can await.
|
|
2480
|
+
*
|
|
2481
|
+
* Android's HeadlessJsTaskService tears the runtime down the moment the task
|
|
2482
|
+
* promise settles, so a wake holds this from the instant the task starts until
|
|
2483
|
+
* live calls hit zero. What the hold actually covers is the gap before the call
|
|
2484
|
+
* library's foreground service exists — once that is up it holds the process,
|
|
2485
|
+
* and a ceiling reached mid-call ends only this task, not the call (measured: a
|
|
2486
|
+
* 60s ring survived the then-45s ceiling untouched).
|
|
2487
|
+
*
|
|
2488
|
+
* So the ceiling is a bound on a wake that never delivers, not a correctness
|
|
2489
|
+
* guarantee. It is set to the same 300s as the max park/ring, the service's task
|
|
2490
|
+
* timeout, the OS answer deadline and the library's incoming-call timeout purely
|
|
2491
|
+
* so the numbers don't contradict each other; keep them aligned.
|
|
2201
2492
|
*/
|
|
2493
|
+
declare class RuntimeHold {
|
|
2494
|
+
private readonly ceilingMs;
|
|
2495
|
+
private readonly timers;
|
|
2496
|
+
private release;
|
|
2497
|
+
private waiting;
|
|
2498
|
+
constructor(ceilingMs?: number, timers?: BridgeTimers);
|
|
2499
|
+
acquire(): Promise<void>;
|
|
2500
|
+
releaseNow(): void;
|
|
2501
|
+
/** Non-null while a hold is in flight — the reliable "a wake is mid-flight" signal. */
|
|
2502
|
+
pending(): Promise<void> | null;
|
|
2503
|
+
private settle;
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
/**
|
|
2507
|
+
* What the bridge needs from a call. Overloads rather than a generic `on`: a
|
|
2508
|
+
* generic over a narrower event map is not assignable from the SDK's wider one.
|
|
2509
|
+
*/
|
|
2510
|
+
interface BridgeCall {
|
|
2511
|
+
readonly id: string;
|
|
2512
|
+
readonly from: string;
|
|
2513
|
+
readonly fromName: string | null;
|
|
2514
|
+
readonly state: CallState;
|
|
2515
|
+
readonly isMuted: boolean;
|
|
2516
|
+
answer(): void;
|
|
2517
|
+
reject(reason?: 'busy' | 'decline'): void;
|
|
2518
|
+
hangup(): void;
|
|
2519
|
+
hold(): void;
|
|
2520
|
+
resume(): void;
|
|
2521
|
+
mute(): void;
|
|
2522
|
+
unmute(): void;
|
|
2523
|
+
sendDtmf(digits: string): void;
|
|
2524
|
+
on(event: 'answered' | 'resumed', handler: () => void): void;
|
|
2525
|
+
on(event: 'held', handler: (by: HeldBy) => void): void;
|
|
2526
|
+
on(event: 'ended', handler: (reason: CallEndReason) => void): void;
|
|
2527
|
+
off(event: 'held' | 'resumed' | 'ended', handler: (...args: never[]) => void): void;
|
|
2528
|
+
}
|
|
2529
|
+
interface BridgePhoneError {
|
|
2530
|
+
code?: string;
|
|
2531
|
+
message?: string;
|
|
2532
|
+
fatal?: boolean;
|
|
2533
|
+
callId?: string | null;
|
|
2534
|
+
}
|
|
2535
|
+
/** What the bridge needs from the phone — `DialStackPhone` satisfies it. */
|
|
2536
|
+
interface BridgePhone {
|
|
2537
|
+
readonly isConnected: boolean;
|
|
2538
|
+
connect(): Promise<void>;
|
|
2539
|
+
disconnect(): void;
|
|
2540
|
+
setToken(token: string): void;
|
|
2541
|
+
/** Place an outbound call. The bridge reports it to the OS around this. */
|
|
2542
|
+
call(destination: string): Promise<BridgeCall>;
|
|
2543
|
+
on(event: 'connected' | 'reconnected', handler: () => void): void;
|
|
2544
|
+
on(event: 'disconnected', handler: (error?: unknown) => void): void;
|
|
2545
|
+
on(event: 'reconnecting', handler: (attempt: number, delayMs: number) => void): void;
|
|
2546
|
+
on(event: 'incoming', handler: (call: BridgeCall) => void): void;
|
|
2547
|
+
on(event: 'error', handler: (err: BridgePhoneError) => void): void;
|
|
2548
|
+
}
|
|
2549
|
+
type AppLifecycleState = 'active' | 'background' | 'inactive' | 'unknown' | 'extension';
|
|
2550
|
+
/** Foreground/background as a port so the bridge has no react-native import. */
|
|
2551
|
+
interface AppLifecycle {
|
|
2552
|
+
onChange(listener: (state: AppLifecycleState) => void): () => void;
|
|
2553
|
+
}
|
|
2554
|
+
interface NativeCallBridgeOptions {
|
|
2555
|
+
phone: BridgePhone;
|
|
2556
|
+
os: OsCallAdapter;
|
|
2557
|
+
lifecycle?: AppLifecycle;
|
|
2558
|
+
hold?: RuntimeHold;
|
|
2559
|
+
log?: (message: string) => void;
|
|
2560
|
+
/** From the OS report until `connect()` resolves: a socket that never comes up. */
|
|
2561
|
+
registrationDeadlineMs?: number;
|
|
2562
|
+
/**
|
|
2563
|
+
* From `connect()` resolving until `incoming`. The proxy resumes a parked INVITE
|
|
2564
|
+
* on REGISTER or within its 1s poll, so after this nothing is coming.
|
|
2565
|
+
*/
|
|
2566
|
+
deliveryDeadlineMs?: number;
|
|
2567
|
+
/** An OS answer held for a call that has not arrived yet is stale past this. */
|
|
2568
|
+
answerTtlMs?: number;
|
|
2569
|
+
/**
|
|
2570
|
+
* How many calls the integration puts on the OS call screen at once (default 1).
|
|
2571
|
+
* A second concurrent call past the cap is NOT reported to the OS but still
|
|
2572
|
+
* rings and is answerable in-app; it just gets no OS session. Calls pair with
|
|
2573
|
+
* sessions by call_id, so any value is safe; raising it is a product choice
|
|
2574
|
+
* (OS call-waiting) and needs an adapter whose library holds several sessions.
|
|
2575
|
+
*/
|
|
2576
|
+
maxOsCalls?: number;
|
|
2577
|
+
/** Defaults to the JS timers; see `BridgeTimers` for when to pass native ones. */
|
|
2578
|
+
timers?: BridgeTimers;
|
|
2579
|
+
now?: () => number;
|
|
2580
|
+
}
|
|
2581
|
+
declare function toOsEndReason(reason: CallEndReason): OsEndReason;
|
|
2582
|
+
/**
|
|
2583
|
+
* The bidirectional bridge between the OS call surface and the SDK (OS↔SDK
|
|
2584
|
+
* answer/end/mute/hold/DTMF and lifecycle), and the only thing that subscribes to
|
|
2585
|
+
* either. Works with no renderer: a push-woken headless runtime starts it like
|
|
2586
|
+
* the app window does.
|
|
2587
|
+
*
|
|
2588
|
+
* ## Pairing a session with a call
|
|
2589
|
+
*
|
|
2590
|
+
* A call has one `call_id` everywhere: on the socket, in the wake webhook and on
|
|
2591
|
+
* the INVITE a wake resumes. The OS mints its own session id at setup, and the
|
|
2592
|
+
* bridge links it to that `call_id` (`ids`) the moment it knows both: when it
|
|
2593
|
+
* reports a call itself, when an outbound call is placed, and when the OS
|
|
2594
|
+
* reports a wake push's session (`incomingReported`, carrying the `call_id` the
|
|
2595
|
+
* push delivered). Every lookup goes through that link; none guesses by
|
|
2596
|
+
* position. A second session for a `call_id` already linked is the same call
|
|
2597
|
+
* shown twice, and is ended.
|
|
2598
|
+
*
|
|
2599
|
+
* The contract this puts on the integrator: the wake push carries the
|
|
2600
|
+
* webhook's `call_id`, and the OS adapter reports it on `incomingReported`.
|
|
2601
|
+
*/
|
|
2602
|
+
declare class NativeCallBridge {
|
|
2603
|
+
readonly hold: RuntimeHold;
|
|
2604
|
+
private readonly timers;
|
|
2605
|
+
private readonly phone;
|
|
2606
|
+
private readonly os;
|
|
2607
|
+
private readonly lifecycle;
|
|
2608
|
+
private readonly log;
|
|
2609
|
+
private readonly registrationDeadlineMs;
|
|
2610
|
+
private readonly deliveryDeadlineMs;
|
|
2611
|
+
private readonly answerTtlMs;
|
|
2612
|
+
private readonly maxOsCalls;
|
|
2613
|
+
private readonly now;
|
|
2614
|
+
private started;
|
|
2615
|
+
private appState;
|
|
2616
|
+
private reconnecting;
|
|
2617
|
+
private readonly ids;
|
|
2618
|
+
private readonly calls;
|
|
2619
|
+
private readonly callErrorWaiters;
|
|
2620
|
+
private readonly ending;
|
|
2621
|
+
private readonly changingHold;
|
|
2622
|
+
private reconcileTimer;
|
|
2623
|
+
/** Outbound calls still being placed, so not yet in `calls`. */
|
|
2624
|
+
private placingOutbound;
|
|
2625
|
+
/** Calls this app placed. For these `answered` means the far end picked up. */
|
|
2626
|
+
private readonly outboundCalls;
|
|
2627
|
+
/** OS sessions created by a native wake report, before their call arrives on the socket. */
|
|
2628
|
+
private readonly wakeSessions;
|
|
2629
|
+
private readonly reportedToOs;
|
|
2630
|
+
private readonly connectedReported;
|
|
2631
|
+
/**
|
|
2632
|
+
* An OS answer/decline that landed before the SDK had the call. On a wake the
|
|
2633
|
+
* user taps within ~200ms while connect() takes seconds, so it's applied on arrival.
|
|
2634
|
+
*/
|
|
2635
|
+
private readonly pendingAnswer;
|
|
2636
|
+
private readonly pendingDecline;
|
|
2637
|
+
private readonly answerDeadlines;
|
|
2638
|
+
private readonly suppressHoldEcho;
|
|
2639
|
+
private readonly wakeDeadlines;
|
|
2640
|
+
private readonly reportingCalls;
|
|
2641
|
+
private readonly traceLines;
|
|
2642
|
+
private traceSnapshot;
|
|
2643
|
+
private readonly traceListeners;
|
|
2644
|
+
constructor(options: NativeCallBridgeOptions);
|
|
2645
|
+
/** Idempotent. Call from the app entry on every path (window and headless). */
|
|
2646
|
+
start(): void;
|
|
2647
|
+
/**
|
|
2648
|
+
* Resolving means the SIP AOR is REGISTERed, so a parked INVITE can reach us.
|
|
2649
|
+
* The phone joins a connect already in flight, so this never starts a second
|
|
2650
|
+
* one. Never `reconnect()` here — that disposes live calls.
|
|
2651
|
+
*
|
|
2652
|
+
* `forWake` is the corpse-proofing path: when the OS freezes a backgrounded app
|
|
2653
|
+
* it can kill the WebSocket with no close frame reaching JS, so no `disconnected`
|
|
2654
|
+
* fires and `isConnected` still reads "connected" over a dead socket. Trusting them never re-REGISTERs the AOR and the parked call is never
|
|
2655
|
+
* re-forked. So a wake with NO live call sheds whatever socket state we hold and
|
|
2656
|
+
* reconnects for real. A wake WITH a live call skips this: that call proves the
|
|
2657
|
+
* socket and a teardown would drop it. A connect still in flight is joined, not
|
|
2658
|
+
* shed: it has no stale socket to distrust, and its own timeouts bound it.
|
|
2659
|
+
*/
|
|
2660
|
+
ensureConnected(forWake?: boolean): Promise<void>;
|
|
2661
|
+
/** In-app Answer, routed through the OS so it converges on the one answer path. */
|
|
2662
|
+
answer(callId: string): void;
|
|
2663
|
+
/** In-app End/Decline, via the same convergence. */
|
|
2664
|
+
end(callId: string): void;
|
|
2665
|
+
/**
|
|
2666
|
+
* Place an outbound call, reported to the OS FIRST so on Android the foreground
|
|
2667
|
+
* service starts and the call survives backgrounding mid-dial (otherwise the OS
|
|
2668
|
+
* reaps the process and drops the call). If placement fails we end the session
|
|
2669
|
+
* we opened; from then on it rides the same `watchCall` lifecycle.
|
|
2670
|
+
*
|
|
2671
|
+
* An OS-initiated dial (`callIntent`) comes through here too, so both
|
|
2672
|
+
* directions converge on one path.
|
|
2673
|
+
*/
|
|
2674
|
+
call(destination: string, displayName?: string | null): Promise<BridgeCall>;
|
|
2675
|
+
/** Ring buffer for a debug view; one-way, `useSyncExternalStore`-shaped. */
|
|
2676
|
+
getTrace(): readonly string[];
|
|
2677
|
+
subscribeTrace(listener: () => void): () => void;
|
|
2678
|
+
private subscribePhone;
|
|
2679
|
+
private onIncoming;
|
|
2680
|
+
private applyHeld;
|
|
2681
|
+
private reportIncoming;
|
|
2682
|
+
private endDuplicateSession;
|
|
2683
|
+
/**
|
|
2684
|
+
* An OS answer we could not act on yet. The OS holds its answer request open
|
|
2685
|
+
* meanwhile — CallKit keeps the incoming UI up, Telecom leaves the call in
|
|
2686
|
+
* CONNECTING — so if no call ever arrives to consume it, say so rather than
|
|
2687
|
+
* leaving the phone ringing until the adapter's own far longer deadline.
|
|
2688
|
+
*
|
|
2689
|
+
* Handle-stored and cleared by `forgetSession`, like the wake deadlines. It
|
|
2690
|
+
* has to be: session ids are recycled, so a timer left armed past its own
|
|
2691
|
+
* session fires against a LATER tap on the same id and reports a live call
|
|
2692
|
+
* failed. `armedAt` is the second half of that — it pins the timer to the one
|
|
2693
|
+
* tap it was armed for even if the map was refilled meanwhile.
|
|
2694
|
+
*/
|
|
2695
|
+
private armAnswerDeadline;
|
|
2696
|
+
private dropHeldAnswer;
|
|
2697
|
+
private answerCall;
|
|
2698
|
+
private reportConnectedReliably;
|
|
2699
|
+
private watchCall;
|
|
2700
|
+
private reportHold;
|
|
2701
|
+
/**
|
|
2702
|
+
* Hang up, and make sure the call ends. The SDK sends one frame; this retries
|
|
2703
|
+
* one the server never answers and, once the attempts run out, ends the call
|
|
2704
|
+
* here so the OS and the app don't keep a call nothing can reach.
|
|
2705
|
+
*
|
|
2706
|
+
* An error reply means the server has no such call, and the SDK ends it. A
|
|
2707
|
+
* closed socket ends it too, through the lost session.
|
|
2708
|
+
*/
|
|
2709
|
+
private endCall;
|
|
2710
|
+
/**
|
|
2711
|
+
* Hold or resume, confirmed. Resolves once the call's held/resumed arrives.
|
|
2712
|
+
*
|
|
2713
|
+
* The far end refusing on state (`invalid_message`) or a closed socket rejects
|
|
2714
|
+
* at once and leaves the call alone. A failure on the server's side (far end
|
|
2715
|
+
* unreachable) or no reply is retried; once the attempts run out the in-dialog
|
|
2716
|
+
* route is dead and every other action would fail the same way, so the call is
|
|
2717
|
+
* ended and the promise rejects.
|
|
2718
|
+
*/
|
|
2719
|
+
private changeHold;
|
|
2720
|
+
private confirmHold;
|
|
2721
|
+
private attempt;
|
|
2722
|
+
/** The pause after a failed attempt; none after the last, which has nothing to wait for. */
|
|
2723
|
+
private backoffAfter;
|
|
2724
|
+
private abandonCall;
|
|
2725
|
+
private teardownCall;
|
|
2726
|
+
private reportEndedReliably;
|
|
2727
|
+
private subscribeOs;
|
|
2728
|
+
private onIncomingReported;
|
|
2729
|
+
private armWakeDeadline;
|
|
2730
|
+
private forgetSession;
|
|
2731
|
+
private wakeInFlight;
|
|
2732
|
+
/** How many OS-reported calls are still live — the OS's session count. */
|
|
2733
|
+
private liveOsCallCount;
|
|
2734
|
+
private linkOsSession;
|
|
2735
|
+
/**
|
|
2736
|
+
* Check, shortly after a change, that the OS shows what the call is.
|
|
2737
|
+
*
|
|
2738
|
+
* Every OS report above is retried when it fails, but a report the OS takes
|
|
2739
|
+
* and then doesn't act on goes unseen: iOS accepts a connected report for an
|
|
2740
|
+
* incoming call and keeps it ringing. So compare what the OS says it shows
|
|
2741
|
+
* with the call, and correct the OS; the call's state is the server's, and
|
|
2742
|
+
* the OS follows it.
|
|
2743
|
+
*
|
|
2744
|
+
* Only the session the OS calls active is read, since that is all the port
|
|
2745
|
+
* offers. It is the one on screen, and with the default of one OS call it is
|
|
2746
|
+
* every session there is.
|
|
2747
|
+
*/
|
|
2748
|
+
private scheduleReconcile;
|
|
2749
|
+
private reconcile;
|
|
2750
|
+
private reconcileConnected;
|
|
2751
|
+
private reconcileHeld;
|
|
2752
|
+
private callForSession;
|
|
2753
|
+
private subscribeLifecycle;
|
|
2754
|
+
private dropRegistrationIfIdle;
|
|
2755
|
+
private trace;
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
/** SoftphoneProvider (React Native), same API as the web provider. */
|
|
2202
2759
|
|
|
2203
2760
|
type AppearanceOptions = Parameters<typeof resolveSoftphonePalette>[0];
|
|
2204
2761
|
type ConnectionState = SoftphoneConnectionState;
|
|
2205
2762
|
interface SoftphoneProviderProps {
|
|
2763
|
+
/**
|
|
2764
|
+
* Adopt an existing phone instead of constructing one from `token`, for a
|
|
2765
|
+
* native call surface where a call can exist before any phone does. Neither
|
|
2766
|
+
* connected nor disconnected by the provider.
|
|
2767
|
+
*/
|
|
2768
|
+
existingPhone?: DialStackPhone | null;
|
|
2206
2769
|
/** WebRTC user session token. */
|
|
2207
2770
|
token: string;
|
|
2208
2771
|
/**
|
|
2209
|
-
* Persistence for the selected E911 address id
|
|
2210
|
-
*
|
|
2211
|
-
*
|
|
2212
|
-
* defaulted safely), so the host supplies a small `PlatformStorage` adapter —
|
|
2213
|
-
* typically MMKV- or AsyncStorage-backed. See the example apps for reference
|
|
2214
|
-
* adapters; a synchronous in-memory adapter is also valid if you don't need
|
|
2215
|
-
* persistence. It only stores the E911 id, nothing sensitive.
|
|
2772
|
+
* Persistence for the selected E911 address id. REQUIRED on RN: the SDK takes
|
|
2773
|
+
* no persistence dependency of its own. A synchronous in-memory adapter is
|
|
2774
|
+
* valid if you don't need persistence.
|
|
2216
2775
|
*/
|
|
2217
2776
|
storage: PlatformStorage;
|
|
2218
2777
|
/** API base URL (defaults to the SDK's production endpoint). */
|
|
2219
2778
|
apiBaseUrl?: string;
|
|
2220
2779
|
/**
|
|
2221
|
-
* Called shortly before the session token expires
|
|
2222
|
-
*
|
|
2223
|
-
*
|
|
2224
|
-
* the SDK keeps the (still-valid) connection open and surfaces an `error`.
|
|
2780
|
+
* Called shortly before the session token expires. Return a fresh token; the
|
|
2781
|
+
* SDK delivers it in-band with no reconnect. On reject the SDK keeps the
|
|
2782
|
+
* still-valid connection and surfaces an `error`.
|
|
2225
2783
|
*/
|
|
2226
2784
|
onTokenExpiring?: () => Promise<string>;
|
|
2227
|
-
/**
|
|
2228
|
-
*
|
|
2785
|
+
/** E911 address id to present on connect; when supplied the host manages E911
|
|
2786
|
+
* and the built-in prompt is disabled. */
|
|
2229
2787
|
emergencyAddressId?: string;
|
|
2230
2788
|
/**
|
|
2231
|
-
*
|
|
2232
|
-
*
|
|
2233
|
-
* that calls this to prefill the address fields; the host owns the location
|
|
2234
|
-
* permission prompt, geolocation, and reverse-geocoding. Omit for manual entry.
|
|
2789
|
+
* Device-location source for the E911 form's "Use my current location" action;
|
|
2790
|
+
* the host owns the permission prompt, geolocation, and reverse-geocoding.
|
|
2235
2791
|
*/
|
|
2236
2792
|
locationProvider?: () => Promise<EmergencyAddressInput>;
|
|
2237
2793
|
/** Connect automatically once mounted (default: true). */
|
|
2238
2794
|
autoConnect?: boolean;
|
|
2239
|
-
/** Theming — the shared appearance surface
|
|
2795
|
+
/** Theming — the shared appearance surface. */
|
|
2240
2796
|
appearance?: AppearanceOptions;
|
|
2241
|
-
/** Locale for UI strings (defaults to English)
|
|
2797
|
+
/** Locale for UI strings (defaults to English). */
|
|
2242
2798
|
locale?: Locale;
|
|
2243
2799
|
/** Default country for number formatting. */
|
|
2244
2800
|
defaultCountry?: CountryCode;
|
|
@@ -2246,6 +2802,9 @@ interface SoftphoneProviderProps {
|
|
|
2246
2802
|
state: ConnectionState;
|
|
2247
2803
|
}) => void;
|
|
2248
2804
|
onIncomingCall?: (event: {
|
|
2805
|
+
/** Handle for `callActionsFor()` / `answerCall()`, and what a host bridging
|
|
2806
|
+
* to a native call UI binds its OS session to. */
|
|
2807
|
+
callId: string;
|
|
2249
2808
|
from: string;
|
|
2250
2809
|
fromName: string | null;
|
|
2251
2810
|
}) => void;
|
|
@@ -2260,17 +2819,39 @@ interface SoftphoneProviderProps {
|
|
|
2260
2819
|
code: string;
|
|
2261
2820
|
message: string;
|
|
2262
2821
|
}) => void;
|
|
2822
|
+
/**
|
|
2823
|
+
* The call bridge, when this app reports calls to the OS. Outbound placement
|
|
2824
|
+
* then goes through it, so the OS learns about the call BEFORE it is dialled —
|
|
2825
|
+
* on Android that report is what starts the foreground service, without which
|
|
2826
|
+
* backgrounding mid-dial lets the OS reap the process and drop the call.
|
|
2827
|
+
* Answering in-app goes through it too, so the OS answers before the call
|
|
2828
|
+
* starts media (on iOS that is what gives the call its audio).
|
|
2829
|
+
*/
|
|
2830
|
+
bridge?: Pick<NativeCallBridge, 'call' | 'answer'>;
|
|
2831
|
+
/**
|
|
2832
|
+
* Who owns call audio: ringing, outbound ringback, the audio session and the
|
|
2833
|
+
* proximity sensor. `'sdk'` (the default): the provider does all of it.
|
|
2834
|
+
* `'host'`: the provider does none of it, for an app that reports calls to
|
|
2835
|
+
* CallKit/Telecom itself, which ring the phone and own the session. Doing any
|
|
2836
|
+
* of it on top of the OS breaks capture. Always `'host'` with a `bridge`.
|
|
2837
|
+
*/
|
|
2838
|
+
callAudio?: CallAudioOwner;
|
|
2839
|
+
/**
|
|
2840
|
+
* The output route, for a host that owns call audio: without it the in-call
|
|
2841
|
+
* speaker button is disabled, since only the OS integration can move the
|
|
2842
|
+
* route. Ignored unless call audio is `'host'`.
|
|
2843
|
+
*/
|
|
2844
|
+
audioOutput?: AudioOutputController;
|
|
2263
2845
|
children: React.ReactNode;
|
|
2264
2846
|
}
|
|
2265
|
-
/**
|
|
2266
|
-
*
|
|
2267
|
-
* `locationProvider`. (`palette` is on the base — computed identically to web.)
|
|
2268
|
-
*/
|
|
2847
|
+
/** The native softphone context: the shared base plus the native-only
|
|
2848
|
+
* `locationProvider`. */
|
|
2269
2849
|
interface SoftphoneContextValue extends SoftphoneContextBase {
|
|
2270
2850
|
/** Host-supplied device-location source for the E911 form, or undefined. */
|
|
2271
2851
|
locationProvider: (() => Promise<EmergencyAddressInput>) | undefined;
|
|
2852
|
+
audio: UseAudioOutput;
|
|
2272
2853
|
}
|
|
2273
|
-
declare function SoftphoneProvider({ token, storage, apiBaseUrl, onTokenExpiring, emergencyAddressId, locationProvider, autoConnect, appearance, locale, defaultCountry, onConnectionStateChange, onIncomingCall, onCallStarted, onCallEnded, onError, children, }: SoftphoneProviderProps): React.JSX.Element;
|
|
2854
|
+
declare function SoftphoneProvider({ existingPhone, token, storage, apiBaseUrl, onTokenExpiring, emergencyAddressId, locationProvider, autoConnect, appearance, locale, defaultCountry, onConnectionStateChange, onIncomingCall, onCallStarted, onCallEnded, onError, bridge, callAudio: requestedCallAudio, audioOutput, children, }: SoftphoneProviderProps): React.JSX.Element;
|
|
2274
2855
|
/** Access the full softphone context. Throws when used outside the provider. */
|
|
2275
2856
|
declare function useSoftphone(): SoftphoneContextValue;
|
|
2276
2857
|
/** The active foreground call + its actions (build-your-own convenience). */
|
|
@@ -2278,6 +2859,8 @@ declare function useActiveCall(): {
|
|
|
2278
2859
|
activeCall: Call | null;
|
|
2279
2860
|
actions: UseCallActions;
|
|
2280
2861
|
};
|
|
2862
|
+
/** The call-audio output route and the speaker toggle. */
|
|
2863
|
+
declare function useAudioOutput(): UseAudioOutput;
|
|
2281
2864
|
/** The currently-ringing inbound call, or null. */
|
|
2282
2865
|
declare function useIncomingCall(): Call | null;
|
|
2283
2866
|
|
|
@@ -2317,10 +2900,10 @@ declare function Softphone({ autoFocusDestination }: SoftphoneProps): React.JSX.
|
|
|
2317
2900
|
*/
|
|
2318
2901
|
|
|
2319
2902
|
interface DialPadProps {
|
|
2320
|
-
/** Autofocus the destination field on mount. */
|
|
2321
2903
|
autoFocusDestination?: boolean;
|
|
2904
|
+
compact?: boolean;
|
|
2322
2905
|
}
|
|
2323
|
-
declare function DialPad({ autoFocusDestination }: DialPadProps): React.JSX.Element;
|
|
2906
|
+
declare function DialPad({ autoFocusDestination, compact, }: DialPadProps): React.JSX.Element;
|
|
2324
2907
|
|
|
2325
2908
|
/**
|
|
2326
2909
|
* IncomingCall / IncomingStack (RN) — the ringing inbound UI.
|
|
@@ -2363,5 +2946,38 @@ declare function OngoingCall(): React.JSX.Element | null;
|
|
|
2363
2946
|
|
|
2364
2947
|
declare function EmergencyBanner(): React.JSX.Element | null;
|
|
2365
2948
|
|
|
2366
|
-
|
|
2367
|
-
|
|
2949
|
+
type CreatePhoneOptions = Omit<PhoneOptions, 'createSignalingSocket' | 'deferInboundCapture'>;
|
|
2950
|
+
/**
|
|
2951
|
+
* The process's one phone.
|
|
2952
|
+
*
|
|
2953
|
+
* Module scope is the correct scope, not a shortcut: a push wake and the UI run
|
|
2954
|
+
* in the same JS runtime but enter from different places (a headless task, the
|
|
2955
|
+
* app window), so this is a plain shared reference. Two phones would register
|
|
2956
|
+
* the same SIP AOR twice and the call would go to whichever registered last
|
|
2957
|
+
* while the UI watched the other — a silent failure, so a second construction
|
|
2958
|
+
* fails loudly instead. Construct eagerly at app entry: `new DialStackPhone()`
|
|
2959
|
+
* does no I/O (only `connect()` does).
|
|
2960
|
+
*/
|
|
2961
|
+
declare function createPhone(options: CreatePhoneOptions): DialStackPhone;
|
|
2962
|
+
declare function getPhone(): DialStackPhone | null;
|
|
2963
|
+
/** For consumers that must not be the one to create it (the UI). */
|
|
2964
|
+
declare function requirePhone(): DialStackPhone;
|
|
2965
|
+
|
|
2966
|
+
declare const appLifecycle: AppLifecycle;
|
|
2967
|
+
declare const DEFAULT_WAKE_TASK = "DialStackWake";
|
|
2968
|
+
/**
|
|
2969
|
+
* Android's wake: the adapter's native push service reports the call to Telecom
|
|
2970
|
+
* with no JS, and something must then start a runtime so the SDK can
|
|
2971
|
+
* re-REGISTER and receive the parked INVITE. That is this headless task; the
|
|
2972
|
+
* app's HeadlessJsTaskService names it (`taskName`).
|
|
2973
|
+
*
|
|
2974
|
+
* iOS has no equivalent — PushKit launches the process and React Native starts
|
|
2975
|
+
* normally, so the app entry's `bridge.start()` is the whole wake path.
|
|
2976
|
+
*
|
|
2977
|
+
* A single file with a runtime branch rather than `.android.ts`/`.ios.ts`: the
|
|
2978
|
+
* package ships as one bundle, so Metro's platform extensions cannot apply.
|
|
2979
|
+
*/
|
|
2980
|
+
declare function registerWakeTask(bridge: NativeCallBridge, taskName?: string): void;
|
|
2981
|
+
|
|
2982
|
+
export { Call, DEFAULT_WAKE_TASK, DialPad, EmergencyBanner, IncomingCall, NativeCallBridge, OngoingCall, RuntimeHold, Softphone, SoftphoneProvider, appLifecycle, createPhone, getPhone, nativeSignalingSocket, registerWakeTask, requirePhone, toOsEndReason, useActiveCall, useAudioOutput, useIncomingCall, useSoftphone };
|
|
2983
|
+
export type { AppLifecycle, AppLifecycleState, AudioOutput, AudioOutputController, AudioOutputKind, BridgeCall, BridgePhone, BridgePhoneError, BridgeTimers, CallEndReason, CallState, ConnectionState, CreatePhoneOptions, DialPadProps, EmergencyAddressInput, NativeCallBridgeOptions, OsActiveSession, OsCallAdapter, OsCallEventName, OsCallEvents, OsEndReason, OsIncomingCall, OsOutgoingCall, OsSessionId, OsSessionStatus, PlatformStorage, SoftphoneContextValue, SoftphoneProps, SoftphoneProviderProps, UseAudioOutput, UseCallActions };
|