@ethora/chat-component 26.10.7 → 26.10.8
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 +88 -3
- package/dist/ChatProfileModal-83t5BRkI.js +2298 -0
- package/dist/{FilePreviewModal-DlOjnYzv.js → FilePreviewModal-DxUGjSHe.js} +130 -130
- package/dist/{ManageDataModal-BZLGJZze.js → ManageDataModal-DFE_Wegj.js} +14 -14
- package/dist/{MarkdownBody-CtArwCDa.js → MarkdownBody-uFITrPjT.js} +1 -1
- package/dist/{MessageSearchModal-CcUnfZlU.js → MessageSearchModal-CsbTWgNm.js} +32 -32
- package/dist/{PublicChatsModal-BWn0NXVo.js → PublicChatsModal-bDOp3IL9.js} +19 -19
- package/dist/{SideDrawer-CqZl9sxK.js → SideDrawer-A_1ECqD2.js} +28 -28
- package/dist/{UserProfileModal-D0JZ3-uy.js → UserProfileModal-Ca5wHYvr.js} +124 -120
- package/dist/{UserSettingsModal-OvykYDsT.js → UserSettingsModal-CAyyQGV6.js} +2 -2
- package/dist/{VideoCallOverlay-DrvO2v_g.js → VideoCallOverlay-BMiQh8kI.js} +5908 -4168
- package/dist/{VisibilityModal-pOF9-UXR.js → VisibilityModal-BXyIy7_J.js} +7 -7
- package/dist/assets/icons.d.ts +4 -0
- package/dist/calls/callDisplay.d.ts +22 -0
- package/dist/calls/callMode.d.ts +105 -0
- package/dist/calls/openScheduledCall.d.ts +20 -0
- package/dist/calls/p2p/P2PCallController.d.ts +160 -0
- package/dist/calls/p2p/icePolicy.d.ts +36 -0
- package/dist/calls/p2p/negotiator.d.ts +56 -0
- package/dist/calls/p2p/p2pRuntime.d.ts +35 -0
- package/dist/calls/p2p/signalInbox.d.ts +34 -0
- package/dist/calls/p2p/signalStanza.d.ts +51 -0
- package/dist/calls/placeCall.d.ts +20 -0
- package/dist/calls/scheduledCall.d.ts +101 -0
- package/dist/calls/scheduledCallActions.d.ts +12 -0
- package/dist/calls/scheduledCallFormat.d.ts +13 -0
- package/dist/calls/scheduledCallsConfig.d.ts +27 -0
- package/dist/calls/senderTrust.d.ts +7 -0
- package/dist/calls/startCall.d.ts +16 -0
- package/dist/components/MainComponents/ChatWrapper.d.ts +1 -0
- package/dist/components/MainComponents/LoginWrapper.d.ts +1 -0
- package/dist/components/MainComponents/ProfileImagePlaceholder.d.ts +1 -0
- package/dist/components/MainComponents/ReduxWrapper.d.ts +6 -0
- package/dist/components/MenuRoom/MenuRoom.d.ts +1 -0
- package/dist/components/Modals/NewChatModal/styledNewChatComponents.d.ts +27 -0
- package/dist/components/Modals/styledModalComponents.d.ts +21 -0
- package/dist/components/ScheduledCalls/ScheduleCallModal.d.ts +3 -0
- package/dist/components/ScheduledCalls/ScheduledCallCancelConfirm.d.ts +8 -0
- package/dist/components/ScheduledCalls/ScheduledCallCard.d.ts +7 -0
- package/dist/components/ScheduledCalls/ScheduledCallLayer.d.ts +15 -0
- package/dist/components/ScheduledCalls/ScheduledCallPreJoin.d.ts +3 -0
- package/dist/components/VideoCalls/P2PCallRunner.d.ts +1 -0
- package/dist/components/VideoCalls/P2PCallSession.d.ts +14 -0
- package/dist/components/VideoCalls/VideoCallSession.d.ts +37 -0
- package/dist/{handleCopyClick-BUbVvd2G.js → handleCopyClick-BX1fcFGN.js} +1 -1
- package/dist/helpers/callPush.d.ts +2 -0
- package/dist/helpers/canLeaveRoom.d.ts +3 -0
- package/dist/helpers/chatComponentConfigEvents.d.ts +16 -0
- package/dist/helpers/liveConfig.d.ts +46 -0
- package/dist/helpers/reportRoom.d.ts +18 -0
- package/dist/helpers/scheduledCallMessage.d.ts +23 -0
- package/dist/helpers/storeConsole.d.ts +1 -0
- package/dist/hooks/useNow.d.ts +6 -0
- package/dist/hooks/useRemoteAppearance.d.ts +21 -0
- package/dist/hooks/useTypography.d.ts +4 -0
- package/dist/{main-CpHMnydn.js → main-B25iFKAC.js} +15794 -13324
- package/dist/main.d.ts +3 -1
- package/dist/main.js +28 -26
- package/dist/networking/api-requests/callState.api.d.ts +24 -0
- package/dist/networking/api-requests/remoteAppearance.api.d.ts +20 -0
- package/dist/networking/api-requests/rooms.api.d.ts +2 -1
- package/dist/networking/api-requests/scheduledCalls.api.d.ts +19 -0
- package/dist/networking/ethoraEvents.d.ts +14 -1
- package/dist/networking/xmpp/notifyXmppEvent.d.ts +6 -0
- package/dist/roomStore/callSlice.d.ts +23 -1
- package/dist/roomStore/chatSettingsSlice.d.ts +9 -1
- package/dist/roomStore/index.d.ts +4 -0
- package/dist/roomStore/scheduledCallSlice.d.ts +47 -0
- package/dist/types/models/config.model.d.ts +71 -1
- package/dist/types/models/message.model.d.ts +2 -0
- package/dist/types/models/room.model.d.ts +2 -0
- package/dist/types/models/scheduledCall.model.d.ts +116 -0
- package/package.json +1 -1
- package/dist/ChatProfileModal-BDZvd0rm.js +0 -2168
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs as r, jsx as s } from "react/jsx-runtime";
|
|
2
2
|
import { useState as b, useEffect as x } from "react";
|
|
3
|
-
import {
|
|
4
|
-
import { S as
|
|
3
|
+
import { a4 as o, a as L, Z as P, u as z, bg as D, bh as I } from "./main-B25iFKAC.js";
|
|
4
|
+
import { S as N, D as C, a as $, b as k, e as w, f as p } from "./SideDrawer-A_1ECqD2.js";
|
|
5
5
|
o.div``;
|
|
6
6
|
o.div`
|
|
7
7
|
font-size: 16px;
|
|
@@ -25,7 +25,7 @@ const O = o.div`
|
|
|
25
25
|
`, u = o.input`
|
|
26
26
|
accent-color: ${({ $radioColor: i }) => i || "var(--ethora-color-primary, #0052CD)"};
|
|
27
27
|
margin: 0px;
|
|
28
|
-
`,
|
|
28
|
+
`, R = o.div`
|
|
29
29
|
padding: 10px 15px;
|
|
30
30
|
margin-top: 16px;
|
|
31
31
|
color: ${({ type: i }) => i === "success" ? "#10b981" : "#ef4444"};
|
|
@@ -42,9 +42,9 @@ const O = o.div`
|
|
|
42
42
|
`, B = ({
|
|
43
43
|
handleCloseModal: i
|
|
44
44
|
}) => {
|
|
45
|
-
const S = L(), e = P(), { user: n, config: l } =
|
|
45
|
+
const S = L(), e = P(), { user: n, config: l } = z(
|
|
46
46
|
(t) => t.chatSettingStore
|
|
47
|
-
), g = (t) => S(
|
|
47
|
+
), g = (t) => S(I(t)), [a, m] = b(n?.isProfileOpen), [c, v] = b(n?.isAssetsOpen), [f, y] = b(null), d = (t, A) => {
|
|
48
48
|
y({ message: t, type: A }), setTimeout(() => y(null), 3e3);
|
|
49
49
|
};
|
|
50
50
|
return x(() => {
|
|
@@ -56,7 +56,7 @@ const O = o.div`
|
|
|
56
56
|
g(t.user), d(e("notification.saved"), "success");
|
|
57
57
|
}).catch(() => d(e("toast.error"), "error"));
|
|
58
58
|
}, [c]), /* @__PURE__ */ r(
|
|
59
|
-
|
|
59
|
+
N,
|
|
60
60
|
{
|
|
61
61
|
title: e("settings.visibility.title"),
|
|
62
62
|
onClose: i,
|
|
@@ -128,7 +128,7 @@ const O = o.div`
|
|
|
128
128
|
/* @__PURE__ */ s(p, { children: e("settings.visibility.individualDescription") })
|
|
129
129
|
] }) }) })
|
|
130
130
|
] }),
|
|
131
|
-
f && /* @__PURE__ */ s(
|
|
131
|
+
f && /* @__PURE__ */ s(R, { type: f.type, children: f.message })
|
|
132
132
|
]
|
|
133
133
|
}
|
|
134
134
|
);
|
package/dist/assets/icons.d.ts
CHANGED
|
@@ -43,6 +43,10 @@ export declare const VideoCallIcon: ({ color, ...props }: {
|
|
|
43
43
|
[x: string]: any;
|
|
44
44
|
color?: string;
|
|
45
45
|
}) => import("react/jsx-runtime").JSX.Element;
|
|
46
|
+
export declare const CalendarIcon: ({ color, ...props }: {
|
|
47
|
+
[x: string]: any;
|
|
48
|
+
color?: string;
|
|
49
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
46
50
|
export declare const AudioCallIcon: ({ color, ...props }: {
|
|
47
51
|
[x: string]: any;
|
|
48
52
|
color?: string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Shown on the ring screen when no human-readable name is known. */
|
|
2
|
+
export declare const NEUTRAL_INCOMING_CALL_LABEL = "Incoming call";
|
|
3
|
+
/**
|
|
4
|
+
* True for strings that are a chat's internal name or an id rather than
|
|
5
|
+
* something a person would be called: two user JIDs joined (the private chat
|
|
6
|
+
* name), a lone user id, an `<appId>_<uuid>` group name, or any long run of
|
|
7
|
+
* hex / underscore / hyphen.
|
|
8
|
+
*/
|
|
9
|
+
export declare const looksLikeRawRoomName: (value: unknown) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Plain-text, display-safe version of a name: control characters stripped,
|
|
12
|
+
* whitespace collapsed and trimmed, capped at about 80 characters. Returns ''
|
|
13
|
+
* when nothing usable is left or the value is a raw chat name / id.
|
|
14
|
+
*/
|
|
15
|
+
export declare const sanitizeCallName: (value: unknown) => string;
|
|
16
|
+
/** sanitizeCallName with null instead of '' (state-friendly). */
|
|
17
|
+
export declare const safeCallName: (value: unknown) => string | null;
|
|
18
|
+
/**
|
|
19
|
+
* An xmppUsername from a stanza attribute, or null when it is not shaped like
|
|
20
|
+
* one (it is only ever used for lookups and routing, never shown).
|
|
21
|
+
*/
|
|
22
|
+
export declare const sanitizeXmppUsername: (value: unknown) => string | null;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export type CallMode = 'livekit' | 'p2p';
|
|
2
|
+
export type CallKindName = 'audio' | 'video';
|
|
3
|
+
export type CallRoomType = 'private' | 'group';
|
|
4
|
+
/** Serializable subset of RTCIceServer (kept free of DOM types for redux). */
|
|
5
|
+
export interface IceServerConfig {
|
|
6
|
+
urls: string | string[];
|
|
7
|
+
username?: string;
|
|
8
|
+
credential?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Accepts an array, an `{ iceServers }` envelope or a JSON string of either
|
|
12
|
+
* (the call-token stanza carries the list as a JSON attribute) and returns
|
|
13
|
+
* only well formed stun/turn entries. Anything else is dropped, never thrown.
|
|
14
|
+
*/
|
|
15
|
+
export declare const normalizeIceServers: (raw: unknown) => IceServerConfig[];
|
|
16
|
+
/**
|
|
17
|
+
* A LiveKit server URL as the backend publishes it. Only websocket / http(s)
|
|
18
|
+
* URLs are accepted: the value ends up as the connect target of the media
|
|
19
|
+
* session, so anything else (javascript:, data:, garbage) is treated as unset.
|
|
20
|
+
*/
|
|
21
|
+
export declare const normalizeLivekitUrl: (raw: unknown) => string | null;
|
|
22
|
+
/** Attributes of a `<data type="call-token">` element, defensively read. */
|
|
23
|
+
export interface ParsedCallTokenAttrs {
|
|
24
|
+
token: string;
|
|
25
|
+
room: string;
|
|
26
|
+
/** Raw `callId` attribute, null when absent (the JWT may still carry it). */
|
|
27
|
+
callId: string | null;
|
|
28
|
+
/** Valid `kind` attribute, null when absent or unrecognised. */
|
|
29
|
+
kind: CallKindName | null;
|
|
30
|
+
mode: CallMode;
|
|
31
|
+
group: boolean;
|
|
32
|
+
/** Callee's ICE servers for `mode=p2p`, null when absent or unparseable. */
|
|
33
|
+
iceServers: IceServerConfig[] | null;
|
|
34
|
+
/**
|
|
35
|
+
* Public LiveKit wss URL for `mode=livekit`, null when the server did not
|
|
36
|
+
* send one (older backends), it is empty, or it is not a usable URL.
|
|
37
|
+
*/
|
|
38
|
+
livekitUrl: string | null;
|
|
39
|
+
/**
|
|
40
|
+
* Caller's display name as plain text (control characters stripped, capped,
|
|
41
|
+
* raw chat names refused); '' when absent. Only trustworthy from the system
|
|
42
|
+
* account, see calls/senderTrust.
|
|
43
|
+
*/
|
|
44
|
+
callerName: string;
|
|
45
|
+
/** Caller's xmppUsername when well formed, else null. Same trust rule. */
|
|
46
|
+
caller: string | null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Tolerant reader for the call-token stanza. A server that sends neither
|
|
50
|
+
* `mode` nor `group` (every server before P2P / group calls) yields
|
|
51
|
+
* `mode: 'livekit'`, `group: false`, i.e. exactly the legacy behaviour.
|
|
52
|
+
*/
|
|
53
|
+
export declare const parseCallTokenAttrs: (attrs: Record<string, any> | undefined | null) => ParsedCallTokenAttrs;
|
|
54
|
+
/** Normalised `POST /v1/chats/call/create/:chatName` response. */
|
|
55
|
+
export interface CallCreateResult {
|
|
56
|
+
callId: string | null;
|
|
57
|
+
mode: CallMode;
|
|
58
|
+
kind: CallKindName;
|
|
59
|
+
/** Server-reported group flag, null when the server did not say. */
|
|
60
|
+
group: boolean | null;
|
|
61
|
+
/** Public LiveKit wss URL when the server knows it. */
|
|
62
|
+
livekitUrl: string | null;
|
|
63
|
+
iceServers: IceServerConfig[];
|
|
64
|
+
ttlSeconds: number | null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Reads the create response. Old servers answer with no `mode`, which maps to
|
|
68
|
+
* `livekit` (the caller then waits for its own call-token as before). A p2p
|
|
69
|
+
* answer without a callId is unusable (no way to correlate signalling) and
|
|
70
|
+
* throws.
|
|
71
|
+
*/
|
|
72
|
+
export declare const parseCallCreateResponse: (data: unknown, fallbackKind: CallKindName) => CallCreateResult;
|
|
73
|
+
/**
|
|
74
|
+
* Which room types may start / receive calls. Hosts that never set
|
|
75
|
+
* `allowedRoomTypes` get 1:1 and group chats; an explicit list is respected
|
|
76
|
+
* as written (so `['private']` keeps a host on 1:1 only).
|
|
77
|
+
*/
|
|
78
|
+
export declare const DEFAULT_CALL_ROOM_TYPES: CallRoomType[];
|
|
79
|
+
export declare const isCallRoomTypeAllowed: (roomType: string | undefined | null, allowedRoomTypes?: CallRoomType[] | null) => boolean;
|
|
80
|
+
export interface BusyCallSnapshot {
|
|
81
|
+
phase: string;
|
|
82
|
+
direction: 'incoming' | 'outgoing' | null;
|
|
83
|
+
mode: CallMode;
|
|
84
|
+
callId: string | null;
|
|
85
|
+
roomBareName: string | null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* What to do with an incoming call-token while another call may be active.
|
|
89
|
+
* - accept: nothing active, ring.
|
|
90
|
+
* - duplicate: same callId as the active call (server re-delivery), ignore.
|
|
91
|
+
* - replace: legacy LiveKit behaviour (a new token overwrites the old
|
|
92
|
+
* call), kept exactly for livekit-vs-livekit.
|
|
93
|
+
* - glare-yield: both sides dialled each other; the lexicographically smaller
|
|
94
|
+
* callId wins on BOTH ends, and this is the loser: drop our
|
|
95
|
+
* outgoing call and ring for theirs.
|
|
96
|
+
* - glare-keep: same race, we hold the winning callId, ignore theirs (the
|
|
97
|
+
* other side yields).
|
|
98
|
+
* - busy: a different call is in progress, decline the newcomer.
|
|
99
|
+
*/
|
|
100
|
+
export type IncomingWhileBusyDecision = 'accept' | 'duplicate' | 'replace' | 'glare-yield' | 'glare-keep' | 'busy';
|
|
101
|
+
export declare const decideIncomingWhileBusy: (current: BusyCallSnapshot, incoming: {
|
|
102
|
+
mode: CallMode;
|
|
103
|
+
callId: string | null;
|
|
104
|
+
roomBareName: string | null;
|
|
105
|
+
}) => IncomingWhileBusyDecision;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ScheduledCallLinkOptions } from './scheduledCall';
|
|
2
|
+
/**
|
|
3
|
+
* Asks the chat to open a scheduled call: select its room and show the
|
|
4
|
+
* pre-join panel. Returns false (and does nothing) when `slug` is not a
|
|
5
|
+
* plausible slug. The request waits in the store until a signed-in <Chat> is
|
|
6
|
+
* mounted, so a deep link can be passed in before either is ready; failures
|
|
7
|
+
* are reported through `config.scheduledCalls.onOpenError`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function openScheduledCall(slug: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The slug of a scheduled-call join link (`https://<host>/call/<slug>` or
|
|
12
|
+
* `<scheme>://call/<slug>`), or null when `url` is not one. The hosted web app
|
|
13
|
+
* (`DEFAULT_SCHEDULED_CALL_HOSTS`) and the page's own host are always
|
|
14
|
+
* accepted, plus `options.hosts` or, without them, the hosts of
|
|
15
|
+
* `config.scheduledCalls`. Schemes: `options.schemes`, else the configured
|
|
16
|
+
* ones, unset means any. Same name, options and defaults as the React Native SDK.
|
|
17
|
+
*/
|
|
18
|
+
/** The hosted web app: always accepted. */
|
|
19
|
+
export declare const DEFAULT_SCHEDULED_CALL_HOSTS: string[];
|
|
20
|
+
export declare function parseScheduledCallLink(url: unknown, options?: ScheduledCallLinkOptions): string | null;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { IceServerConfig } from '../callMode';
|
|
2
|
+
import { P2PRole } from './negotiator';
|
|
3
|
+
import { P2PSignal, P2PSignalName, P2PSignalPayload } from './signalStanza';
|
|
4
|
+
export type P2PStatus = 'preparing' | 'waiting' | 'connecting' | 'connected' | 'reconnecting' | 'failed' | 'ended';
|
|
5
|
+
export interface P2PSnapshot {
|
|
6
|
+
status: P2PStatus;
|
|
7
|
+
localStream: MediaStream | null;
|
|
8
|
+
remoteStream: MediaStream | null;
|
|
9
|
+
micEnabled: boolean;
|
|
10
|
+
camEnabled: boolean;
|
|
11
|
+
remoteVideoLive: boolean;
|
|
12
|
+
/** Dismissible, non-fatal hint (e.g. the camera could not be re-enabled). */
|
|
13
|
+
notice: string | null;
|
|
14
|
+
}
|
|
15
|
+
export interface P2PCallParams {
|
|
16
|
+
callId: string;
|
|
17
|
+
role: P2PRole;
|
|
18
|
+
kind: 'audio' | 'video';
|
|
19
|
+
/** Peer's xmpp localpart; signals from anyone else are ignored. */
|
|
20
|
+
peerLocal: string;
|
|
21
|
+
iceServers: IceServerConfig[];
|
|
22
|
+
startWithMicOn?: boolean;
|
|
23
|
+
startWithCameraOn?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface P2PCallDeps {
|
|
26
|
+
createPeerConnection: (config: RTCConfiguration) => RTCPeerConnection;
|
|
27
|
+
getUserMedia: (constraints: MediaStreamConstraints) => Promise<MediaStream>;
|
|
28
|
+
createMediaStream: () => MediaStream;
|
|
29
|
+
/** Fresh credentials; null/empty keeps the servers already in use. */
|
|
30
|
+
fetchIceServers: () => Promise<IceServerConfig[] | null>;
|
|
31
|
+
sendSignal: (signal: P2PSignalName, payload: P2PSignalPayload) => void;
|
|
32
|
+
log?: (message: string, detail?: unknown) => void;
|
|
33
|
+
}
|
|
34
|
+
export interface P2PCallEvents {
|
|
35
|
+
/** The offer/answer exchange completed: media is now negotiating. */
|
|
36
|
+
onNegotiated?: () => void;
|
|
37
|
+
/** Media is flowing for the first time. */
|
|
38
|
+
onConnected?: () => void;
|
|
39
|
+
/** The peer ended the call (hang up, decline, their own failure). */
|
|
40
|
+
onRemoteEnd?: (reason: string) => void;
|
|
41
|
+
/** The call failed locally; the message is meant for the user. */
|
|
42
|
+
onFailed?: (message: string) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Called exactly once when the call's resources are released. `started` is
|
|
45
|
+
* false for a callee that never accepted (nothing was negotiated).
|
|
46
|
+
*/
|
|
47
|
+
onDisposed?: (info: {
|
|
48
|
+
started: boolean;
|
|
49
|
+
}) => void;
|
|
50
|
+
}
|
|
51
|
+
export interface P2PTimings {
|
|
52
|
+
connectTimeoutMs: number;
|
|
53
|
+
offerResendMs: number[];
|
|
54
|
+
}
|
|
55
|
+
export declare const describeMediaError: (error: unknown) => string;
|
|
56
|
+
/**
|
|
57
|
+
* What to show when the PEER ended the call for a reason that is a failure
|
|
58
|
+
* rather than a normal hang up / decline. Null means "just close the call".
|
|
59
|
+
*/
|
|
60
|
+
export declare const remoteEndNotice: (reason: string) => string | null;
|
|
61
|
+
export declare class P2PCallController {
|
|
62
|
+
readonly callId: string;
|
|
63
|
+
readonly role: P2PRole;
|
|
64
|
+
readonly kind: 'audio' | 'video';
|
|
65
|
+
private readonly params;
|
|
66
|
+
private readonly deps;
|
|
67
|
+
private readonly events;
|
|
68
|
+
private readonly timings;
|
|
69
|
+
private readonly negotiator;
|
|
70
|
+
private readonly policy;
|
|
71
|
+
private snapshot;
|
|
72
|
+
private readonly listeners;
|
|
73
|
+
private pc;
|
|
74
|
+
private localStream;
|
|
75
|
+
private audioTrack;
|
|
76
|
+
private videoTrack;
|
|
77
|
+
private videoSender;
|
|
78
|
+
private ownedTracks;
|
|
79
|
+
private remoteStream;
|
|
80
|
+
private remoteCleanups;
|
|
81
|
+
private queue;
|
|
82
|
+
private timers;
|
|
83
|
+
private graceTimer;
|
|
84
|
+
private deadlineTimer;
|
|
85
|
+
private connectTimer;
|
|
86
|
+
private started;
|
|
87
|
+
private disposed;
|
|
88
|
+
private endSent;
|
|
89
|
+
private connectedOnce;
|
|
90
|
+
private negotiated;
|
|
91
|
+
private cameraBusy;
|
|
92
|
+
private remoteVideoWatched;
|
|
93
|
+
private remoteVideoStalled;
|
|
94
|
+
private lastConn;
|
|
95
|
+
private candidateGateOpen;
|
|
96
|
+
private heldOutgoingCandidates;
|
|
97
|
+
private sentCandidates;
|
|
98
|
+
private lastOffer;
|
|
99
|
+
constructor(params: P2PCallParams, deps: P2PCallDeps, events?: P2PCallEvents, timings?: Partial<P2PTimings>);
|
|
100
|
+
subscribe: (listener: () => void) => (() => void);
|
|
101
|
+
getSnapshot: () => P2PSnapshot;
|
|
102
|
+
private update;
|
|
103
|
+
private log;
|
|
104
|
+
private later;
|
|
105
|
+
private cancel;
|
|
106
|
+
/**
|
|
107
|
+
* Caller: get media, build the connection, offer. Callee: get media, build
|
|
108
|
+
* the connection, then apply the offer that may already be waiting. Safe to
|
|
109
|
+
* call once; every failure ends the call with a message instead of throwing.
|
|
110
|
+
*/
|
|
111
|
+
start(): Promise<void>;
|
|
112
|
+
/** Feed a signal from the peer (already routed by callId). */
|
|
113
|
+
receive(signal: P2PSignal): void;
|
|
114
|
+
/** Local user hung up (or cancelled): tell the peer and release everything. */
|
|
115
|
+
hangup(reason?: string): void;
|
|
116
|
+
/**
|
|
117
|
+
* Release every resource: timers, listeners, media tracks, the connection.
|
|
118
|
+
* Idempotent, safe from any exit path. Tells the peer unless it is already
|
|
119
|
+
* known to be over.
|
|
120
|
+
*/
|
|
121
|
+
dispose(): void;
|
|
122
|
+
private sendEnd;
|
|
123
|
+
private fail;
|
|
124
|
+
private handleRemoteEnd;
|
|
125
|
+
private acquireMedia;
|
|
126
|
+
private releaseMedia;
|
|
127
|
+
toggleMic(): void;
|
|
128
|
+
/**
|
|
129
|
+
* Turning the camera off stops sending video and releases the device (the
|
|
130
|
+
* camera light goes out); turning it on opens the camera again and swaps it
|
|
131
|
+
* into the existing sender, so no renegotiation is needed.
|
|
132
|
+
*/
|
|
133
|
+
toggleCamera(): Promise<void>;
|
|
134
|
+
dismissNotice(): void;
|
|
135
|
+
private createConnection;
|
|
136
|
+
private emitCandidate;
|
|
137
|
+
private openCandidateGate;
|
|
138
|
+
private onRemoteTrack;
|
|
139
|
+
private remoteVideoLive;
|
|
140
|
+
/**
|
|
141
|
+
* A peer that turns its camera off drops the sender's track (replaceTrack
|
|
142
|
+
* with null). Chrome does not mute the receiving track for that, so the
|
|
143
|
+
* `mute` event alone leaves the last frame frozen on screen. Frames that stop
|
|
144
|
+
* arriving are the signal that does work in every browser, so the received
|
|
145
|
+
* video is polled and a stall counts as "no live picture" until frames flow
|
|
146
|
+
* again.
|
|
147
|
+
*/
|
|
148
|
+
private watchRemoteVideo;
|
|
149
|
+
private enqueue;
|
|
150
|
+
private sendOffer;
|
|
151
|
+
private scheduleOfferResends;
|
|
152
|
+
private runCommands;
|
|
153
|
+
private applyOffer;
|
|
154
|
+
private onNegotiated;
|
|
155
|
+
private connState;
|
|
156
|
+
private onConnectionState;
|
|
157
|
+
private runIceAction;
|
|
158
|
+
/** ICE restart with fresh TURN credentials; the caller of this is the policy. */
|
|
159
|
+
private restartIce;
|
|
160
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type P2PConnState = 'new' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed';
|
|
2
|
+
export type IceAction = {
|
|
3
|
+
type: 'none';
|
|
4
|
+
} | {
|
|
5
|
+
type: 'wait';
|
|
6
|
+
ms: number;
|
|
7
|
+
} | {
|
|
8
|
+
type: 'restart';
|
|
9
|
+
deadlineMs: number;
|
|
10
|
+
} | {
|
|
11
|
+
type: 'recovered';
|
|
12
|
+
} | {
|
|
13
|
+
type: 'give-up';
|
|
14
|
+
reason: string;
|
|
15
|
+
};
|
|
16
|
+
export interface IcePolicyOptions {
|
|
17
|
+
graceMs?: number;
|
|
18
|
+
restartDeadlineMs?: number;
|
|
19
|
+
maxRestartsPerCall?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare class IceRestartPolicy {
|
|
22
|
+
private readonly graceMs;
|
|
23
|
+
private readonly restartDeadlineMs;
|
|
24
|
+
private readonly maxRestarts;
|
|
25
|
+
private restarts;
|
|
26
|
+
private attemptedThisOutage;
|
|
27
|
+
private inOutage;
|
|
28
|
+
constructor(options?: IcePolicyOptions);
|
|
29
|
+
get restartCount(): number;
|
|
30
|
+
private tryRestart;
|
|
31
|
+
onState(state: P2PConnState): IceAction;
|
|
32
|
+
/** The grace period after `disconnected` ran out without recovery. */
|
|
33
|
+
onGraceElapsed(): IceAction;
|
|
34
|
+
/** The restart deadline ran out; `stillDown` is the state at that moment. */
|
|
35
|
+
onRestartDeadline(stillDown: boolean): IceAction;
|
|
36
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { P2PSignal } from './signalStanza';
|
|
2
|
+
export type P2PRole = 'caller' | 'callee';
|
|
3
|
+
export type NegotiatorCommand = {
|
|
4
|
+
type: 'apply-offer';
|
|
5
|
+
sdp: string;
|
|
6
|
+
initial: boolean;
|
|
7
|
+
rollback: boolean;
|
|
8
|
+
} | {
|
|
9
|
+
type: 'apply-answer';
|
|
10
|
+
sdp: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'add-candidate';
|
|
13
|
+
candidate: RTCIceCandidateInit;
|
|
14
|
+
} | {
|
|
15
|
+
type: 'remote-end';
|
|
16
|
+
reason: string;
|
|
17
|
+
};
|
|
18
|
+
export declare class P2PNegotiator {
|
|
19
|
+
readonly callId: string;
|
|
20
|
+
readonly role: P2PRole;
|
|
21
|
+
private readonly peerLocal;
|
|
22
|
+
private ready;
|
|
23
|
+
private ended;
|
|
24
|
+
private remoteDescriptionSet;
|
|
25
|
+
private initialOfferTaken;
|
|
26
|
+
private heldOffer;
|
|
27
|
+
private localOfferPending;
|
|
28
|
+
private lastOfferSdp;
|
|
29
|
+
private lastAnswerSdp;
|
|
30
|
+
private heldCandidates;
|
|
31
|
+
private seenCandidates;
|
|
32
|
+
constructor(params: {
|
|
33
|
+
callId: string;
|
|
34
|
+
role: P2PRole;
|
|
35
|
+
peerLocal: string;
|
|
36
|
+
});
|
|
37
|
+
get isEnded(): boolean;
|
|
38
|
+
/** True while we have sent an offer and not yet received its answer. */
|
|
39
|
+
get awaitingAnswer(): boolean;
|
|
40
|
+
private accepts;
|
|
41
|
+
receive(signal: P2PSignal): NegotiatorCommand[];
|
|
42
|
+
/**
|
|
43
|
+
* Local media and the peer connection exist. For the callee this releases
|
|
44
|
+
* the offer that arrived while it was still on the ring screen.
|
|
45
|
+
*/
|
|
46
|
+
markReady(): NegotiatorCommand[];
|
|
47
|
+
/** We created and set a local offer; its answer is now expected. */
|
|
48
|
+
markLocalOffer(): void;
|
|
49
|
+
/**
|
|
50
|
+
* A remote description (offer or answer) was applied to the connection.
|
|
51
|
+
* Returns the candidates that were waiting for it.
|
|
52
|
+
*/
|
|
53
|
+
markRemoteApplied(): NegotiatorCommand[];
|
|
54
|
+
/** Local teardown: later signals are ignored. */
|
|
55
|
+
stop(): void;
|
|
56
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Element } from 'ltx';
|
|
2
|
+
import { P2PSignalInbox } from './signalInbox';
|
|
3
|
+
import { P2PSignalName, P2PSignalPayload } from './signalStanza';
|
|
4
|
+
import { P2PCallController, P2PCallDeps } from './P2PCallController';
|
|
5
|
+
/** Signals that arrive for a call nobody is handling yet wait here. */
|
|
6
|
+
export declare const p2pInbox: P2PSignalInbox;
|
|
7
|
+
/**
|
|
8
|
+
* Entry point for an incoming `<data type="call-p2p">` message (called from
|
|
9
|
+
* the stanza router). Stale, replayed, malformed or self-sent signals are
|
|
10
|
+
* dropped; the rest go to the inbox, which delivers or briefly holds them.
|
|
11
|
+
*/
|
|
12
|
+
export declare const handleP2PSignalStanza: (inner: Element) => void;
|
|
13
|
+
/** Sends one signal straight to the peer's bare JID over the live XMPP client. */
|
|
14
|
+
export declare const sendP2PSignal: (params: {
|
|
15
|
+
peerXmppUsername: string;
|
|
16
|
+
callId: string;
|
|
17
|
+
signal: P2PSignalName;
|
|
18
|
+
payload: P2PSignalPayload;
|
|
19
|
+
}) => boolean;
|
|
20
|
+
/** The real browser dependencies for one call's controller. */
|
|
21
|
+
export declare const createBrowserP2PDeps: (params: {
|
|
22
|
+
callId: string;
|
|
23
|
+
peerXmppUsername: string;
|
|
24
|
+
}) => P2PCallDeps;
|
|
25
|
+
export declare const getActiveP2PController: () => P2PCallController | null;
|
|
26
|
+
export declare const subscribeActiveP2P: (listener: () => void) => (() => void);
|
|
27
|
+
/**
|
|
28
|
+
* Returns the controller for `key`, creating it on first use. Effects that
|
|
29
|
+
* unmount and immediately remount (React StrictMode, a parent re-keying) get
|
|
30
|
+
* the SAME controller back instead of tearing the call down and starting a
|
|
31
|
+
* second negotiation: release() only disposes after a tick, and acquire()
|
|
32
|
+
* within that tick cancels it.
|
|
33
|
+
*/
|
|
34
|
+
export declare const acquireP2PController: (key: string, create: () => P2PCallController) => P2PCallController;
|
|
35
|
+
export declare const releaseP2PController: (key: string) => void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { P2PSignal } from './signalStanza';
|
|
2
|
+
export interface SignalInboxOptions {
|
|
3
|
+
/** How long signals for a call with no listener are kept. */
|
|
4
|
+
ttlMs?: number;
|
|
5
|
+
/** Most distinct unclaimed callIds held at once (oldest dropped first). */
|
|
6
|
+
maxPendingCalls?: number;
|
|
7
|
+
/** Most signals kept per unclaimed call. */
|
|
8
|
+
maxPerCall?: number;
|
|
9
|
+
now?: () => number;
|
|
10
|
+
}
|
|
11
|
+
export type SignalDelivery = 'delivered' | 'buffered' | 'dropped';
|
|
12
|
+
type Handler = (signal: P2PSignal) => void;
|
|
13
|
+
export declare class P2PSignalInbox {
|
|
14
|
+
private readonly ttlMs;
|
|
15
|
+
private readonly maxPendingCalls;
|
|
16
|
+
private readonly maxPerCall;
|
|
17
|
+
private readonly now;
|
|
18
|
+
private readonly pending;
|
|
19
|
+
private readonly handlers;
|
|
20
|
+
private readonly closed;
|
|
21
|
+
constructor(options?: SignalInboxOptions);
|
|
22
|
+
private sweep;
|
|
23
|
+
receive(signal: P2PSignal): SignalDelivery;
|
|
24
|
+
/**
|
|
25
|
+
* Starts delivering a call's signals to `handler`, replaying anything held
|
|
26
|
+
* for it first (in arrival order). Returns a detach function that also
|
|
27
|
+
* marks the call closed.
|
|
28
|
+
*/
|
|
29
|
+
attach(callId: string, handler: Handler): () => void;
|
|
30
|
+
/** The call is over: forget held signals and drop future stragglers. */
|
|
31
|
+
close(callId: string): void;
|
|
32
|
+
clear(): void;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Element } from 'ltx';
|
|
2
|
+
export declare const P2P_SIGNAL_TYPE = "call-p2p";
|
|
3
|
+
export type P2PSignalName = 'offer' | 'answer' | 'candidate' | 'end';
|
|
4
|
+
export interface P2PSdpPayload {
|
|
5
|
+
type: 'offer' | 'answer';
|
|
6
|
+
sdp: string;
|
|
7
|
+
}
|
|
8
|
+
export interface P2PCandidatePayload {
|
|
9
|
+
candidate: string;
|
|
10
|
+
sdpMid?: string | null;
|
|
11
|
+
sdpMLineIndex?: number | null;
|
|
12
|
+
usernameFragment?: string | null;
|
|
13
|
+
}
|
|
14
|
+
export interface P2PEndPayload {
|
|
15
|
+
reason?: string;
|
|
16
|
+
}
|
|
17
|
+
export type P2PSignalPayload = P2PSdpPayload | P2PCandidatePayload | P2PEndPayload;
|
|
18
|
+
export interface P2PSignal {
|
|
19
|
+
callId: string;
|
|
20
|
+
signal: P2PSignalName;
|
|
21
|
+
payload: P2PSignalPayload;
|
|
22
|
+
/** Sender's bare JID as written in `from` (lower-cased), '' when missing. */
|
|
23
|
+
fromBare: string;
|
|
24
|
+
/** Sender's xmpp localpart (the part before '@'), '' when missing. */
|
|
25
|
+
fromLocal: string;
|
|
26
|
+
/**
|
|
27
|
+
* Age of the stanza from the server's stanza-id timestamp, null when the
|
|
28
|
+
* stanza carries none (direct messages often do not).
|
|
29
|
+
*/
|
|
30
|
+
ageMs: number | null;
|
|
31
|
+
/** Carries <delay/>: it was stored offline and replayed, so it is stale. */
|
|
32
|
+
delayed: boolean;
|
|
33
|
+
}
|
|
34
|
+
export declare const MAX_P2P_SIGNAL_AGE_MS = 15000;
|
|
35
|
+
export declare const buildP2PSignalStanza: (params: {
|
|
36
|
+
to: string;
|
|
37
|
+
callId: string;
|
|
38
|
+
signal: P2PSignalName;
|
|
39
|
+
payload?: P2PSignalPayload;
|
|
40
|
+
}) => Element;
|
|
41
|
+
/**
|
|
42
|
+
* Reads a `call-p2p` message. Returns null for anything that is not a
|
|
43
|
+
* well formed signal (wrong type, unknown signal, bad JSON, bad payload).
|
|
44
|
+
*/
|
|
45
|
+
export declare const parseP2PSignalStanza: (stanza: Element, now?: number) => P2PSignal | null;
|
|
46
|
+
/**
|
|
47
|
+
* Whether a parsed signal belongs to a live negotiation. An `end` is always
|
|
48
|
+
* accepted unless replayed (it only ever acts on a call that is still
|
|
49
|
+
* active, and stopping a call late is better than never).
|
|
50
|
+
*/
|
|
51
|
+
export declare const isP2PSignalFresh: (signal: P2PSignal) => boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Dispatch } from '@reduxjs/toolkit';
|
|
2
|
+
import { IRoom } from '../types/types';
|
|
3
|
+
import { CallKindName } from './callMode';
|
|
4
|
+
/**
|
|
5
|
+
* Dials a call in `room`: the one path every "start or join a call" entry
|
|
6
|
+
* point shares (the chat header buttons and the scheduled-call Join button).
|
|
7
|
+
* Which transport carries it is the server's decision (p2p for 1:1, LiveKit
|
|
8
|
+
* for groups); a group call that is already running is joined, not duplicated.
|
|
9
|
+
*
|
|
10
|
+
* `chatName` is the bare room name (the JID local part), never the display
|
|
11
|
+
* title. Failures surface through the call slice's error state.
|
|
12
|
+
*/
|
|
13
|
+
export declare function placeRoomCall(params: {
|
|
14
|
+
dispatch: Dispatch;
|
|
15
|
+
room: Pick<IRoom, 'jid' | 'name' | 'members'> & {
|
|
16
|
+
type?: IRoom['type'];
|
|
17
|
+
};
|
|
18
|
+
kind: CallKindName;
|
|
19
|
+
selfXmppUsername?: string | null;
|
|
20
|
+
}): Promise<void>;
|