@polymorfa/sdk 0.1.0-dev.20260922093854
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/LICENSE +21 -0
- package/README.md +1881 -0
- package/dist/bridge.d.ts +31 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +39 -0
- package/dist/bridge.js.map +1 -0
- package/dist/calls/api.d.ts +109 -0
- package/dist/calls/api.d.ts.map +1 -0
- package/dist/calls/api.js +179 -0
- package/dist/calls/api.js.map +1 -0
- package/dist/calls/call.d.ts +286 -0
- package/dist/calls/call.d.ts.map +1 -0
- package/dist/calls/call.js +860 -0
- package/dist/calls/call.js.map +1 -0
- package/dist/calls/client.d.ts +124 -0
- package/dist/calls/client.d.ts.map +1 -0
- package/dist/calls/client.js +447 -0
- package/dist/calls/client.js.map +1 -0
- package/dist/calls/diagnostics.d.ts +74 -0
- package/dist/calls/diagnostics.d.ts.map +1 -0
- package/dist/calls/diagnostics.js +133 -0
- package/dist/calls/diagnostics.js.map +1 -0
- package/dist/calls/errors.d.ts +37 -0
- package/dist/calls/errors.d.ts.map +1 -0
- package/dist/calls/errors.js +52 -0
- package/dist/calls/errors.js.map +1 -0
- package/dist/calls/events.d.ts +15 -0
- package/dist/calls/events.d.ts.map +1 -0
- package/dist/calls/events.js +34 -0
- package/dist/calls/events.js.map +1 -0
- package/dist/calls/index.d.ts +6 -0
- package/dist/calls/index.d.ts.map +1 -0
- package/dist/calls/index.js +7 -0
- package/dist/calls/index.js.map +1 -0
- package/dist/calls/internal.d.ts +18 -0
- package/dist/calls/internal.d.ts.map +1 -0
- package/dist/calls/internal.js +18 -0
- package/dist/calls/internal.js.map +1 -0
- package/dist/calls/lifecycle.d.ts +111 -0
- package/dist/calls/lifecycle.d.ts.map +1 -0
- package/dist/calls/lifecycle.js +490 -0
- package/dist/calls/lifecycle.js.map +1 -0
- package/dist/calls/media.d.ts +102 -0
- package/dist/calls/media.d.ts.map +1 -0
- package/dist/calls/media.js +414 -0
- package/dist/calls/media.js.map +1 -0
- package/dist/calls/protocol.d.ts +216 -0
- package/dist/calls/protocol.d.ts.map +1 -0
- package/dist/calls/protocol.js +268 -0
- package/dist/calls/protocol.js.map +1 -0
- package/dist/calls/token.d.ts +43 -0
- package/dist/calls/token.d.ts.map +1 -0
- package/dist/calls/token.js +146 -0
- package/dist/calls/token.js.map +1 -0
- package/dist/client.d.ts +65 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +119 -0
- package/dist/client.js.map +1 -0
- package/dist/credentials.d.ts +50 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +73 -0
- package/dist/credentials.js.map +1 -0
- package/dist/errors.d.ts +71 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +129 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/media/whatsapp.d.ts +105 -0
- package/dist/media/whatsapp.d.ts.map +1 -0
- package/dist/media/whatsapp.js +624 -0
- package/dist/media/whatsapp.js.map +1 -0
- package/dist/messaging/bansafe.d.ts +30 -0
- package/dist/messaging/bansafe.d.ts.map +1 -0
- package/dist/messaging/bansafe.js +86 -0
- package/dist/messaging/bansafe.js.map +1 -0
- package/dist/messaging/business.d.ts +35 -0
- package/dist/messaging/business.d.ts.map +1 -0
- package/dist/messaging/business.js +145 -0
- package/dist/messaging/business.js.map +1 -0
- package/dist/messaging/calls.d.ts +9 -0
- package/dist/messaging/calls.d.ts.map +1 -0
- package/dist/messaging/calls.js +16 -0
- package/dist/messaging/calls.js.map +1 -0
- package/dist/messaging/campaigns.d.ts +19 -0
- package/dist/messaging/campaigns.d.ts.map +1 -0
- package/dist/messaging/campaigns.js +77 -0
- package/dist/messaging/campaigns.js.map +1 -0
- package/dist/messaging/channels.d.ts +23 -0
- package/dist/messaging/channels.d.ts.map +1 -0
- package/dist/messaging/channels.js +104 -0
- package/dist/messaging/channels.js.map +1 -0
- package/dist/messaging/chats.d.ts +14 -0
- package/dist/messaging/chats.d.ts.map +1 -0
- package/dist/messaging/chats.js +51 -0
- package/dist/messaging/chats.js.map +1 -0
- package/dist/messaging/client-tokens.d.ts +15 -0
- package/dist/messaging/client-tokens.d.ts.map +1 -0
- package/dist/messaging/client-tokens.js +64 -0
- package/dist/messaging/client-tokens.js.map +1 -0
- package/dist/messaging/client.d.ts +58 -0
- package/dist/messaging/client.d.ts.map +1 -0
- package/dist/messaging/client.js +100 -0
- package/dist/messaging/client.js.map +1 -0
- package/dist/messaging/contacts.d.ts +20 -0
- package/dist/messaging/contacts.d.ts.map +1 -0
- package/dist/messaging/contacts.js +75 -0
- package/dist/messaging/contacts.js.map +1 -0
- package/dist/messaging/groups.d.ts +34 -0
- package/dist/messaging/groups.d.ts.map +1 -0
- package/dist/messaging/groups.js +97 -0
- package/dist/messaging/groups.js.map +1 -0
- package/dist/messaging/identities.d.ts +9 -0
- package/dist/messaging/identities.d.ts.map +1 -0
- package/dist/messaging/identities.js +21 -0
- package/dist/messaging/identities.js.map +1 -0
- package/dist/messaging/labels.d.ts +16 -0
- package/dist/messaging/labels.d.ts.map +1 -0
- package/dist/messaging/labels.js +65 -0
- package/dist/messaging/labels.js.map +1 -0
- package/dist/messaging/media.d.ts +72 -0
- package/dist/messaging/media.d.ts.map +1 -0
- package/dist/messaging/media.js +142 -0
- package/dist/messaging/media.js.map +1 -0
- package/dist/messaging/messages.d.ts +18 -0
- package/dist/messaging/messages.d.ts.map +1 -0
- package/dist/messaging/messages.js +32 -0
- package/dist/messaging/messages.js.map +1 -0
- package/dist/messaging/observation-policies.d.ts +10 -0
- package/dist/messaging/observation-policies.d.ts.map +1 -0
- package/dist/messaging/observation-policies.js +28 -0
- package/dist/messaging/observation-policies.js.map +1 -0
- package/dist/messaging/onboarding.d.ts +53 -0
- package/dist/messaging/onboarding.d.ts.map +1 -0
- package/dist/messaging/onboarding.js +69 -0
- package/dist/messaging/onboarding.js.map +1 -0
- package/dist/messaging/presence.d.ts +12 -0
- package/dist/messaging/presence.d.ts.map +1 -0
- package/dist/messaging/presence.js +43 -0
- package/dist/messaging/presence.js.map +1 -0
- package/dist/messaging/privacy.d.ts +11 -0
- package/dist/messaging/privacy.d.ts.map +1 -0
- package/dist/messaging/privacy.js +34 -0
- package/dist/messaging/privacy.js.map +1 -0
- package/dist/messaging/profile.d.ts +13 -0
- package/dist/messaging/profile.d.ts.map +1 -0
- package/dist/messaging/profile.js +49 -0
- package/dist/messaging/profile.js.map +1 -0
- package/dist/messaging/quick-replies.d.ts +13 -0
- package/dist/messaging/quick-replies.d.ts.map +1 -0
- package/dist/messaging/quick-replies.js +45 -0
- package/dist/messaging/quick-replies.js.map +1 -0
- package/dist/messaging/quicklinks.d.ts +69 -0
- package/dist/messaging/quicklinks.d.ts.map +1 -0
- package/dist/messaging/quicklinks.js +45 -0
- package/dist/messaging/quicklinks.js.map +1 -0
- package/dist/messaging/session-configuration.d.ts +67 -0
- package/dist/messaging/session-configuration.d.ts.map +1 -0
- package/dist/messaging/session-configuration.js +2 -0
- package/dist/messaging/session-configuration.js.map +1 -0
- package/dist/messaging/sessions.d.ts +26 -0
- package/dist/messaging/sessions.d.ts.map +1 -0
- package/dist/messaging/sessions.js +108 -0
- package/dist/messaging/sessions.js.map +1 -0
- package/dist/messaging/templates.d.ts +15 -0
- package/dist/messaging/templates.d.ts.map +1 -0
- package/dist/messaging/templates.js +67 -0
- package/dist/messaging/templates.js.map +1 -0
- package/dist/messaging/testing-configuration.d.ts +89 -0
- package/dist/messaging/testing-configuration.d.ts.map +1 -0
- package/dist/messaging/testing-configuration.js +14 -0
- package/dist/messaging/testing-configuration.js.map +1 -0
- package/dist/messaging/types.d.ts +1793 -0
- package/dist/messaging/types.d.ts.map +1 -0
- package/dist/messaging/types.js +25 -0
- package/dist/messaging/types.js.map +1 -0
- package/dist/messaging/users.d.ts +9 -0
- package/dist/messaging/users.d.ts.map +1 -0
- package/dist/messaging/users.js +15 -0
- package/dist/messaging/users.js.map +1 -0
- package/dist/messaging/voip.d.ts +51 -0
- package/dist/messaging/voip.d.ts.map +1 -0
- package/dist/messaging/voip.js +293 -0
- package/dist/messaging/voip.js.map +1 -0
- package/dist/messaging/webhooks.d.ts +13 -0
- package/dist/messaging/webhooks.d.ts.map +1 -0
- package/dist/messaging/webhooks.js +48 -0
- package/dist/messaging/webhooks.js.map +1 -0
- package/dist/node.d.ts +55 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +237 -0
- package/dist/node.js.map +1 -0
- package/dist/pagination.d.ts +24 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +28 -0
- package/dist/pagination.js.map +1 -0
- package/dist/platform/api-keys.d.ts +10 -0
- package/dist/platform/api-keys.d.ts.map +1 -0
- package/dist/platform/api-keys.js +22 -0
- package/dist/platform/api-keys.js.map +1 -0
- package/dist/platform/audiences.d.ts +16 -0
- package/dist/platform/audiences.d.ts.map +1 -0
- package/dist/platform/audiences.js +46 -0
- package/dist/platform/audiences.js.map +1 -0
- package/dist/platform/audit-logs.d.ts +9 -0
- package/dist/platform/audit-logs.d.ts.map +1 -0
- package/dist/platform/audit-logs.js +20 -0
- package/dist/platform/audit-logs.js.map +1 -0
- package/dist/platform/bansafe.d.ts +24 -0
- package/dist/platform/bansafe.d.ts.map +1 -0
- package/dist/platform/bansafe.js +70 -0
- package/dist/platform/bansafe.js.map +1 -0
- package/dist/platform/billing.d.ts +13 -0
- package/dist/platform/billing.d.ts.map +1 -0
- package/dist/platform/billing.js +27 -0
- package/dist/platform/billing.js.map +1 -0
- package/dist/platform/campaigns.d.ts +28 -0
- package/dist/platform/campaigns.d.ts.map +1 -0
- package/dist/platform/campaigns.js +92 -0
- package/dist/platform/campaigns.js.map +1 -0
- package/dist/platform/customers.d.ts +23 -0
- package/dist/platform/customers.d.ts.map +1 -0
- package/dist/platform/customers.js +140 -0
- package/dist/platform/customers.js.map +1 -0
- package/dist/platform/developer-resources.d.ts +101 -0
- package/dist/platform/developer-resources.d.ts.map +1 -0
- package/dist/platform/developer-resources.js +250 -0
- package/dist/platform/developer-resources.js.map +1 -0
- package/dist/platform/developer-types.d.ts +378 -0
- package/dist/platform/developer-types.d.ts.map +1 -0
- package/dist/platform/developer-types.js +2 -0
- package/dist/platform/developer-types.js.map +1 -0
- package/dist/platform/event-stream.d.ts +114 -0
- package/dist/platform/event-stream.d.ts.map +1 -0
- package/dist/platform/event-stream.js +308 -0
- package/dist/platform/event-stream.js.map +1 -0
- package/dist/platform/media.d.ts +13 -0
- package/dist/platform/media.d.ts.map +1 -0
- package/dist/platform/media.js +33 -0
- package/dist/platform/media.js.map +1 -0
- package/dist/platform/members.d.ts +9 -0
- package/dist/platform/members.d.ts.map +1 -0
- package/dist/platform/members.js +15 -0
- package/dist/platform/members.js.map +1 -0
- package/dist/platform/opt-outs.d.ts +15 -0
- package/dist/platform/opt-outs.d.ts.map +1 -0
- package/dist/platform/opt-outs.js +36 -0
- package/dist/platform/opt-outs.js.map +1 -0
- package/dist/platform/organizations.d.ts +9 -0
- package/dist/platform/organizations.d.ts.map +1 -0
- package/dist/platform/organizations.js +15 -0
- package/dist/platform/organizations.js.map +1 -0
- package/dist/platform/project-tokens.d.ts +9 -0
- package/dist/platform/project-tokens.d.ts.map +1 -0
- package/dist/platform/project-tokens.js +16 -0
- package/dist/platform/project-tokens.js.map +1 -0
- package/dist/platform/projects.d.ts +24 -0
- package/dist/platform/projects.d.ts.map +1 -0
- package/dist/platform/projects.js +83 -0
- package/dist/platform/projects.js.map +1 -0
- package/dist/platform/quicklink-settings.d.ts +85 -0
- package/dist/platform/quicklink-settings.d.ts.map +1 -0
- package/dist/platform/quicklink-settings.js +35 -0
- package/dist/platform/quicklink-settings.js.map +1 -0
- package/dist/platform/response.d.ts +10 -0
- package/dist/platform/response.d.ts.map +1 -0
- package/dist/platform/response.js +41 -0
- package/dist/platform/response.js.map +1 -0
- package/dist/platform/security-incidents.d.ts +10 -0
- package/dist/platform/security-incidents.d.ts.map +1 -0
- package/dist/platform/security-incidents.js +22 -0
- package/dist/platform/security-incidents.js.map +1 -0
- package/dist/platform/session-bans.d.ts +13 -0
- package/dist/platform/session-bans.d.ts.map +1 -0
- package/dist/platform/session-bans.js +21 -0
- package/dist/platform/session-bans.js.map +1 -0
- package/dist/platform/session-configuration.d.ts +15 -0
- package/dist/platform/session-configuration.d.ts.map +1 -0
- package/dist/platform/session-configuration.js +36 -0
- package/dist/platform/session-configuration.js.map +1 -0
- package/dist/platform/sessions.d.ts +26 -0
- package/dist/platform/sessions.d.ts.map +1 -0
- package/dist/platform/sessions.js +121 -0
- package/dist/platform/sessions.js.map +1 -0
- package/dist/platform/sip-trunks.d.ts +141 -0
- package/dist/platform/sip-trunks.d.ts.map +1 -0
- package/dist/platform/sip-trunks.js +140 -0
- package/dist/platform/sip-trunks.js.map +1 -0
- package/dist/platform/types.d.ts +897 -0
- package/dist/platform/types.d.ts.map +1 -0
- package/dist/platform/types.js +2 -0
- package/dist/platform/types.js.map +1 -0
- package/dist/raw.d.ts +26 -0
- package/dist/raw.d.ts.map +1 -0
- package/dist/raw.js +79 -0
- package/dist/raw.js.map +1 -0
- package/dist/system.d.ts +37 -0
- package/dist/system.d.ts.map +1 -0
- package/dist/system.js +45 -0
- package/dist/system.js.map +1 -0
- package/dist/transport/body.d.ts +6 -0
- package/dist/transport/body.d.ts.map +1 -0
- package/dist/transport/body.js +33 -0
- package/dist/transport/body.js.map +1 -0
- package/dist/transport/content-disposition.d.ts +9 -0
- package/dist/transport/content-disposition.d.ts.map +1 -0
- package/dist/transport/content-disposition.js +107 -0
- package/dist/transport/content-disposition.js.map +1 -0
- package/dist/transport/http.d.ts +35 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +660 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/idempotency.d.ts +10 -0
- package/dist/transport/idempotency.d.ts.map +1 -0
- package/dist/transport/idempotency.js +13 -0
- package/dist/transport/idempotency.js.map +1 -0
- package/dist/transport/retry.d.ts +11 -0
- package/dist/transport/retry.d.ts.map +1 -0
- package/dist/transport/retry.js +45 -0
- package/dist/transport/retry.js.map +1 -0
- package/dist/transport/types.d.ts +58 -0
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/types.js +2 -0
- package/dist/transport/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/dist/webhooks/events.d.ts +741 -0
- package/dist/webhooks/events.d.ts.map +1 -0
- package/dist/webhooks/events.js +80 -0
- package/dist/webhooks/events.js.map +1 -0
- package/dist/webhooks/index.d.ts +4 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +4 -0
- package/dist/webhooks/index.js.map +1 -0
- package/dist/webhooks/utilities.d.ts +33 -0
- package/dist/webhooks/utilities.d.ts.map +1 -0
- package/dist/webhooks/utilities.js +67 -0
- package/dist/webhooks/utilities.js.map +1 -0
- package/dist/webhooks/verify.d.ts +9 -0
- package/dist/webhooks/verify.d.ts.map +1 -0
- package/dist/webhooks/verify.js +77 -0
- package/dist/webhooks/verify.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,860 @@
|
|
|
1
|
+
import { CallReporter } from "./diagnostics.js";
|
|
2
|
+
import { CallClaimedError, CallsAuthError, CallsError } from "./errors.js";
|
|
3
|
+
import { Emitter } from "./events.js";
|
|
4
|
+
import { MediaSocket, } from "./media.js";
|
|
5
|
+
import { createConnectionId, isConnectionId, } from "./protocol.js";
|
|
6
|
+
function videoSource(source) {
|
|
7
|
+
if (source.participant !== undefined)
|
|
8
|
+
return {
|
|
9
|
+
id: source.source,
|
|
10
|
+
label: source.participant.phoneNumber ?? source.participant.id,
|
|
11
|
+
participant: source.participant,
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
id: source.source,
|
|
15
|
+
label: source.connectionParticipant ?? source.connectionId,
|
|
16
|
+
connectionId: source.connectionId,
|
|
17
|
+
...(source.connectionParticipant === undefined
|
|
18
|
+
? {}
|
|
19
|
+
: { connectionParticipant: source.connectionParticipant }),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The call's merged audio as signed 16-bit mono PCM samples at
|
|
24
|
+
* {@link sampleRate}: `on("data")` is the call → you, `write()` is you → the call.
|
|
25
|
+
*/
|
|
26
|
+
export class AudioTrack extends Emitter {
|
|
27
|
+
#socket;
|
|
28
|
+
#sampleRate;
|
|
29
|
+
constructor(sampleRate) {
|
|
30
|
+
super();
|
|
31
|
+
this.#sampleRate = sampleRate;
|
|
32
|
+
}
|
|
33
|
+
/** s16le mono PCM rate both directions use. Final once the call is connected. */
|
|
34
|
+
get sampleRate() {
|
|
35
|
+
return this.#sampleRate;
|
|
36
|
+
}
|
|
37
|
+
/** Push s16le mono PCM at {@link sampleRate}. Returns false when no media is attached. */
|
|
38
|
+
write(pcm) {
|
|
39
|
+
return this.#socket?.writeAudio(pcm) ?? false;
|
|
40
|
+
}
|
|
41
|
+
/** @internal */
|
|
42
|
+
_attach(socket, sampleRate) {
|
|
43
|
+
this.#socket = socket;
|
|
44
|
+
if (sampleRate !== undefined)
|
|
45
|
+
this.#sampleRate = sampleRate;
|
|
46
|
+
}
|
|
47
|
+
/** @internal */
|
|
48
|
+
_push(pcm) {
|
|
49
|
+
this.emit("data", pcm);
|
|
50
|
+
}
|
|
51
|
+
/** @internal */
|
|
52
|
+
_detach() {
|
|
53
|
+
this.#socket = undefined;
|
|
54
|
+
this.removeAllListeners();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The call's video: one outgoing H.264 stream, and one received stream per
|
|
59
|
+
* remote participant. Streams are separate; nothing is composed.
|
|
60
|
+
*/
|
|
61
|
+
export class VideoTrack extends Emitter {
|
|
62
|
+
#socket;
|
|
63
|
+
#sources = new Map();
|
|
64
|
+
/** Remote participant videos by `id`. */
|
|
65
|
+
get sources() {
|
|
66
|
+
return this.#sources;
|
|
67
|
+
}
|
|
68
|
+
/** Send one encoded frame. Returns false when no media is attached. */
|
|
69
|
+
write(frame) {
|
|
70
|
+
return (this.#socket?.writeVideo({
|
|
71
|
+
keyframe: frame.keyframe,
|
|
72
|
+
timestampUs: frame.timestampUs,
|
|
73
|
+
data: frame.data,
|
|
74
|
+
}) ?? false);
|
|
75
|
+
}
|
|
76
|
+
/** @internal */
|
|
77
|
+
_attach(socket) {
|
|
78
|
+
this.#socket = socket;
|
|
79
|
+
}
|
|
80
|
+
/** @internal */
|
|
81
|
+
_frame(frame) {
|
|
82
|
+
this.emit("frame", {
|
|
83
|
+
source: frame.source,
|
|
84
|
+
keyframe: frame.keyframe,
|
|
85
|
+
timestampUs: frame.timestampUs,
|
|
86
|
+
data: frame.data,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** @internal */
|
|
90
|
+
_source(source) {
|
|
91
|
+
const neutral = videoSource(source);
|
|
92
|
+
this.#sources.set(neutral.id, neutral);
|
|
93
|
+
this.emit("source", neutral);
|
|
94
|
+
}
|
|
95
|
+
/** @internal */
|
|
96
|
+
_sourceRemoved(handle) {
|
|
97
|
+
if (!this.#sources.delete(handle))
|
|
98
|
+
return;
|
|
99
|
+
this.emit("sourceRemoved", handle);
|
|
100
|
+
}
|
|
101
|
+
/** @internal */
|
|
102
|
+
_clearSources() {
|
|
103
|
+
for (const handle of [...this.#sources.keys()])
|
|
104
|
+
this._sourceRemoved(handle);
|
|
105
|
+
}
|
|
106
|
+
/** @internal */
|
|
107
|
+
_keyframeRequest() {
|
|
108
|
+
this.emit("keyframeRequest");
|
|
109
|
+
}
|
|
110
|
+
/** @internal */
|
|
111
|
+
_detach() {
|
|
112
|
+
this.#socket = undefined;
|
|
113
|
+
this.#sources.clear();
|
|
114
|
+
this.removeAllListeners();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export const DEFAULT_CALL_CAPABILITIES = Object.freeze({
|
|
118
|
+
video: true,
|
|
119
|
+
invite: true,
|
|
120
|
+
});
|
|
121
|
+
/**
|
|
122
|
+
* Read platform-reported capabilities, keeping `fallback` (the defaults
|
|
123
|
+
* unless given) for absent or malformed fields.
|
|
124
|
+
*/
|
|
125
|
+
export function capabilitiesFrom(value, fallback = DEFAULT_CALL_CAPABILITIES) {
|
|
126
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
127
|
+
return fallback;
|
|
128
|
+
const reported = value;
|
|
129
|
+
return {
|
|
130
|
+
video: typeof reported["video"] === "boolean"
|
|
131
|
+
? reported["video"]
|
|
132
|
+
: fallback.video,
|
|
133
|
+
invite: typeof reported["invite"] === "boolean"
|
|
134
|
+
? reported["invite"]
|
|
135
|
+
: fallback.invite,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** Upper bound on the platform release after a local media failure. */
|
|
139
|
+
const RELEASE_TIMEOUT_MS = 5_000;
|
|
140
|
+
/**
|
|
141
|
+
* One call. Created by the client for every inbound `call.received` and every
|
|
142
|
+
* successful `place()`. Lifecycle transitions arrive from the client's socket;
|
|
143
|
+
* media arrives on this call's own connection once accepted.
|
|
144
|
+
*/
|
|
145
|
+
export class Call extends Emitter {
|
|
146
|
+
id;
|
|
147
|
+
session;
|
|
148
|
+
direction;
|
|
149
|
+
peer;
|
|
150
|
+
/** True when the call offered video and the call can carry it. */
|
|
151
|
+
get hasVideo() {
|
|
152
|
+
return this.#offeredVideo && this.#capabilities.video;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* What this call supports. An outbound call starts with the defaults and
|
|
156
|
+
* takes the platform's report from `call.accepted` (`capabilities` event).
|
|
157
|
+
*/
|
|
158
|
+
get capabilities() {
|
|
159
|
+
return this.#capabilities;
|
|
160
|
+
}
|
|
161
|
+
#offeredVideo;
|
|
162
|
+
#capabilities;
|
|
163
|
+
/** This client's media connection id; reused on reconnect. */
|
|
164
|
+
connectionId;
|
|
165
|
+
audio;
|
|
166
|
+
video;
|
|
167
|
+
#api;
|
|
168
|
+
#mediaOptions;
|
|
169
|
+
#participant;
|
|
170
|
+
#self;
|
|
171
|
+
#now;
|
|
172
|
+
#reconnectAttempts;
|
|
173
|
+
#participants = new Map();
|
|
174
|
+
#departedParticipants = new Set();
|
|
175
|
+
#participantRevisions = new Map();
|
|
176
|
+
#rosterRevision = 0;
|
|
177
|
+
#state;
|
|
178
|
+
#media;
|
|
179
|
+
#endReason;
|
|
180
|
+
#startedAt;
|
|
181
|
+
#connectedAt;
|
|
182
|
+
#endedAt;
|
|
183
|
+
/** The in-flight answer or join, through media attachment. */
|
|
184
|
+
#accepting;
|
|
185
|
+
/** The in-flight accept request alone, without media attachment. */
|
|
186
|
+
#acceptRequest;
|
|
187
|
+
/** This client's accept succeeded. */
|
|
188
|
+
#accepted = false;
|
|
189
|
+
#answered = false;
|
|
190
|
+
#answeredBy;
|
|
191
|
+
#exclusive = false;
|
|
192
|
+
#claimedByOther = false;
|
|
193
|
+
/** This client claimed the call: its own answer, or an exclusive placement. */
|
|
194
|
+
#claimedByUs = false;
|
|
195
|
+
#reconnectTimer;
|
|
196
|
+
#externalMedia;
|
|
197
|
+
#reporter;
|
|
198
|
+
/** Media attached at least once, so there is a connection to report on. */
|
|
199
|
+
#mediaAttached = false;
|
|
200
|
+
/** Successful media reattachments of this connection. */
|
|
201
|
+
#reconnects = 0;
|
|
202
|
+
#mediaReady = false;
|
|
203
|
+
constructor(init) {
|
|
204
|
+
super();
|
|
205
|
+
this.id = init.id;
|
|
206
|
+
this.session = init.session;
|
|
207
|
+
this.direction = init.direction;
|
|
208
|
+
this.peer = init.peer;
|
|
209
|
+
this.#capabilities = init.capabilities ?? DEFAULT_CALL_CAPABILITIES;
|
|
210
|
+
this.#offeredVideo = init.video;
|
|
211
|
+
if (init.connectionId !== undefined && !isConnectionId(init.connectionId))
|
|
212
|
+
throw new CallsError("invalid_connection_id", "connectionId must be 8–64 characters of A–Z, a–z, 0–9, _ or -.");
|
|
213
|
+
this.connectionId = init.connectionId ?? createConnectionId();
|
|
214
|
+
this.#api = init.api;
|
|
215
|
+
this.#mediaOptions = init.media;
|
|
216
|
+
this.#participant = init.participant;
|
|
217
|
+
this.#self = init.self ?? (() => undefined);
|
|
218
|
+
this.#externalMedia = init.mediaMode === "external";
|
|
219
|
+
const report = init.api.report;
|
|
220
|
+
this.#reporter =
|
|
221
|
+
init.diagnostics === undefined ||
|
|
222
|
+
this.#externalMedia ||
|
|
223
|
+
report === undefined
|
|
224
|
+
? undefined
|
|
225
|
+
: new CallReporter({
|
|
226
|
+
connectionId: this.connectionId,
|
|
227
|
+
client: init.diagnostics,
|
|
228
|
+
...(init.now === undefined ? {} : { now: init.now }),
|
|
229
|
+
send: (body) => report.call(init.api, this.id, {
|
|
230
|
+
...body,
|
|
231
|
+
...(init.participant === undefined
|
|
232
|
+
? {}
|
|
233
|
+
: { participant: init.participant }),
|
|
234
|
+
}),
|
|
235
|
+
});
|
|
236
|
+
this.#reconnectAttempts = init.reconnectAttempts ?? 3;
|
|
237
|
+
this.#now = init.now ?? Date.now;
|
|
238
|
+
this.#state = init.direction === "inbound" ? "incoming" : "ringing";
|
|
239
|
+
this.#startedAt = this.#now();
|
|
240
|
+
this.audio = new AudioTrack(16_000);
|
|
241
|
+
this.video = new VideoTrack();
|
|
242
|
+
// The placing participant owns an outbound call.
|
|
243
|
+
if (init.direction === "outbound") {
|
|
244
|
+
this.#accepted = true;
|
|
245
|
+
this.#claimedByUs = init.exclusive === true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
get state() {
|
|
249
|
+
return this.#state;
|
|
250
|
+
}
|
|
251
|
+
get ended() {
|
|
252
|
+
return this.#state === "ended";
|
|
253
|
+
}
|
|
254
|
+
get endReason() {
|
|
255
|
+
return this.#endReason;
|
|
256
|
+
}
|
|
257
|
+
get startedAt() {
|
|
258
|
+
return this.#startedAt;
|
|
259
|
+
}
|
|
260
|
+
get connectedAt() {
|
|
261
|
+
return this.#connectedAt;
|
|
262
|
+
}
|
|
263
|
+
/** Seconds of connected time; frozen at the end so a retained call stops counting. */
|
|
264
|
+
get duration() {
|
|
265
|
+
if (this.#connectedAt === undefined)
|
|
266
|
+
return 0;
|
|
267
|
+
const until = this.#endedAt ?? this.#now();
|
|
268
|
+
return Math.max(0, Math.floor((until - this.#connectedAt) / 1000));
|
|
269
|
+
}
|
|
270
|
+
get endedAt() {
|
|
271
|
+
return this.#endedAt;
|
|
272
|
+
}
|
|
273
|
+
get participants() {
|
|
274
|
+
return [...this.#participants.values()];
|
|
275
|
+
}
|
|
276
|
+
/** Answer and claim state, as last reported by the platform. */
|
|
277
|
+
get claim() {
|
|
278
|
+
return {
|
|
279
|
+
answered: this.#answered,
|
|
280
|
+
...(this.#answeredBy === undefined
|
|
281
|
+
? {}
|
|
282
|
+
: { answeredBy: this.#answeredBy }),
|
|
283
|
+
exclusive: this.#exclusive,
|
|
284
|
+
claimedByOther: this.claimedByOther,
|
|
285
|
+
canJoin: this.canJoin,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* @internal This client holds the claim: its own exclusive answer, or a
|
|
290
|
+
* placement with `exclusive: true`. Media adapters end such a call, rather
|
|
291
|
+
* than leave it, when its media fails.
|
|
292
|
+
*/
|
|
293
|
+
get _claimedBySelf() {
|
|
294
|
+
return this.#claimedByUs;
|
|
295
|
+
}
|
|
296
|
+
get claimedByOther() {
|
|
297
|
+
return this.#claimedByOther && !this.#accepted && !this.ended;
|
|
298
|
+
}
|
|
299
|
+
get canJoin() {
|
|
300
|
+
return (this.#state === "incoming" &&
|
|
301
|
+
this.#answered &&
|
|
302
|
+
!this.#exclusive &&
|
|
303
|
+
!this.#claimedByOther);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Answer a ringing call and attach media. With `exclusive: true` the call
|
|
307
|
+
* is claimed: other participants stop ringing and cannot join. Resolves
|
|
308
|
+
* once media is connected (after acceptance for external media). Rejects
|
|
309
|
+
* with {@link CallClaimedError} when another participant claimed the call;
|
|
310
|
+
* the call then stays visible as `claimedByOther`.
|
|
311
|
+
*/
|
|
312
|
+
answer(options = {}) {
|
|
313
|
+
return this.#accept({
|
|
314
|
+
exclusive: options.exclusive === true,
|
|
315
|
+
video: options.video ?? this.hasVideo,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Join a call another participant answered without claiming it. Never
|
|
320
|
+
* claims. Rejects with {@link CallClaimedError} when the call is claimed.
|
|
321
|
+
*/
|
|
322
|
+
join(options = {}) {
|
|
323
|
+
if (!this.#answered && this.#state === "incoming")
|
|
324
|
+
return Promise.reject(new CallsError("call_not_answered", "Nobody has answered this call yet; answer it instead."));
|
|
325
|
+
return this.#accept({
|
|
326
|
+
exclusive: false,
|
|
327
|
+
video: options.video ?? this.hasVideo,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
#accept(options) {
|
|
331
|
+
// A concurrent answer or join settles with the attempt already running,
|
|
332
|
+
// so it also waits for media and fails if media fails.
|
|
333
|
+
if (this.#accepting !== undefined)
|
|
334
|
+
return this.#accepting;
|
|
335
|
+
if (this.#state !== "incoming")
|
|
336
|
+
return Promise.reject(new CallsError("invalid_state", `Cannot answer a call in state "${this.#state}".`));
|
|
337
|
+
if (this.claimedByOther)
|
|
338
|
+
return Promise.reject(new CallClaimedError());
|
|
339
|
+
let accepted = false;
|
|
340
|
+
const accepting = (async () => {
|
|
341
|
+
this.#transition("connecting");
|
|
342
|
+
const result = await this.#api.accept(this.id, {
|
|
343
|
+
exclusive: options.exclusive,
|
|
344
|
+
video: options.video,
|
|
345
|
+
...(this.#participant === undefined
|
|
346
|
+
? {}
|
|
347
|
+
: { participant: this.#participant }),
|
|
348
|
+
});
|
|
349
|
+
accepted = true;
|
|
350
|
+
this.#accepted = true;
|
|
351
|
+
this.#claimedByUs = result.exclusive;
|
|
352
|
+
this.#applyClaim({
|
|
353
|
+
answered: result.answered,
|
|
354
|
+
answeredBy: result.answeredBy,
|
|
355
|
+
exclusive: result.exclusive,
|
|
356
|
+
});
|
|
357
|
+
})();
|
|
358
|
+
const run = accepting
|
|
359
|
+
.then(() => this.#bridge())
|
|
360
|
+
.catch(async (cause) => {
|
|
361
|
+
this.#accepting = undefined;
|
|
362
|
+
if (this.#state === "ended")
|
|
363
|
+
throw cause;
|
|
364
|
+
if (!accepted) {
|
|
365
|
+
// The accept itself was refused: nothing changed on the platform.
|
|
366
|
+
if (cause instanceof CallClaimedError) {
|
|
367
|
+
this.#claimedByOther = true;
|
|
368
|
+
this.#applyClaim({ answered: true, exclusive: true });
|
|
369
|
+
}
|
|
370
|
+
this.#transition("incoming");
|
|
371
|
+
throw cause;
|
|
372
|
+
}
|
|
373
|
+
this.#reportFailure(cause);
|
|
374
|
+
await this.#release(cause);
|
|
375
|
+
this.#mediaFailed(cause);
|
|
376
|
+
throw cause;
|
|
377
|
+
});
|
|
378
|
+
this.#accepting = run;
|
|
379
|
+
this.#acceptRequest = accepting;
|
|
380
|
+
void accepting
|
|
381
|
+
.finally(() => {
|
|
382
|
+
if (this.#acceptRequest === accepting)
|
|
383
|
+
this.#acceptRequest = undefined;
|
|
384
|
+
})
|
|
385
|
+
.catch(() => undefined);
|
|
386
|
+
// Once settled, later answer() and join() calls check the call's state
|
|
387
|
+
// again instead of reusing this result. Failures already clear it above.
|
|
388
|
+
void run.then(() => {
|
|
389
|
+
if (this.#accepting === run)
|
|
390
|
+
this.#accepting = undefined;
|
|
391
|
+
}, () => undefined);
|
|
392
|
+
return run;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Decline a ringing call. This ends the call for every participant, so
|
|
396
|
+
* applications decide when to call it; the client never declines on its own.
|
|
397
|
+
*/
|
|
398
|
+
async reject() {
|
|
399
|
+
if (this.#state !== "incoming")
|
|
400
|
+
throw new CallsError("invalid_state", `Cannot reject a call in state "${this.#state}".`);
|
|
401
|
+
if (this.#answered)
|
|
402
|
+
throw new CallsError("call_not_ringing", "The call is already answered. Join it or leave it; declining is not possible.");
|
|
403
|
+
await this.#api.reject(this.id, undefined, this.#participant);
|
|
404
|
+
this.#end("rejected");
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Leave the call: close this client's media connection. The call continues
|
|
408
|
+
* for everyone else. On a call this client never joined, only local
|
|
409
|
+
* tracking stops.
|
|
410
|
+
*/
|
|
411
|
+
async leave() {
|
|
412
|
+
if (this.#state === "ended")
|
|
413
|
+
return;
|
|
414
|
+
// Nobody else is on a placed call the remote party has not answered, so
|
|
415
|
+
// leaving it would keep that party ringing with no way to stop it.
|
|
416
|
+
if (this.direction === "outbound" && this.#state === "ringing") {
|
|
417
|
+
await this.end();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
// An accept still in flight may yet succeed and put this client on the
|
|
421
|
+
// call (possibly with a claim); wait for it, so a successful one is left
|
|
422
|
+
// on the platform too instead of only dropped here.
|
|
423
|
+
const acceptRequest = this.#acceptRequest;
|
|
424
|
+
if (!this.#accepted && acceptRequest !== undefined) {
|
|
425
|
+
await acceptRequest.catch(() => undefined);
|
|
426
|
+
if (this.ended)
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const attached = this.#accepted;
|
|
430
|
+
const media = this.#media;
|
|
431
|
+
// A refused request leaves the call live so it can be retried; the local
|
|
432
|
+
// model only goes terminal once this client is really out of the call.
|
|
433
|
+
if (attached && !(media?.leave() ?? false))
|
|
434
|
+
await this.#api.leave(this.id, this.connectionId, undefined, this.#participant);
|
|
435
|
+
this.#end("left");
|
|
436
|
+
}
|
|
437
|
+
/** End the call for every participant. Idempotent. */
|
|
438
|
+
async end() {
|
|
439
|
+
if (this.#state === "ended")
|
|
440
|
+
return;
|
|
441
|
+
await this.#api.end(this.id);
|
|
442
|
+
this.#end("hangup");
|
|
443
|
+
}
|
|
444
|
+
/** Invite another party, turning a 1:1 call into a group call. */
|
|
445
|
+
async addParticipant(to) {
|
|
446
|
+
if (this.#state === "ended")
|
|
447
|
+
throw new CallsError("invalid_state", "Cannot add a participant to an ended call.");
|
|
448
|
+
const requestedAtRevision = this.#rosterRevision;
|
|
449
|
+
const participant = await this.#api.addParticipant(this.id, to);
|
|
450
|
+
// The lifecycle stream may move this participant forward, or report it
|
|
451
|
+
// left, while the request is in flight. Its acknowledgement is then older
|
|
452
|
+
// than the roster and must not overwrite or restore that entry.
|
|
453
|
+
if ((this.#participantRevisions.get(participant.id) ?? 0) <=
|
|
454
|
+
requestedAtRevision)
|
|
455
|
+
this.#applyInviteReply(participant);
|
|
456
|
+
return participant;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* @internal `call.accepted` arrived. For an outbound call the remote party
|
|
460
|
+
* answered; for an inbound call it names who answered and whether they
|
|
461
|
+
* claimed it.
|
|
462
|
+
*/
|
|
463
|
+
async _remoteAccepted(claim = {}) {
|
|
464
|
+
if (!this.ended &&
|
|
465
|
+
claim.capabilities !== undefined &&
|
|
466
|
+
(claim.capabilities.video !== this.#capabilities.video ||
|
|
467
|
+
claim.capabilities.invite !== this.#capabilities.invite)) {
|
|
468
|
+
this.#capabilities = Object.freeze({ ...claim.capabilities });
|
|
469
|
+
this.emit("capabilities", this.#capabilities);
|
|
470
|
+
}
|
|
471
|
+
if (this.direction === "outbound") {
|
|
472
|
+
if (this.#state !== "ringing")
|
|
473
|
+
return;
|
|
474
|
+
// The remote party answered. A placement with `exclusive: true` holds
|
|
475
|
+
// the claim even when the event does not repeat it.
|
|
476
|
+
this.#applyClaim({
|
|
477
|
+
answered: true,
|
|
478
|
+
...(claim.answeredBy === undefined
|
|
479
|
+
? {}
|
|
480
|
+
: { answeredBy: claim.answeredBy }),
|
|
481
|
+
exclusive: claim.exclusive === true || this.#claimedByUs,
|
|
482
|
+
});
|
|
483
|
+
this.#transition("connecting");
|
|
484
|
+
try {
|
|
485
|
+
await this.#bridge();
|
|
486
|
+
}
|
|
487
|
+
catch (cause) {
|
|
488
|
+
this.#reportFailure(cause);
|
|
489
|
+
await this.#release(cause);
|
|
490
|
+
this.#mediaFailed(cause);
|
|
491
|
+
}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (this.ended)
|
|
495
|
+
return;
|
|
496
|
+
const exclusive = claim.exclusive === true;
|
|
497
|
+
const answeredBy = claim.answeredBy;
|
|
498
|
+
const self = this.#self();
|
|
499
|
+
const byUs = answeredBy !== undefined && self !== undefined && answeredBy === self;
|
|
500
|
+
if (exclusive && !byUs && !this.#accepted && this.#accepting === undefined)
|
|
501
|
+
this.#claimedByOther = true;
|
|
502
|
+
this.#applyClaim({
|
|
503
|
+
answered: true,
|
|
504
|
+
...(answeredBy === undefined ? {} : { answeredBy }),
|
|
505
|
+
exclusive,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
/** @internal The platform reported the call over. */
|
|
509
|
+
_remoteEnded(reason) {
|
|
510
|
+
this.#end(reason);
|
|
511
|
+
}
|
|
512
|
+
/** @internal Apply a validated participant update from either transport. */
|
|
513
|
+
_remoteParticipant(frame) {
|
|
514
|
+
if (this.ended)
|
|
515
|
+
return;
|
|
516
|
+
const participantId = frame.type === "participant_left"
|
|
517
|
+
? frame.participantId
|
|
518
|
+
: frame.participant.id;
|
|
519
|
+
this.#rosterRevision += 1;
|
|
520
|
+
this.#participantRevisions.set(participantId, this.#rosterRevision);
|
|
521
|
+
this.#applyParticipant(frame);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* @internal Media owned by another Polymorfa package connected. An outbound
|
|
525
|
+
* call still waits for the remote party to accept. Socket media ignores this.
|
|
526
|
+
*/
|
|
527
|
+
mediaConnected() {
|
|
528
|
+
if (!this.#externalMedia || this.ended)
|
|
529
|
+
return;
|
|
530
|
+
this.#mediaReady = true;
|
|
531
|
+
if (this.#state !== "connecting")
|
|
532
|
+
return;
|
|
533
|
+
this.#connectedAt = this.#now();
|
|
534
|
+
this.#transition("connected");
|
|
535
|
+
this.emit("connected");
|
|
536
|
+
}
|
|
537
|
+
#applyClaim(next) {
|
|
538
|
+
const before = this.claim;
|
|
539
|
+
this.#answered = this.#answered || next.answered;
|
|
540
|
+
if (next.answeredBy !== undefined)
|
|
541
|
+
this.#answeredBy = next.answeredBy;
|
|
542
|
+
this.#exclusive = next.exclusive;
|
|
543
|
+
const after = this.claim;
|
|
544
|
+
if (before.answered !== after.answered ||
|
|
545
|
+
before.answeredBy !== after.answeredBy ||
|
|
546
|
+
before.exclusive !== after.exclusive ||
|
|
547
|
+
before.claimedByOther !== after.claimedByOther ||
|
|
548
|
+
before.canJoin !== after.canJoin)
|
|
549
|
+
this.emit("claim", after);
|
|
550
|
+
}
|
|
551
|
+
#applyInviteReply(participant) {
|
|
552
|
+
if (this.ended)
|
|
553
|
+
return;
|
|
554
|
+
const previous = this.#participants.get(participant.id);
|
|
555
|
+
if (previous !== undefined &&
|
|
556
|
+
participantStateRank(participant.state) <
|
|
557
|
+
participantStateRank(previous.state))
|
|
558
|
+
return;
|
|
559
|
+
if (previous !== undefined && sameParticipant(previous, participant))
|
|
560
|
+
return;
|
|
561
|
+
this.#rosterRevision += 1;
|
|
562
|
+
this.#participantRevisions.set(participant.id, this.#rosterRevision);
|
|
563
|
+
this.#applyParticipant({ type: "participant_joined", participant });
|
|
564
|
+
}
|
|
565
|
+
#applyParticipant(frame) {
|
|
566
|
+
if (this.ended)
|
|
567
|
+
return;
|
|
568
|
+
if (frame.type === "participant_left") {
|
|
569
|
+
if (this.#departedParticipants.has(frame.participantId))
|
|
570
|
+
return;
|
|
571
|
+
this.#departedParticipants.add(frame.participantId);
|
|
572
|
+
this.#participants.delete(frame.participantId);
|
|
573
|
+
this.emit("participantLeft", frame.participantId, frame.reason);
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const participant = frame.participant;
|
|
577
|
+
if (participant.state === "left") {
|
|
578
|
+
this.#applyParticipant({
|
|
579
|
+
type: "participant_left",
|
|
580
|
+
participantId: participant.id,
|
|
581
|
+
});
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
this.#departedParticipants.delete(participant.id);
|
|
585
|
+
const previous = this.#participants.get(participant.id);
|
|
586
|
+
if (previous !== undefined && sameParticipant(previous, participant))
|
|
587
|
+
return;
|
|
588
|
+
this.#participants.set(participant.id, participant);
|
|
589
|
+
if (previous === undefined)
|
|
590
|
+
this.emit("participantJoined", participant);
|
|
591
|
+
else
|
|
592
|
+
this.emit("participantState", participant);
|
|
593
|
+
}
|
|
594
|
+
#mediaFailed(cause) {
|
|
595
|
+
if (this.ended)
|
|
596
|
+
return;
|
|
597
|
+
// The platform accepted; only our media failed. End this client's view
|
|
598
|
+
// of the call and say why.
|
|
599
|
+
this.emit("error", cause instanceof CallsError
|
|
600
|
+
? cause
|
|
601
|
+
: new CallsError("media_failed", cause instanceof Error ? cause.message : "Media could not attach.", { cause }));
|
|
602
|
+
this.#end(cause instanceof CallClaimedError ? "claimed" : "connection_failed");
|
|
603
|
+
}
|
|
604
|
+
async #bridge() {
|
|
605
|
+
if (this.#externalMedia) {
|
|
606
|
+
if (this.#mediaReady)
|
|
607
|
+
this.mediaConnected();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const ready = await this.#attach(false);
|
|
611
|
+
if (ready === undefined)
|
|
612
|
+
return;
|
|
613
|
+
this.#connectedAt = this.#now();
|
|
614
|
+
this.#transition("connected");
|
|
615
|
+
this.emit("connected");
|
|
616
|
+
}
|
|
617
|
+
/** Open one media socket with this call's connection id. */
|
|
618
|
+
async #attach(refreshToken) {
|
|
619
|
+
if (this.ended)
|
|
620
|
+
throw new CallsError("call_ended", "Call ended before media attached.");
|
|
621
|
+
const media = new MediaSocket({
|
|
622
|
+
...this.#mediaOptions,
|
|
623
|
+
api: this.#api,
|
|
624
|
+
callId: this.id,
|
|
625
|
+
connectionId: this.connectionId,
|
|
626
|
+
...(this.#participant === undefined
|
|
627
|
+
? {}
|
|
628
|
+
: { participant: this.#participant }),
|
|
629
|
+
refreshToken,
|
|
630
|
+
});
|
|
631
|
+
this.#media = media;
|
|
632
|
+
media.on("audio", (pcm) => this.audio._push(pcm));
|
|
633
|
+
media.on("video", (frame) => this.video._frame(frame));
|
|
634
|
+
media.on("videoSource", (source) => this.video._source(source));
|
|
635
|
+
media.on("videoSourceRemoved", (handle) => this.video._sourceRemoved(handle));
|
|
636
|
+
media.on("keyframeRequest", () => this.video._keyframeRequest());
|
|
637
|
+
media.on("participantJoined", (participant) => this._remoteParticipant({ type: "participant_joined", participant }));
|
|
638
|
+
media.on("participantState", (participant) => this._remoteParticipant({ type: "participant_state", participant }));
|
|
639
|
+
media.on("participantLeft", (participantId, reason) => this._remoteParticipant({
|
|
640
|
+
type: "participant_left",
|
|
641
|
+
participantId,
|
|
642
|
+
...(reason === undefined ? {} : { reason }),
|
|
643
|
+
}));
|
|
644
|
+
media.on("error", (error) => this.emit("error", error));
|
|
645
|
+
let ready;
|
|
646
|
+
const readyListener = (value) => {
|
|
647
|
+
ready = value;
|
|
648
|
+
};
|
|
649
|
+
media.once("ready", readyListener);
|
|
650
|
+
let attached = false;
|
|
651
|
+
media.on("close", (close) => {
|
|
652
|
+
if (attached)
|
|
653
|
+
this.#mediaClosed(media, close);
|
|
654
|
+
});
|
|
655
|
+
try {
|
|
656
|
+
await media.connect();
|
|
657
|
+
}
|
|
658
|
+
catch (cause) {
|
|
659
|
+
if (this.#media === media)
|
|
660
|
+
this.#media = undefined;
|
|
661
|
+
media.close();
|
|
662
|
+
throw cause;
|
|
663
|
+
}
|
|
664
|
+
if (this.ended || this.#media !== media) {
|
|
665
|
+
media.close();
|
|
666
|
+
return undefined;
|
|
667
|
+
}
|
|
668
|
+
if (!media.connected) {
|
|
669
|
+
// Closed between `ready` and this continuation.
|
|
670
|
+
this.#media = undefined;
|
|
671
|
+
throw new CallsError("media_closed", "Media connection closed.");
|
|
672
|
+
}
|
|
673
|
+
attached = true;
|
|
674
|
+
this.#mediaAttached = true;
|
|
675
|
+
this.audio._attach(media, ready?.sampleRate ?? media.sampleRate);
|
|
676
|
+
this.video._attach(media);
|
|
677
|
+
return ready;
|
|
678
|
+
}
|
|
679
|
+
#mediaClosed(media, close) {
|
|
680
|
+
if (this.#media !== media || this.ended)
|
|
681
|
+
return;
|
|
682
|
+
this.#media = undefined;
|
|
683
|
+
this.audio._attach(undefined);
|
|
684
|
+
this.video._attach(undefined);
|
|
685
|
+
this.video._clearSources();
|
|
686
|
+
switch (close.reason) {
|
|
687
|
+
case "local":
|
|
688
|
+
return;
|
|
689
|
+
case "left":
|
|
690
|
+
this.#end("left");
|
|
691
|
+
return;
|
|
692
|
+
case "ended":
|
|
693
|
+
this.#end("remote_hangup");
|
|
694
|
+
return;
|
|
695
|
+
case "claimed":
|
|
696
|
+
this.#end("claimed");
|
|
697
|
+
return;
|
|
698
|
+
case "refused":
|
|
699
|
+
this.#reporter?.error("other");
|
|
700
|
+
void this.#failAfterRelease(new CallsError("media_refused", "The platform refused the media connection."));
|
|
701
|
+
return;
|
|
702
|
+
case "unauthorized":
|
|
703
|
+
case "lost":
|
|
704
|
+
this.#reconnect(close.reason === "unauthorized", 0);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
#reconnect(refreshToken, attempt) {
|
|
709
|
+
if (this.ended)
|
|
710
|
+
return;
|
|
711
|
+
if (attempt >= this.#reconnectAttempts) {
|
|
712
|
+
this.#reporter?.error(refreshToken ? "token_refresh_failed" : "reconnect_exhausted");
|
|
713
|
+
void this.#failAfterRelease(refreshToken
|
|
714
|
+
? new CallsAuthError()
|
|
715
|
+
: new CallsError("media_lost", "Media connection lost."));
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
this.#transition("reconnecting");
|
|
719
|
+
const delay = Math.min(8_000, 1_000 * 2 ** attempt);
|
|
720
|
+
this.#reconnectTimer = (this.#mediaOptions.setTimeout ?? setTimeout)(() => {
|
|
721
|
+
this.#reconnectTimer = undefined;
|
|
722
|
+
void this.#attach(refreshToken).then((ready) => {
|
|
723
|
+
if (ready === undefined || this.ended)
|
|
724
|
+
return;
|
|
725
|
+
this.#reconnects += 1;
|
|
726
|
+
this.#transition("connected");
|
|
727
|
+
}, (cause) => {
|
|
728
|
+
if (this.ended)
|
|
729
|
+
return;
|
|
730
|
+
if (cause instanceof CallClaimedError) {
|
|
731
|
+
this.#end("claimed");
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (!retryable(cause)) {
|
|
735
|
+
// Refused or no longer available: reattaching cannot help.
|
|
736
|
+
this.#reportFailure(cause);
|
|
737
|
+
void this.#failAfterRelease(cause instanceof CallsError
|
|
738
|
+
? cause
|
|
739
|
+
: new CallsError("media_failed", "Media could not attach."));
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
this.#reconnect(cause instanceof CallsAuthError || refreshToken, attempt + 1);
|
|
743
|
+
});
|
|
744
|
+
}, delay);
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* The platform still counts this client as on the call after its media
|
|
748
|
+
* failed. Release it before the call becomes terminal here, so the remote
|
|
749
|
+
* party is not left on a call nobody controls: end a call this client
|
|
750
|
+
* claimed (nobody else can take it over), otherwise leave with this
|
|
751
|
+
* connection. A claim by someone else needs nothing. Bounded by
|
|
752
|
+
* RELEASE_TIMEOUT_MS; failures are swallowed so the media error stays the
|
|
753
|
+
* one reported. External media adapters release the call themselves.
|
|
754
|
+
*/
|
|
755
|
+
async #release(cause) {
|
|
756
|
+
if (this.ended || !this.#accepted || this.#externalMedia)
|
|
757
|
+
return;
|
|
758
|
+
if (cause instanceof CallClaimedError)
|
|
759
|
+
return;
|
|
760
|
+
const abort = new AbortController();
|
|
761
|
+
let timer;
|
|
762
|
+
const timeout = new Promise((resolve) => {
|
|
763
|
+
timer = (this.#mediaOptions.setTimeout ?? setTimeout)(() => {
|
|
764
|
+
abort.abort();
|
|
765
|
+
resolve();
|
|
766
|
+
}, RELEASE_TIMEOUT_MS);
|
|
767
|
+
});
|
|
768
|
+
const request = this.#claimedByUs
|
|
769
|
+
? this.#api.end(this.id, abort.signal)
|
|
770
|
+
: this.#api.leave(this.id, this.connectionId, abort.signal, this.#participant);
|
|
771
|
+
try {
|
|
772
|
+
await Promise.race([request.catch(() => undefined), timeout]);
|
|
773
|
+
}
|
|
774
|
+
finally {
|
|
775
|
+
(this.#mediaOptions.clearTimeout ?? clearTimeout)(timer);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* Report why media failed. Claims by another participant are not a media
|
|
780
|
+
* failure; causes without a closer code are `other`.
|
|
781
|
+
*/
|
|
782
|
+
#reportFailure(cause) {
|
|
783
|
+
if (this.#reporter === undefined || cause instanceof CallClaimedError)
|
|
784
|
+
return;
|
|
785
|
+
this.#reporter.error(cause instanceof CallsAuthError ||
|
|
786
|
+
(cause instanceof CallsError && cause.code === "token_failed")
|
|
787
|
+
? "token_refresh_failed"
|
|
788
|
+
: cause instanceof CallsError && cause.code === "media_timeout"
|
|
789
|
+
? "media_timeout"
|
|
790
|
+
: "other");
|
|
791
|
+
}
|
|
792
|
+
/** Release the call on the platform, then report `error` and end here. */
|
|
793
|
+
async #failAfterRelease(error) {
|
|
794
|
+
await this.#release();
|
|
795
|
+
if (this.ended)
|
|
796
|
+
return;
|
|
797
|
+
this.emit("error", error);
|
|
798
|
+
this.#end("connection_failed");
|
|
799
|
+
}
|
|
800
|
+
#transition(next) {
|
|
801
|
+
const previous = this.#state;
|
|
802
|
+
if (previous === next)
|
|
803
|
+
return;
|
|
804
|
+
this.#state = next;
|
|
805
|
+
this.emit("state", next, previous);
|
|
806
|
+
}
|
|
807
|
+
#end(reason) {
|
|
808
|
+
if (this.#state === "ended")
|
|
809
|
+
return;
|
|
810
|
+
this.#endReason = reason;
|
|
811
|
+
this.#endedAt = this.#now();
|
|
812
|
+
if (this.#reconnectTimer !== undefined) {
|
|
813
|
+
(this.#mediaOptions.clearTimeout ?? clearTimeout)(this.#reconnectTimer);
|
|
814
|
+
this.#reconnectTimer = undefined;
|
|
815
|
+
}
|
|
816
|
+
this.#transition("ended");
|
|
817
|
+
// The connection's final figure: how often it reconnected. Only real
|
|
818
|
+
// measurements are sent, and socket media measures nothing else.
|
|
819
|
+
if (this.#mediaAttached)
|
|
820
|
+
this.#reporter?.quality({ reconnects: this.#reconnects }, true);
|
|
821
|
+
this.#reporter?.stop();
|
|
822
|
+
const media = this.#media;
|
|
823
|
+
this.#media = undefined;
|
|
824
|
+
media?.close();
|
|
825
|
+
this.audio._detach();
|
|
826
|
+
this.video._detach();
|
|
827
|
+
this.emit("ended", reason);
|
|
828
|
+
this.removeAllListeners();
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/** Whether a failed reattach is worth another attempt. */
|
|
832
|
+
function retryable(cause) {
|
|
833
|
+
if (cause instanceof CallsAuthError)
|
|
834
|
+
return true;
|
|
835
|
+
if (!(cause instanceof CallsError))
|
|
836
|
+
return true;
|
|
837
|
+
return ["media_closed", "media_timeout", "token_failed"].includes(cause.code);
|
|
838
|
+
}
|
|
839
|
+
function participantStateRank(state) {
|
|
840
|
+
switch (state) {
|
|
841
|
+
case "invited":
|
|
842
|
+
return 0;
|
|
843
|
+
case "ringing":
|
|
844
|
+
return 1;
|
|
845
|
+
case "connected":
|
|
846
|
+
return 2;
|
|
847
|
+
case "left":
|
|
848
|
+
return 3;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function sameParticipant(a, b) {
|
|
852
|
+
return (a.id === b.id &&
|
|
853
|
+
a.phoneNumber === b.phoneNumber &&
|
|
854
|
+
a.bsuid === b.bsuid &&
|
|
855
|
+
a.username === b.username &&
|
|
856
|
+
a.audioMuted === b.audioMuted &&
|
|
857
|
+
a.video === b.video &&
|
|
858
|
+
a.state === b.state);
|
|
859
|
+
}
|
|
860
|
+
//# sourceMappingURL=call.js.map
|