@dialstack/sdk-native 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -10
- package/dist/index.d.ts +680 -55
- package/dist/index.js +3219 -608
- 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,26 @@ 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
|
+
private reportAnswerFailure;
|
|
650
|
+
/** Set by the owner to surface a late mic failure, since `Call` has no error event. */
|
|
651
|
+
private reportCaptureFailure;
|
|
652
|
+
onCaptureFailure(report: (e: unknown) => void): void;
|
|
594
653
|
whenLocalMediaReady(): Promise<void>;
|
|
595
654
|
private acquireLocalMedia;
|
|
596
655
|
/** The selected mic, or null for the OS default. */
|
|
@@ -606,7 +665,7 @@ declare class Call {
|
|
|
606
665
|
/** Route this call's ringback tone to `deviceId` (null = OS default). */
|
|
607
666
|
setAudioOutputDevice(deviceId: string | null): void;
|
|
608
667
|
private watchCaptureTrack;
|
|
609
|
-
private
|
|
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;
|
|
@@ -855,6 +988,7 @@ declare class DialStackPhone {
|
|
|
855
988
|
private fetchIceServers;
|
|
856
989
|
private handleMessage;
|
|
857
990
|
private handleIncoming;
|
|
991
|
+
private answerFailureReporter;
|
|
858
992
|
private emit;
|
|
859
993
|
}
|
|
860
994
|
|
|
@@ -1095,37 +1229,19 @@ interface AppearanceOptions$1 {
|
|
|
1095
1229
|
* the `appearance` prop flows through identically on both platforms.
|
|
1096
1230
|
*/
|
|
1097
1231
|
|
|
1098
|
-
/** Flat color set the Softphone paints from, after appearance is resolved. */
|
|
1099
1232
|
interface SoftphonePalette {
|
|
1100
|
-
/** Component background. */
|
|
1101
1233
|
background: string;
|
|
1102
|
-
/** Dial-pad key / control surface fill. */
|
|
1103
1234
|
surface: string;
|
|
1104
|
-
/** Surface fill while pressed/active. */
|
|
1105
1235
|
surfaceActive: string;
|
|
1106
|
-
/** Primary text (digits, destination, peer name). */
|
|
1107
1236
|
text: string;
|
|
1108
|
-
/** Secondary text (letter sub-labels, call state, hints). */
|
|
1109
1237
|
textSecondary: string;
|
|
1110
|
-
/** Hairline borders/dividers. */
|
|
1111
1238
|
border: string;
|
|
1112
|
-
/** Accent (links, focus ring, keypad/secondary control highlight). */
|
|
1113
1239
|
accent: string;
|
|
1114
|
-
/** Call / Answer (green) action color. */
|
|
1115
1240
|
success: string;
|
|
1116
|
-
/** Hang up / Decline (red) action color. */
|
|
1117
1241
|
danger: string;
|
|
1118
|
-
/** Warning / caution (amber) — used by the E911 banner. */
|
|
1119
1242
|
warning: string;
|
|
1120
|
-
/** Foreground used on top of accent/success/danger fills. */
|
|
1121
1243
|
onAccent: string;
|
|
1122
1244
|
}
|
|
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
1245
|
declare function resolveSoftphonePalette(appearance?: AppearanceOptions$1): SoftphonePalette;
|
|
1130
1246
|
|
|
1131
1247
|
/**
|
|
@@ -1180,6 +1296,9 @@ declare const en: {
|
|
|
1180
1296
|
addCall: string;
|
|
1181
1297
|
addCallPlaceholder: string;
|
|
1182
1298
|
addCallSend: string;
|
|
1299
|
+
merge: string;
|
|
1300
|
+
split: string;
|
|
1301
|
+
conferenceLabel: string;
|
|
1183
1302
|
cancel: string;
|
|
1184
1303
|
stateTrying: string;
|
|
1185
1304
|
stateRinging: string;
|
|
@@ -1618,9 +1737,16 @@ declare const en: {
|
|
|
1618
1737
|
extensionPlaceholder: string;
|
|
1619
1738
|
addUser: string;
|
|
1620
1739
|
removeUser: string;
|
|
1740
|
+
removePhoneAccess: string;
|
|
1741
|
+
adminBadge: string;
|
|
1621
1742
|
noUsers: string;
|
|
1622
1743
|
atLeastOne: string;
|
|
1623
1744
|
duplicateEmail: string;
|
|
1745
|
+
extensionRequired: string;
|
|
1746
|
+
extensionTaken: string;
|
|
1747
|
+
noPhoneService: string;
|
|
1748
|
+
givePhoneAccess: string;
|
|
1749
|
+
givingPhoneAccess: string;
|
|
1624
1750
|
};
|
|
1625
1751
|
location: {
|
|
1626
1752
|
heading: string;
|
|
@@ -1893,6 +2019,7 @@ declare const en: {
|
|
|
1893
2019
|
focDateTooSoon: string;
|
|
1894
2020
|
focDateTooFar: string;
|
|
1895
2021
|
focTimeRequired: string;
|
|
2022
|
+
focTimeOutOfWindow: string;
|
|
1896
2023
|
billCopyRequired: string;
|
|
1897
2024
|
signatureRequired: string;
|
|
1898
2025
|
eligibilityError: string;
|
|
@@ -2031,6 +2158,7 @@ declare const en: {
|
|
|
2031
2158
|
perUser: string;
|
|
2032
2159
|
perPhoneNumber: string;
|
|
2033
2160
|
perVoiceAiLocation: string;
|
|
2161
|
+
voiceAiPerLocationNote: string;
|
|
2034
2162
|
perMonth: string;
|
|
2035
2163
|
rateNotSet: string;
|
|
2036
2164
|
pendingRate: string;
|
|
@@ -2179,6 +2307,19 @@ interface SoftphoneContextBase {
|
|
|
2179
2307
|
completeAttendedTransfer: () => void;
|
|
2180
2308
|
cancelAttendedTransfer: () => void;
|
|
2181
2309
|
placeCall: (destination: string) => Promise<void>;
|
|
2310
|
+
/** The calls bridged into a local three-way conference (empty when not merged). */
|
|
2311
|
+
mergedCalls: Call[];
|
|
2312
|
+
isMerged: boolean;
|
|
2313
|
+
/** While merged, every remote party mixed — what the local user hears. */
|
|
2314
|
+
conferenceAudio: MediaStream | null;
|
|
2315
|
+
/** Whether merging is possible: two connected calls, no transfer, WebAudio present. */
|
|
2316
|
+
canMerge: boolean;
|
|
2317
|
+
mergeCalls: () => void;
|
|
2318
|
+
splitMerge: () => void;
|
|
2319
|
+
/** Hang up every leg of the conference (the merged UI shows one Hang up). */
|
|
2320
|
+
hangupConference: () => void;
|
|
2321
|
+
/** Hold or resume every leg of the conference together. */
|
|
2322
|
+
holdConference: (held: boolean) => void;
|
|
2182
2323
|
emergency: UseEmergencyBinding;
|
|
2183
2324
|
emergencyManagedByHost: boolean;
|
|
2184
2325
|
lastError: {
|
|
@@ -2193,52 +2334,476 @@ interface SoftphoneContextBase {
|
|
|
2193
2334
|
}
|
|
2194
2335
|
|
|
2195
2336
|
/**
|
|
2196
|
-
*
|
|
2197
|
-
*
|
|
2198
|
-
*
|
|
2199
|
-
|
|
2200
|
-
|
|
2337
|
+
* Its own module so `createPhone()` can use it without importing the provider
|
|
2338
|
+
* (and React with it): a push-woken headless runtime has no renderer, yet it
|
|
2339
|
+
* needs the same header or the ingress 403s its handshake.
|
|
2340
|
+
*/
|
|
2341
|
+
declare const nativeSignalingSocket: SignalingSocketFactory;
|
|
2342
|
+
|
|
2343
|
+
type AudioOutputKind = 'earpiece' | 'speaker' | 'wired' | 'bluetooth' | 'car' | 'airplay' | 'other';
|
|
2344
|
+
interface AudioOutput {
|
|
2345
|
+
id: string;
|
|
2346
|
+
kind: AudioOutputKind;
|
|
2347
|
+
name: string;
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Call-audio routing, implemented by a host that owns call audio
|
|
2351
|
+
* (`callAudio="host"`) over whatever its OS integration exposes.
|
|
2352
|
+
*
|
|
2353
|
+
* `current()` must be what the OS reports, never what was last requested: the
|
|
2354
|
+
* route also changes from the CallKit/Telecom UI, a headset, or a car, and the
|
|
2355
|
+
* softphone shows only this. It must return the same object until the route
|
|
2356
|
+
* changes.
|
|
2357
|
+
*/
|
|
2358
|
+
interface AudioOutputController {
|
|
2359
|
+
current(): AudioOutput | null;
|
|
2360
|
+
onChange(listener: (output: AudioOutput | null) => void): () => void;
|
|
2361
|
+
setSpeaker(on: boolean): Promise<void>;
|
|
2362
|
+
}
|
|
2363
|
+
interface UseAudioOutput {
|
|
2364
|
+
supported: boolean;
|
|
2365
|
+
current: AudioOutput | null;
|
|
2366
|
+
speakerOn: boolean;
|
|
2367
|
+
toggleSpeaker: () => void;
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
type CallAudioOwner = 'sdk' | 'host';
|
|
2371
|
+
|
|
2372
|
+
/**
|
|
2373
|
+
* The OS call surface (CallKit on iOS, Telecom on Android) as the bridge needs
|
|
2374
|
+
* it — shaped by the bridge, not by any library. See the contract suite for
|
|
2375
|
+
* what an adapter must guarantee.
|
|
2376
|
+
*/
|
|
2377
|
+
/** OS-side call identity: the UUID CallKit/Telecom track the call under. */
|
|
2378
|
+
type OsSessionId = string;
|
|
2379
|
+
interface OsIncomingCall {
|
|
2380
|
+
callId: string;
|
|
2381
|
+
/** Caller handle as the OS should display/dial it. */
|
|
2382
|
+
from: string;
|
|
2383
|
+
displayName?: string | null;
|
|
2384
|
+
}
|
|
2385
|
+
interface OsOutgoingCall {
|
|
2386
|
+
/** The recipient handle the OS should display/dial. */
|
|
2387
|
+
to: string;
|
|
2388
|
+
displayName?: string | null;
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* Why the call ended, in the OS's vocabulary. `answeredElsewhere` /
|
|
2392
|
+
* `declinedElsewhere` make the OS dismiss the ring silently instead of logging
|
|
2393
|
+
* a missed call.
|
|
2394
|
+
*/
|
|
2395
|
+
type OsEndReason = 'remoteEnded' | 'unanswered' | 'answeredElsewhere' | 'declinedElsewhere' | 'failed' | 'unknown';
|
|
2396
|
+
/** Where the OS thinks a session is. `connecting` is answered but not yet reported connected. */
|
|
2397
|
+
type OsSessionStatus = 'ringing' | 'connecting' | 'connected' | 'ended';
|
|
2398
|
+
interface OsActiveSession {
|
|
2399
|
+
sessionId: OsSessionId;
|
|
2400
|
+
callId: string | null;
|
|
2401
|
+
/**
|
|
2402
|
+
* The OS's own view, for reconciling against the SDK call. Optional: an
|
|
2403
|
+
* adapter that can't read it leaves it out and the bridge doesn't reconcile
|
|
2404
|
+
* that field.
|
|
2405
|
+
*/
|
|
2406
|
+
status?: OsSessionStatus;
|
|
2407
|
+
held?: boolean;
|
|
2408
|
+
}
|
|
2409
|
+
interface OsCallEvents {
|
|
2410
|
+
/**
|
|
2411
|
+
* A call was reported to the OS — by native code before JS existed, or by
|
|
2412
|
+
* `reportIncoming`. `callId` is the call_id the session was created with (a
|
|
2413
|
+
* wake push's, or the one passed to `reportIncoming`): the bridge pairs the
|
|
2414
|
+
* session with its call by it, and ends a session that has none.
|
|
2415
|
+
*/
|
|
2416
|
+
incomingReported: (e: OsActiveSession) => void;
|
|
2417
|
+
/** The OS wants to PLACE a call — the user dialled from the platform's own UI
|
|
2418
|
+
* (Recents, Siri, dialer intent), not from the app. */
|
|
2419
|
+
callIntent: (e: {
|
|
2420
|
+
handle: string;
|
|
2421
|
+
}) => void;
|
|
2422
|
+
answer: (e: {
|
|
2423
|
+
sessionId: OsSessionId;
|
|
2424
|
+
}) => void;
|
|
2425
|
+
end: (e: {
|
|
2426
|
+
sessionId: OsSessionId;
|
|
2427
|
+
}) => void;
|
|
2428
|
+
setHeld: (e: {
|
|
2429
|
+
sessionId: OsSessionId;
|
|
2430
|
+
held: boolean;
|
|
2431
|
+
}) => void;
|
|
2432
|
+
setMuted: (e: {
|
|
2433
|
+
sessionId: OsSessionId;
|
|
2434
|
+
muted: boolean;
|
|
2435
|
+
}) => void;
|
|
2436
|
+
dtmf: (e: {
|
|
2437
|
+
sessionId: OsSessionId;
|
|
2438
|
+
digits: string;
|
|
2439
|
+
}) => void;
|
|
2440
|
+
/** iOS only in practice: CallKit gates audio, so WebRTC audio starts here. */
|
|
2441
|
+
audioSessionActivated: (e: {
|
|
2442
|
+
sessionId: OsSessionId | null;
|
|
2443
|
+
}) => void;
|
|
2444
|
+
}
|
|
2445
|
+
type OsCallEventName = keyof OsCallEvents;
|
|
2446
|
+
interface OsCallAdapter {
|
|
2447
|
+
/** Resolves to the OS-assigned session id (the OS assigns, never the caller). */
|
|
2448
|
+
reportIncoming(call: OsIncomingCall): Promise<OsSessionId>;
|
|
2449
|
+
/**
|
|
2450
|
+
* Report an outbound call so the OS holds a live session — on Android that
|
|
2451
|
+
* starts the foreground service keeping the process and call alive when
|
|
2452
|
+
* backgrounded, which an outbound call otherwise lacks.
|
|
2453
|
+
*/
|
|
2454
|
+
reportOutgoing(call: OsOutgoingCall): Promise<OsSessionId>;
|
|
2455
|
+
/** The call the OS is showing as incoming/dialling is now connected. */
|
|
2456
|
+
reportConnected(sessionId: OsSessionId): Promise<void>;
|
|
2457
|
+
/** Must not re-emit `end` — the bridge is telling the OS, not the other way round. */
|
|
2458
|
+
reportEnded(sessionId: OsSessionId, reason: OsEndReason): Promise<void>;
|
|
2459
|
+
/** Remote-initiated state only; the OS already knows about its own actions. */
|
|
2460
|
+
setHeld(sessionId: OsSessionId, held: boolean): Promise<void>;
|
|
2461
|
+
setMuted(sessionId: OsSessionId, muted: boolean): Promise<void>;
|
|
2462
|
+
/** The session the OS currently treats as active, if any. */
|
|
2463
|
+
getActiveSession(): Promise<OsActiveSession | null>;
|
|
2464
|
+
/**
|
|
2465
|
+
* The app's own Answer/End buttons go through the OS too, so every answer —
|
|
2466
|
+
* lock screen, notification, car, watch, in-app — arrives as the same
|
|
2467
|
+
* `answer`/`end` event and there is one code path, not two.
|
|
2468
|
+
*/
|
|
2469
|
+
answer(sessionId: OsSessionId): Promise<void>;
|
|
2470
|
+
end(sessionId: OsSessionId): Promise<void>;
|
|
2471
|
+
on<K extends OsCallEventName>(event: K, listener: OsCallEvents[K]): () => void;
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
/**
|
|
2475
|
+
* Where the bridge schedules its deadlines and retries. The JS timers are the
|
|
2476
|
+
* default, but React Native on Android pauses them in an app that is
|
|
2477
|
+
* backgrounded without being killed, even while a headless task runs. A wake
|
|
2478
|
+
* whose call never arrives (answered or cancelled before the app re-registered)
|
|
2479
|
+
* then waits on a deadline that never fires, and the OS keeps ringing. An
|
|
2480
|
+
* integration that can schedule natively passes timers here.
|
|
2481
|
+
*/
|
|
2482
|
+
interface BridgeTimers {
|
|
2483
|
+
setTimeout(fn: () => void, ms: number): unknown;
|
|
2484
|
+
clearTimeout(handle: unknown): void;
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
/**
|
|
2488
|
+
* "Keep this JS runtime alive" as a promise the platform can await.
|
|
2489
|
+
*
|
|
2490
|
+
* Android's HeadlessJsTaskService tears the runtime down the moment the task
|
|
2491
|
+
* promise settles, so a wake holds this from the instant the task starts until
|
|
2492
|
+
* live calls hit zero. What the hold actually covers is the gap before the call
|
|
2493
|
+
* library's foreground service exists — once that is up it holds the process,
|
|
2494
|
+
* and a ceiling reached mid-call ends only this task, not the call (measured: a
|
|
2495
|
+
* 60s ring survived the then-45s ceiling untouched).
|
|
2496
|
+
*
|
|
2497
|
+
* So the ceiling is a bound on a wake that never delivers, not a correctness
|
|
2498
|
+
* guarantee. It is set to the same 300s as the max park/ring, the service's task
|
|
2499
|
+
* timeout, the OS answer deadline and the library's incoming-call timeout purely
|
|
2500
|
+
* so the numbers don't contradict each other; keep them aligned.
|
|
2201
2501
|
*/
|
|
2502
|
+
declare class RuntimeHold {
|
|
2503
|
+
private readonly ceilingMs;
|
|
2504
|
+
private readonly timers;
|
|
2505
|
+
private release;
|
|
2506
|
+
private waiting;
|
|
2507
|
+
constructor(ceilingMs?: number, timers?: BridgeTimers);
|
|
2508
|
+
acquire(): Promise<void>;
|
|
2509
|
+
releaseNow(): void;
|
|
2510
|
+
/** Non-null while a hold is in flight — the reliable "a wake is mid-flight" signal. */
|
|
2511
|
+
pending(): Promise<void> | null;
|
|
2512
|
+
private settle;
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
/**
|
|
2516
|
+
* What the bridge needs from a call. Overloads rather than a generic `on`: a
|
|
2517
|
+
* generic over a narrower event map is not assignable from the SDK's wider one.
|
|
2518
|
+
*/
|
|
2519
|
+
interface BridgeCall {
|
|
2520
|
+
readonly id: string;
|
|
2521
|
+
readonly from: string;
|
|
2522
|
+
readonly fromName: string | null;
|
|
2523
|
+
readonly state: CallState;
|
|
2524
|
+
readonly isMuted: boolean;
|
|
2525
|
+
answer(): void;
|
|
2526
|
+
reject(reason?: 'busy' | 'decline'): void;
|
|
2527
|
+
hangup(): void;
|
|
2528
|
+
hold(): void;
|
|
2529
|
+
resume(): void;
|
|
2530
|
+
mute(): void;
|
|
2531
|
+
unmute(): void;
|
|
2532
|
+
sendDtmf(digits: string): void;
|
|
2533
|
+
on(event: 'answered' | 'resumed', handler: () => void): void;
|
|
2534
|
+
on(event: 'held', handler: (by: HeldBy) => void): void;
|
|
2535
|
+
on(event: 'ended', handler: (reason: CallEndReason) => void): void;
|
|
2536
|
+
off(event: 'held' | 'resumed' | 'ended', handler: (...args: never[]) => void): void;
|
|
2537
|
+
}
|
|
2538
|
+
interface BridgePhoneError {
|
|
2539
|
+
code?: string;
|
|
2540
|
+
message?: string;
|
|
2541
|
+
fatal?: boolean;
|
|
2542
|
+
callId?: string | null;
|
|
2543
|
+
}
|
|
2544
|
+
/** What the bridge needs from the phone — `DialStackPhone` satisfies it. */
|
|
2545
|
+
interface BridgePhone {
|
|
2546
|
+
readonly isConnected: boolean;
|
|
2547
|
+
connect(): Promise<void>;
|
|
2548
|
+
disconnect(): void;
|
|
2549
|
+
setToken(token: string): void;
|
|
2550
|
+
/** Place an outbound call. The bridge reports it to the OS around this. */
|
|
2551
|
+
call(destination: string): Promise<BridgeCall>;
|
|
2552
|
+
on(event: 'connected' | 'reconnected', handler: () => void): void;
|
|
2553
|
+
on(event: 'disconnected', handler: (error?: unknown) => void): void;
|
|
2554
|
+
on(event: 'reconnecting', handler: (attempt: number, delayMs: number) => void): void;
|
|
2555
|
+
on(event: 'incoming', handler: (call: BridgeCall) => void): void;
|
|
2556
|
+
on(event: 'error', handler: (err: BridgePhoneError) => void): void;
|
|
2557
|
+
}
|
|
2558
|
+
type AppLifecycleState = 'active' | 'background' | 'inactive' | 'unknown' | 'extension';
|
|
2559
|
+
/** Foreground/background as a port so the bridge has no react-native import. */
|
|
2560
|
+
interface AppLifecycle {
|
|
2561
|
+
onChange(listener: (state: AppLifecycleState) => void): () => void;
|
|
2562
|
+
}
|
|
2563
|
+
interface NativeCallBridgeOptions {
|
|
2564
|
+
phone: BridgePhone;
|
|
2565
|
+
os: OsCallAdapter;
|
|
2566
|
+
lifecycle?: AppLifecycle;
|
|
2567
|
+
hold?: RuntimeHold;
|
|
2568
|
+
log?: (message: string) => void;
|
|
2569
|
+
/** From the OS report until `connect()` resolves: a socket that never comes up. */
|
|
2570
|
+
registrationDeadlineMs?: number;
|
|
2571
|
+
/**
|
|
2572
|
+
* From `connect()` resolving until `incoming`. The proxy resumes a parked INVITE
|
|
2573
|
+
* on REGISTER or within its 1s poll, so after this nothing is coming.
|
|
2574
|
+
*/
|
|
2575
|
+
deliveryDeadlineMs?: number;
|
|
2576
|
+
/** An OS answer held for a call that has not arrived yet is stale past this. */
|
|
2577
|
+
answerTtlMs?: number;
|
|
2578
|
+
/**
|
|
2579
|
+
* How many calls the integration puts on the OS call screen at once (default 1).
|
|
2580
|
+
* A second concurrent call past the cap is NOT reported to the OS but still
|
|
2581
|
+
* rings and is answerable in-app; it just gets no OS session. Calls pair with
|
|
2582
|
+
* sessions by call_id, so any value is safe; raising it is a product choice
|
|
2583
|
+
* (OS call-waiting) and needs an adapter whose library holds several sessions.
|
|
2584
|
+
*/
|
|
2585
|
+
maxOsCalls?: number;
|
|
2586
|
+
/** Defaults to the JS timers; see `BridgeTimers` for when to pass native ones. */
|
|
2587
|
+
timers?: BridgeTimers;
|
|
2588
|
+
now?: () => number;
|
|
2589
|
+
}
|
|
2590
|
+
declare function toOsEndReason(reason: CallEndReason): OsEndReason;
|
|
2591
|
+
/**
|
|
2592
|
+
* The bidirectional bridge between the OS call surface and the SDK (OS↔SDK
|
|
2593
|
+
* answer/end/mute/hold/DTMF and lifecycle), and the only thing that subscribes to
|
|
2594
|
+
* either. Works with no renderer: a push-woken headless runtime starts it like
|
|
2595
|
+
* the app window does.
|
|
2596
|
+
*
|
|
2597
|
+
* ## Pairing a session with a call
|
|
2598
|
+
*
|
|
2599
|
+
* A call has one `call_id` everywhere: on the socket, in the wake webhook and on
|
|
2600
|
+
* the INVITE a wake resumes. The OS mints its own session id at setup, and the
|
|
2601
|
+
* bridge links it to that `call_id` (`ids`) the moment it knows both: when it
|
|
2602
|
+
* reports a call itself, when an outbound call is placed, and when the OS
|
|
2603
|
+
* reports a wake push's session (`incomingReported`, carrying the `call_id` the
|
|
2604
|
+
* push delivered). Every lookup goes through that link; none guesses by
|
|
2605
|
+
* position. A second session for a `call_id` already linked is the same call
|
|
2606
|
+
* shown twice, and is ended.
|
|
2607
|
+
*
|
|
2608
|
+
* The contract this puts on the integrator: the wake push carries the
|
|
2609
|
+
* webhook's `call_id`, and the OS adapter reports it on `incomingReported`.
|
|
2610
|
+
*/
|
|
2611
|
+
declare class NativeCallBridge {
|
|
2612
|
+
readonly hold: RuntimeHold;
|
|
2613
|
+
private readonly timers;
|
|
2614
|
+
private readonly phone;
|
|
2615
|
+
private readonly os;
|
|
2616
|
+
private readonly lifecycle;
|
|
2617
|
+
private readonly log;
|
|
2618
|
+
private readonly registrationDeadlineMs;
|
|
2619
|
+
private readonly deliveryDeadlineMs;
|
|
2620
|
+
private readonly answerTtlMs;
|
|
2621
|
+
private readonly maxOsCalls;
|
|
2622
|
+
private readonly now;
|
|
2623
|
+
private started;
|
|
2624
|
+
private appState;
|
|
2625
|
+
private reconnecting;
|
|
2626
|
+
private readonly ids;
|
|
2627
|
+
private readonly calls;
|
|
2628
|
+
private readonly callErrorWaiters;
|
|
2629
|
+
private readonly ending;
|
|
2630
|
+
private readonly changingHold;
|
|
2631
|
+
private reconcileTimer;
|
|
2632
|
+
/** Outbound calls still being placed, so not yet in `calls`. */
|
|
2633
|
+
private placingOutbound;
|
|
2634
|
+
/** Calls this app placed. For these `answered` means the far end picked up. */
|
|
2635
|
+
private readonly outboundCalls;
|
|
2636
|
+
/** OS sessions created by a native wake report, before their call arrives on the socket. */
|
|
2637
|
+
private readonly wakeSessions;
|
|
2638
|
+
private readonly reportedToOs;
|
|
2639
|
+
private readonly connectedReported;
|
|
2640
|
+
/**
|
|
2641
|
+
* An OS answer/decline that landed before the SDK had the call. On a wake the
|
|
2642
|
+
* user taps within ~200ms while connect() takes seconds, so it's applied on arrival.
|
|
2643
|
+
*/
|
|
2644
|
+
private readonly pendingAnswer;
|
|
2645
|
+
private readonly pendingDecline;
|
|
2646
|
+
private readonly answerDeadlines;
|
|
2647
|
+
private readonly suppressHoldEcho;
|
|
2648
|
+
private readonly wakeDeadlines;
|
|
2649
|
+
private readonly reportingCalls;
|
|
2650
|
+
private readonly traceLines;
|
|
2651
|
+
private traceSnapshot;
|
|
2652
|
+
private readonly traceListeners;
|
|
2653
|
+
constructor(options: NativeCallBridgeOptions);
|
|
2654
|
+
/** Idempotent. Call from the app entry on every path (window and headless). */
|
|
2655
|
+
start(): void;
|
|
2656
|
+
/**
|
|
2657
|
+
* Resolving means the SIP AOR is REGISTERed, so a parked INVITE can reach us.
|
|
2658
|
+
* The phone joins a connect already in flight, so this never starts a second
|
|
2659
|
+
* one. Never `reconnect()` here — that disposes live calls.
|
|
2660
|
+
*
|
|
2661
|
+
* `forWake` is the corpse-proofing path: when the OS freezes a backgrounded app
|
|
2662
|
+
* it can kill the WebSocket with no close frame reaching JS, so no `disconnected`
|
|
2663
|
+
* fires and `isConnected` still reads "connected" over a dead socket. Trusting them never re-REGISTERs the AOR and the parked call is never
|
|
2664
|
+
* re-forked. So a wake with NO live call sheds whatever socket state we hold and
|
|
2665
|
+
* reconnects for real. A wake WITH a live call skips this: that call proves the
|
|
2666
|
+
* socket and a teardown would drop it. A connect still in flight is joined, not
|
|
2667
|
+
* shed: it has no stale socket to distrust, and its own timeouts bound it.
|
|
2668
|
+
*/
|
|
2669
|
+
ensureConnected(forWake?: boolean): Promise<void>;
|
|
2670
|
+
/** In-app Answer, routed through the OS so it converges on the one answer path. */
|
|
2671
|
+
answer(callId: string): void;
|
|
2672
|
+
/** In-app End/Decline, via the same convergence. */
|
|
2673
|
+
end(callId: string): void;
|
|
2674
|
+
/**
|
|
2675
|
+
* Place an outbound call, reported to the OS FIRST so on Android the foreground
|
|
2676
|
+
* service starts and the call survives backgrounding mid-dial (otherwise the OS
|
|
2677
|
+
* reaps the process and drops the call). If placement fails we end the session
|
|
2678
|
+
* we opened; from then on it rides the same `watchCall` lifecycle.
|
|
2679
|
+
*
|
|
2680
|
+
* An OS-initiated dial (`callIntent`) comes through here too, so both
|
|
2681
|
+
* directions converge on one path.
|
|
2682
|
+
*/
|
|
2683
|
+
call(destination: string, displayName?: string | null): Promise<BridgeCall>;
|
|
2684
|
+
/** Ring buffer for a debug view; one-way, `useSyncExternalStore`-shaped. */
|
|
2685
|
+
getTrace(): readonly string[];
|
|
2686
|
+
subscribeTrace(listener: () => void): () => void;
|
|
2687
|
+
private subscribePhone;
|
|
2688
|
+
private onIncoming;
|
|
2689
|
+
private applyHeld;
|
|
2690
|
+
private reportIncoming;
|
|
2691
|
+
private endDuplicateSession;
|
|
2692
|
+
/**
|
|
2693
|
+
* An OS answer we could not act on yet. The OS holds its answer request open
|
|
2694
|
+
* meanwhile — CallKit keeps the incoming UI up, Telecom leaves the call in
|
|
2695
|
+
* CONNECTING — so if no call ever arrives to consume it, say so rather than
|
|
2696
|
+
* leaving the phone ringing until the adapter's own far longer deadline.
|
|
2697
|
+
*
|
|
2698
|
+
* Handle-stored and cleared by `forgetSession`, like the wake deadlines. It
|
|
2699
|
+
* has to be: session ids are recycled, so a timer left armed past its own
|
|
2700
|
+
* session fires against a LATER tap on the same id and reports a live call
|
|
2701
|
+
* failed. `armedAt` is the second half of that — it pins the timer to the one
|
|
2702
|
+
* tap it was armed for even if the map was refilled meanwhile.
|
|
2703
|
+
*/
|
|
2704
|
+
private armAnswerDeadline;
|
|
2705
|
+
private dropHeldAnswer;
|
|
2706
|
+
private answerCall;
|
|
2707
|
+
private reportConnectedReliably;
|
|
2708
|
+
private watchCall;
|
|
2709
|
+
private reportHold;
|
|
2710
|
+
/**
|
|
2711
|
+
* Hang up, and make sure the call ends. The SDK sends one frame; this retries
|
|
2712
|
+
* one the server never answers and, once the attempts run out, ends the call
|
|
2713
|
+
* here so the OS and the app don't keep a call nothing can reach.
|
|
2714
|
+
*
|
|
2715
|
+
* An error reply means the server has no such call, and the SDK ends it. A
|
|
2716
|
+
* closed socket ends it too, through the lost session.
|
|
2717
|
+
*/
|
|
2718
|
+
private endCall;
|
|
2719
|
+
/**
|
|
2720
|
+
* Hold or resume, confirmed. Resolves once the call's held/resumed arrives.
|
|
2721
|
+
*
|
|
2722
|
+
* The far end refusing on state (`invalid_message`) or a closed socket rejects
|
|
2723
|
+
* at once and leaves the call alone. A failure on the server's side (far end
|
|
2724
|
+
* unreachable) or no reply is retried; once the attempts run out the in-dialog
|
|
2725
|
+
* route is dead and every other action would fail the same way, so the call is
|
|
2726
|
+
* ended and the promise rejects.
|
|
2727
|
+
*/
|
|
2728
|
+
private changeHold;
|
|
2729
|
+
private confirmHold;
|
|
2730
|
+
private attempt;
|
|
2731
|
+
/** The pause after a failed attempt; none after the last, which has nothing to wait for. */
|
|
2732
|
+
private backoffAfter;
|
|
2733
|
+
private abandonCall;
|
|
2734
|
+
private teardownCall;
|
|
2735
|
+
private reportEndedReliably;
|
|
2736
|
+
private subscribeOs;
|
|
2737
|
+
private onIncomingReported;
|
|
2738
|
+
private armWakeDeadline;
|
|
2739
|
+
private forgetSession;
|
|
2740
|
+
private wakeInFlight;
|
|
2741
|
+
/** How many OS-reported calls are still live — the OS's session count. */
|
|
2742
|
+
private liveOsCallCount;
|
|
2743
|
+
private linkOsSession;
|
|
2744
|
+
/**
|
|
2745
|
+
* Check, shortly after a change, that the OS shows what the call is.
|
|
2746
|
+
*
|
|
2747
|
+
* Every OS report above is retried when it fails, but a report the OS takes
|
|
2748
|
+
* and then doesn't act on goes unseen: iOS accepts a connected report for an
|
|
2749
|
+
* incoming call and keeps it ringing. So compare what the OS says it shows
|
|
2750
|
+
* with the call, and correct the OS; the call's state is the server's, and
|
|
2751
|
+
* the OS follows it.
|
|
2752
|
+
*
|
|
2753
|
+
* Only the session the OS calls active is read, since that is all the port
|
|
2754
|
+
* offers. It is the one on screen, and with the default of one OS call it is
|
|
2755
|
+
* every session there is.
|
|
2756
|
+
*/
|
|
2757
|
+
private scheduleReconcile;
|
|
2758
|
+
private reconcile;
|
|
2759
|
+
private reconcileConnected;
|
|
2760
|
+
private reconcileHeld;
|
|
2761
|
+
private callForSession;
|
|
2762
|
+
private subscribeLifecycle;
|
|
2763
|
+
private dropRegistrationIfIdle;
|
|
2764
|
+
private trace;
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
/** SoftphoneProvider (React Native), same API as the web provider. */
|
|
2202
2768
|
|
|
2203
2769
|
type AppearanceOptions = Parameters<typeof resolveSoftphonePalette>[0];
|
|
2204
2770
|
type ConnectionState = SoftphoneConnectionState;
|
|
2205
2771
|
interface SoftphoneProviderProps {
|
|
2772
|
+
/**
|
|
2773
|
+
* Adopt an existing phone instead of constructing one from `token`, for a
|
|
2774
|
+
* native call surface where a call can exist before any phone does. Neither
|
|
2775
|
+
* connected nor disconnected by the provider.
|
|
2776
|
+
*/
|
|
2777
|
+
existingPhone?: DialStackPhone | null;
|
|
2206
2778
|
/** WebRTC user session token. */
|
|
2207
2779
|
token: string;
|
|
2208
2780
|
/**
|
|
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.
|
|
2781
|
+
* Persistence for the selected E911 address id. REQUIRED on RN: the SDK takes
|
|
2782
|
+
* no persistence dependency of its own. A synchronous in-memory adapter is
|
|
2783
|
+
* valid if you don't need persistence.
|
|
2216
2784
|
*/
|
|
2217
2785
|
storage: PlatformStorage;
|
|
2218
2786
|
/** API base URL (defaults to the SDK's production endpoint). */
|
|
2219
2787
|
apiBaseUrl?: string;
|
|
2220
2788
|
/**
|
|
2221
|
-
* Called shortly before the session token expires
|
|
2222
|
-
*
|
|
2223
|
-
*
|
|
2224
|
-
* the SDK keeps the (still-valid) connection open and surfaces an `error`.
|
|
2789
|
+
* Called shortly before the session token expires. Return a fresh token; the
|
|
2790
|
+
* SDK delivers it in-band with no reconnect. On reject the SDK keeps the
|
|
2791
|
+
* still-valid connection and surfaces an `error`.
|
|
2225
2792
|
*/
|
|
2226
2793
|
onTokenExpiring?: () => Promise<string>;
|
|
2227
|
-
/**
|
|
2228
|
-
*
|
|
2794
|
+
/** E911 address id to present on connect; when supplied the host manages E911
|
|
2795
|
+
* and the built-in prompt is disabled. */
|
|
2229
2796
|
emergencyAddressId?: string;
|
|
2230
2797
|
/**
|
|
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.
|
|
2798
|
+
* Device-location source for the E911 form's "Use my current location" action;
|
|
2799
|
+
* the host owns the permission prompt, geolocation, and reverse-geocoding.
|
|
2235
2800
|
*/
|
|
2236
2801
|
locationProvider?: () => Promise<EmergencyAddressInput>;
|
|
2237
2802
|
/** Connect automatically once mounted (default: true). */
|
|
2238
2803
|
autoConnect?: boolean;
|
|
2239
|
-
/** Theming — the shared appearance surface
|
|
2804
|
+
/** Theming — the shared appearance surface. */
|
|
2240
2805
|
appearance?: AppearanceOptions;
|
|
2241
|
-
/** Locale for UI strings (defaults to English)
|
|
2806
|
+
/** Locale for UI strings (defaults to English). */
|
|
2242
2807
|
locale?: Locale;
|
|
2243
2808
|
/** Default country for number formatting. */
|
|
2244
2809
|
defaultCountry?: CountryCode;
|
|
@@ -2246,6 +2811,9 @@ interface SoftphoneProviderProps {
|
|
|
2246
2811
|
state: ConnectionState;
|
|
2247
2812
|
}) => void;
|
|
2248
2813
|
onIncomingCall?: (event: {
|
|
2814
|
+
/** Handle for `callActionsFor()` / `answerCall()`, and what a host bridging
|
|
2815
|
+
* to a native call UI binds its OS session to. */
|
|
2816
|
+
callId: string;
|
|
2249
2817
|
from: string;
|
|
2250
2818
|
fromName: string | null;
|
|
2251
2819
|
}) => void;
|
|
@@ -2260,17 +2828,39 @@ interface SoftphoneProviderProps {
|
|
|
2260
2828
|
code: string;
|
|
2261
2829
|
message: string;
|
|
2262
2830
|
}) => void;
|
|
2831
|
+
/**
|
|
2832
|
+
* The call bridge, when this app reports calls to the OS. Outbound placement
|
|
2833
|
+
* then goes through it, so the OS learns about the call BEFORE it is dialled —
|
|
2834
|
+
* on Android that report is what starts the foreground service, without which
|
|
2835
|
+
* backgrounding mid-dial lets the OS reap the process and drop the call.
|
|
2836
|
+
* Answering in-app goes through it too, so the OS answers before the call
|
|
2837
|
+
* starts media (on iOS that is what gives the call its audio).
|
|
2838
|
+
*/
|
|
2839
|
+
bridge?: Pick<NativeCallBridge, 'call' | 'answer'>;
|
|
2840
|
+
/**
|
|
2841
|
+
* Who owns call audio: ringing, outbound ringback, the audio session and the
|
|
2842
|
+
* proximity sensor. `'sdk'` (the default): the provider does all of it.
|
|
2843
|
+
* `'host'`: the provider does none of it, for an app that reports calls to
|
|
2844
|
+
* CallKit/Telecom itself, which ring the phone and own the session. Doing any
|
|
2845
|
+
* of it on top of the OS breaks capture. Always `'host'` with a `bridge`.
|
|
2846
|
+
*/
|
|
2847
|
+
callAudio?: CallAudioOwner;
|
|
2848
|
+
/**
|
|
2849
|
+
* The output route, for a host that owns call audio: without it the in-call
|
|
2850
|
+
* speaker button is disabled, since only the OS integration can move the
|
|
2851
|
+
* route. Ignored unless call audio is `'host'`.
|
|
2852
|
+
*/
|
|
2853
|
+
audioOutput?: AudioOutputController;
|
|
2263
2854
|
children: React.ReactNode;
|
|
2264
2855
|
}
|
|
2265
|
-
/**
|
|
2266
|
-
*
|
|
2267
|
-
* `locationProvider`. (`palette` is on the base — computed identically to web.)
|
|
2268
|
-
*/
|
|
2856
|
+
/** The native softphone context: the shared base plus the native-only
|
|
2857
|
+
* `locationProvider`. */
|
|
2269
2858
|
interface SoftphoneContextValue extends SoftphoneContextBase {
|
|
2270
2859
|
/** Host-supplied device-location source for the E911 form, or undefined. */
|
|
2271
2860
|
locationProvider: (() => Promise<EmergencyAddressInput>) | undefined;
|
|
2861
|
+
audio: UseAudioOutput;
|
|
2272
2862
|
}
|
|
2273
|
-
declare function SoftphoneProvider({ token, storage, apiBaseUrl, onTokenExpiring, emergencyAddressId, locationProvider, autoConnect, appearance, locale, defaultCountry, onConnectionStateChange, onIncomingCall, onCallStarted, onCallEnded, onError, children, }: SoftphoneProviderProps): React.JSX.Element;
|
|
2863
|
+
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
2864
|
/** Access the full softphone context. Throws when used outside the provider. */
|
|
2275
2865
|
declare function useSoftphone(): SoftphoneContextValue;
|
|
2276
2866
|
/** The active foreground call + its actions (build-your-own convenience). */
|
|
@@ -2278,6 +2868,8 @@ declare function useActiveCall(): {
|
|
|
2278
2868
|
activeCall: Call | null;
|
|
2279
2869
|
actions: UseCallActions;
|
|
2280
2870
|
};
|
|
2871
|
+
/** The call-audio output route and the speaker toggle. */
|
|
2872
|
+
declare function useAudioOutput(): UseAudioOutput;
|
|
2281
2873
|
/** The currently-ringing inbound call, or null. */
|
|
2282
2874
|
declare function useIncomingCall(): Call | null;
|
|
2283
2875
|
|
|
@@ -2317,10 +2909,10 @@ declare function Softphone({ autoFocusDestination }: SoftphoneProps): React.JSX.
|
|
|
2317
2909
|
*/
|
|
2318
2910
|
|
|
2319
2911
|
interface DialPadProps {
|
|
2320
|
-
/** Autofocus the destination field on mount. */
|
|
2321
2912
|
autoFocusDestination?: boolean;
|
|
2913
|
+
compact?: boolean;
|
|
2322
2914
|
}
|
|
2323
|
-
declare function DialPad({ autoFocusDestination }: DialPadProps): React.JSX.Element;
|
|
2915
|
+
declare function DialPad({ autoFocusDestination, compact, }: DialPadProps): React.JSX.Element;
|
|
2324
2916
|
|
|
2325
2917
|
/**
|
|
2326
2918
|
* IncomingCall / IncomingStack (RN) — the ringing inbound UI.
|
|
@@ -2363,5 +2955,38 @@ declare function OngoingCall(): React.JSX.Element | null;
|
|
|
2363
2955
|
|
|
2364
2956
|
declare function EmergencyBanner(): React.JSX.Element | null;
|
|
2365
2957
|
|
|
2366
|
-
|
|
2367
|
-
|
|
2958
|
+
type CreatePhoneOptions = Omit<PhoneOptions, 'createSignalingSocket' | 'deferInboundCapture'>;
|
|
2959
|
+
/**
|
|
2960
|
+
* The process's one phone.
|
|
2961
|
+
*
|
|
2962
|
+
* Module scope is the correct scope, not a shortcut: a push wake and the UI run
|
|
2963
|
+
* in the same JS runtime but enter from different places (a headless task, the
|
|
2964
|
+
* app window), so this is a plain shared reference. Two phones would register
|
|
2965
|
+
* the same SIP AOR twice and the call would go to whichever registered last
|
|
2966
|
+
* while the UI watched the other — a silent failure, so a second construction
|
|
2967
|
+
* fails loudly instead. Construct eagerly at app entry: `new DialStackPhone()`
|
|
2968
|
+
* does no I/O (only `connect()` does).
|
|
2969
|
+
*/
|
|
2970
|
+
declare function createPhone(options: CreatePhoneOptions): DialStackPhone;
|
|
2971
|
+
declare function getPhone(): DialStackPhone | null;
|
|
2972
|
+
/** For consumers that must not be the one to create it (the UI). */
|
|
2973
|
+
declare function requirePhone(): DialStackPhone;
|
|
2974
|
+
|
|
2975
|
+
declare const appLifecycle: AppLifecycle;
|
|
2976
|
+
declare const DEFAULT_WAKE_TASK = "DialStackWake";
|
|
2977
|
+
/**
|
|
2978
|
+
* Android's wake: the adapter's native push service reports the call to Telecom
|
|
2979
|
+
* with no JS, and something must then start a runtime so the SDK can
|
|
2980
|
+
* re-REGISTER and receive the parked INVITE. That is this headless task; the
|
|
2981
|
+
* app's HeadlessJsTaskService names it (`taskName`).
|
|
2982
|
+
*
|
|
2983
|
+
* iOS has no equivalent — PushKit launches the process and React Native starts
|
|
2984
|
+
* normally, so the app entry's `bridge.start()` is the whole wake path.
|
|
2985
|
+
*
|
|
2986
|
+
* A single file with a runtime branch rather than `.android.ts`/`.ios.ts`: the
|
|
2987
|
+
* package ships as one bundle, so Metro's platform extensions cannot apply.
|
|
2988
|
+
*/
|
|
2989
|
+
declare function registerWakeTask(bridge: NativeCallBridge, taskName?: string): void;
|
|
2990
|
+
|
|
2991
|
+
export { Call, DEFAULT_WAKE_TASK, DialPad, EmergencyBanner, IncomingCall, NativeCallBridge, OngoingCall, RuntimeHold, Softphone, SoftphoneProvider, appLifecycle, createPhone, getPhone, nativeSignalingSocket, registerWakeTask, requirePhone, toOsEndReason, useActiveCall, useAudioOutput, useIncomingCall, useSoftphone };
|
|
2992
|
+
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 };
|