@agatx/serenada-core 0.6.10

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.
Files changed (156) hide show
  1. package/dist/ConsoleLogger.d.ts +6 -0
  2. package/dist/ConsoleLogger.d.ts.map +1 -0
  3. package/dist/ConsoleLogger.js +21 -0
  4. package/dist/ConsoleLogger.js.map +1 -0
  5. package/dist/RoomWatcher.d.ts +34 -0
  6. package/dist/RoomWatcher.d.ts.map +1 -0
  7. package/dist/RoomWatcher.js +103 -0
  8. package/dist/RoomWatcher.js.map +1 -0
  9. package/dist/SerenadaCore.d.ts +47 -0
  10. package/dist/SerenadaCore.d.ts.map +1 -0
  11. package/dist/SerenadaCore.js +141 -0
  12. package/dist/SerenadaCore.js.map +1 -0
  13. package/dist/SerenadaDiagnostics.d.ts +49 -0
  14. package/dist/SerenadaDiagnostics.d.ts.map +1 -0
  15. package/dist/SerenadaDiagnostics.js +421 -0
  16. package/dist/SerenadaDiagnostics.js.map +1 -0
  17. package/dist/SerenadaServerProvider.d.ts +48 -0
  18. package/dist/SerenadaServerProvider.d.ts.map +1 -0
  19. package/dist/SerenadaServerProvider.js +296 -0
  20. package/dist/SerenadaServerProvider.js.map +1 -0
  21. package/dist/SerenadaSession.d.ts +180 -0
  22. package/dist/SerenadaSession.d.ts.map +1 -0
  23. package/dist/SerenadaSession.js +1082 -0
  24. package/dist/SerenadaSession.js.map +1 -0
  25. package/dist/SignalingProvider.d.ts +132 -0
  26. package/dist/SignalingProvider.d.ts.map +1 -0
  27. package/dist/SignalingProvider.js +50 -0
  28. package/dist/SignalingProvider.js.map +1 -0
  29. package/dist/api/roomApi.d.ts +2 -0
  30. package/dist/api/roomApi.d.ts.map +1 -0
  31. package/dist/api/roomApi.js +14 -0
  32. package/dist/api/roomApi.js.map +1 -0
  33. package/dist/cameraModes.d.ts +13 -0
  34. package/dist/cameraModes.d.ts.map +1 -0
  35. package/dist/cameraModes.js +35 -0
  36. package/dist/cameraModes.js.map +1 -0
  37. package/dist/configValidation.d.ts +10 -0
  38. package/dist/configValidation.d.ts.map +1 -0
  39. package/dist/configValidation.js +24 -0
  40. package/dist/configValidation.js.map +1 -0
  41. package/dist/constants.d.ts +33 -0
  42. package/dist/constants.d.ts.map +1 -0
  43. package/dist/constants.js +65 -0
  44. package/dist/constants.js.map +1 -0
  45. package/dist/formatError.d.ts +3 -0
  46. package/dist/formatError.d.ts.map +1 -0
  47. package/dist/formatError.js +7 -0
  48. package/dist/formatError.js.map +1 -0
  49. package/dist/iceServers.d.ts +2 -0
  50. package/dist/iceServers.d.ts.map +1 -0
  51. package/dist/iceServers.js +21 -0
  52. package/dist/iceServers.js.map +1 -0
  53. package/dist/index.d.ts +55 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +44 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/layout/computeLayout.d.ts +81 -0
  58. package/dist/layout/computeLayout.d.ts.map +1 -0
  59. package/dist/layout/computeLayout.js +380 -0
  60. package/dist/layout/computeLayout.js.map +1 -0
  61. package/dist/media/AudioLevelMonitor.d.ts +51 -0
  62. package/dist/media/AudioLevelMonitor.d.ts.map +1 -0
  63. package/dist/media/AudioLevelMonitor.js +179 -0
  64. package/dist/media/AudioLevelMonitor.js.map +1 -0
  65. package/dist/media/MediaEngine.d.ts +137 -0
  66. package/dist/media/MediaEngine.d.ts.map +1 -0
  67. package/dist/media/MediaEngine.js +1224 -0
  68. package/dist/media/MediaEngine.js.map +1 -0
  69. package/dist/media/callStats.d.ts +16 -0
  70. package/dist/media/callStats.d.ts.map +1 -0
  71. package/dist/media/callStats.js +214 -0
  72. package/dist/media/callStats.js.map +1 -0
  73. package/dist/media/localVideoRecovery.d.ts +16 -0
  74. package/dist/media/localVideoRecovery.d.ts.map +1 -0
  75. package/dist/media/localVideoRecovery.js +14 -0
  76. package/dist/media/localVideoRecovery.js.map +1 -0
  77. package/dist/recoveryStorage.d.ts +33 -0
  78. package/dist/recoveryStorage.d.ts.map +1 -0
  79. package/dist/recoveryStorage.js +88 -0
  80. package/dist/recoveryStorage.js.map +1 -0
  81. package/dist/serverUrls.d.ts +8 -0
  82. package/dist/serverUrls.d.ts.map +1 -0
  83. package/dist/serverUrls.js +65 -0
  84. package/dist/serverUrls.js.map +1 -0
  85. package/dist/signaling/SignalingEngine.d.ts +126 -0
  86. package/dist/signaling/SignalingEngine.d.ts.map +1 -0
  87. package/dist/signaling/SignalingEngine.js +720 -0
  88. package/dist/signaling/SignalingEngine.js.map +1 -0
  89. package/dist/signaling/payloads.d.ts +76 -0
  90. package/dist/signaling/payloads.d.ts.map +1 -0
  91. package/dist/signaling/payloads.js +160 -0
  92. package/dist/signaling/payloads.js.map +1 -0
  93. package/dist/signaling/roomStatuses.d.ts +9 -0
  94. package/dist/signaling/roomStatuses.d.ts.map +1 -0
  95. package/dist/signaling/roomStatuses.js +71 -0
  96. package/dist/signaling/roomStatuses.js.map +1 -0
  97. package/dist/signaling/transportConfig.d.ts +3 -0
  98. package/dist/signaling/transportConfig.d.ts.map +1 -0
  99. package/dist/signaling/transportConfig.js +27 -0
  100. package/dist/signaling/transportConfig.js.map +1 -0
  101. package/dist/signaling/transports/index.d.ts +13 -0
  102. package/dist/signaling/transports/index.d.ts.map +1 -0
  103. package/dist/signaling/transports/index.js +11 -0
  104. package/dist/signaling/transports/index.js.map +1 -0
  105. package/dist/signaling/transports/sse.d.ts +26 -0
  106. package/dist/signaling/transports/sse.d.ts.map +1 -0
  107. package/dist/signaling/transports/sse.js +131 -0
  108. package/dist/signaling/transports/sse.js.map +1 -0
  109. package/dist/signaling/transports/types.d.ts +17 -0
  110. package/dist/signaling/transports/types.d.ts.map +1 -0
  111. package/dist/signaling/transports/types.js +2 -0
  112. package/dist/signaling/transports/types.js.map +1 -0
  113. package/dist/signaling/transports/ws.d.ts +21 -0
  114. package/dist/signaling/transports/ws.d.ts.map +1 -0
  115. package/dist/signaling/transports/ws.js +93 -0
  116. package/dist/signaling/transports/ws.js.map +1 -0
  117. package/dist/signaling/types.d.ts +53 -0
  118. package/dist/signaling/types.d.ts.map +1 -0
  119. package/dist/signaling/types.js +2 -0
  120. package/dist/signaling/types.js.map +1 -0
  121. package/dist/types.d.ts +279 -0
  122. package/dist/types.d.ts.map +1 -0
  123. package/dist/types.js +3 -0
  124. package/dist/types.js.map +1 -0
  125. package/package.json +43 -0
  126. package/src/ConsoleLogger.ts +14 -0
  127. package/src/RoomWatcher.ts +127 -0
  128. package/src/SerenadaCore.ts +163 -0
  129. package/src/SerenadaDiagnostics.ts +485 -0
  130. package/src/SerenadaServerProvider.ts +362 -0
  131. package/src/SerenadaSession.ts +1258 -0
  132. package/src/SignalingProvider.ts +207 -0
  133. package/src/api/roomApi.ts +16 -0
  134. package/src/cameraModes.ts +34 -0
  135. package/src/configValidation.ts +35 -0
  136. package/src/constants.ts +77 -0
  137. package/src/formatError.ts +5 -0
  138. package/src/iceServers.ts +20 -0
  139. package/src/index.ts +155 -0
  140. package/src/layout/computeLayout.ts +639 -0
  141. package/src/media/AudioLevelMonitor.ts +190 -0
  142. package/src/media/MediaEngine.ts +1183 -0
  143. package/src/media/callStats.ts +260 -0
  144. package/src/media/localVideoRecovery.ts +39 -0
  145. package/src/recoveryStorage.ts +101 -0
  146. package/src/serverUrls.ts +69 -0
  147. package/src/signaling/SignalingEngine.ts +762 -0
  148. package/src/signaling/payloads.ts +215 -0
  149. package/src/signaling/roomStatuses.ts +89 -0
  150. package/src/signaling/transportConfig.ts +30 -0
  151. package/src/signaling/transports/index.ts +26 -0
  152. package/src/signaling/transports/sse.ts +146 -0
  153. package/src/signaling/transports/types.ts +19 -0
  154. package/src/signaling/transports/ws.ts +108 -0
  155. package/src/signaling/types.ts +68 -0
  156. package/src/types.ts +299 -0
@@ -0,0 +1,127 @@
1
+ import type { RoomWatcherState, SerenadaConfig } from './types.js';
2
+ import { resolveServerUrls } from './serverUrls.js';
3
+ import { SignalingEngine, type SignalingEngineConfig } from './signaling/SignalingEngine.js';
4
+ import type { RoomStatuses } from './signaling/roomStatuses.js';
5
+ import { requireServerHost } from './configValidation.js';
6
+
7
+ type RoomWatcherListener = (state: RoomWatcherState) => void;
8
+
9
+ interface RoomWatcherDependencies {
10
+ createSignalingEngine?: (config: SignalingEngineConfig) => SignalingEngine;
11
+ }
12
+
13
+ /**
14
+ * Monitors room occupancy via the signaling server.
15
+ * Use to show room status (e.g. "1 person waiting") before joining a call.
16
+ */
17
+ export class RoomWatcher {
18
+ private readonly signaling: SignalingEngine;
19
+ private readonly unsubscribeStateChange: () => void;
20
+ private listeners = new Set<RoomWatcherListener>();
21
+ private watchedRoomIds: string[] = [];
22
+ private hasConnected = false;
23
+ private stopped = false;
24
+ private wasConnected = false;
25
+
26
+ constructor(config: SerenadaConfig, dependencies: RoomWatcherDependencies = {}) {
27
+ const serverHost = requireServerHost(config);
28
+ const urls = resolveServerUrls(serverHost);
29
+ this.signaling = (dependencies.createSignalingEngine ?? ((engineConfig) => new SignalingEngine(engineConfig)))({
30
+ wsUrl: urls.wsUrl,
31
+ httpBaseUrl: urls.httpBaseUrl,
32
+ transports: config.transports,
33
+ });
34
+ this.unsubscribeStateChange = this.signaling.onStateChange(() => {
35
+ const nowConnected = this.signaling.isConnected;
36
+ if (nowConnected && !this.wasConnected && this.watchedRoomIds.length > 0) {
37
+ this.signaling.watchRooms(this.watchedRoomIds);
38
+ }
39
+ this.wasConnected = nowConnected;
40
+ this.notify();
41
+ });
42
+ }
43
+
44
+ get isConnected(): boolean {
45
+ return !this.stopped && this.signaling.isConnected;
46
+ }
47
+
48
+ get activeTransport() {
49
+ return this.stopped ? null : this.signaling.activeTransport;
50
+ }
51
+
52
+ get currentStatuses(): RoomStatuses {
53
+ return filterRoomStatuses(this.signaling.roomStatuses, this.watchedRoomIds);
54
+ }
55
+
56
+ /** Subscribe to room status updates. Returns an unsubscribe function. */
57
+ subscribe(listener: RoomWatcherListener): () => void {
58
+ this.listeners.add(listener);
59
+ listener(this.snapshot());
60
+ return () => {
61
+ this.listeners.delete(listener);
62
+ };
63
+ }
64
+
65
+ /** Start watching the given room IDs for occupancy changes. Replaces any previous watch list. */
66
+ watchRooms(roomIds: string[]): void {
67
+ if (this.stopped) return;
68
+
69
+ this.watchedRoomIds = Array.from(new Set(roomIds.filter((roomId): roomId is string => typeof roomId === 'string' && roomId.length > 0)));
70
+ this.notify();
71
+
72
+ if (this.watchedRoomIds.length === 0) {
73
+ if (this.hasConnected && this.signaling.isConnected) {
74
+ this.signaling.watchRooms([]);
75
+ }
76
+ return;
77
+ }
78
+
79
+ if (!this.hasConnected) {
80
+ this.hasConnected = true;
81
+ this.signaling.connect();
82
+ return;
83
+ }
84
+
85
+ if (this.signaling.isConnected) {
86
+ this.signaling.watchRooms(this.watchedRoomIds);
87
+ }
88
+ }
89
+
90
+ /** Stop watching all rooms and release resources. */
91
+ stop(): void {
92
+ if (this.stopped) return;
93
+
94
+ this.stopped = true;
95
+ this.watchedRoomIds = [];
96
+ this.unsubscribeStateChange();
97
+ this.signaling.destroy();
98
+ this.notify();
99
+ this.listeners.clear();
100
+ }
101
+
102
+ private notify(): void {
103
+ const state = this.snapshot();
104
+ for (const listener of this.listeners) {
105
+ listener(state);
106
+ }
107
+ }
108
+
109
+ private snapshot(): RoomWatcherState {
110
+ return {
111
+ isConnected: this.isConnected,
112
+ activeTransport: this.activeTransport,
113
+ roomStatuses: this.currentStatuses,
114
+ };
115
+ }
116
+ }
117
+
118
+ function filterRoomStatuses(roomStatuses: RoomStatuses, watchedRoomIds: string[]): RoomStatuses {
119
+ if (watchedRoomIds.length === 0) {
120
+ return {};
121
+ }
122
+
123
+ const watched = new Set(watchedRoomIds);
124
+ return Object.fromEntries(
125
+ Object.entries(roomStatuses).filter(([roomId]) => watched.has(roomId)),
126
+ );
127
+ }
@@ -0,0 +1,163 @@
1
+ import type { SerenadaConfig, CallState, CreateRoomResult, SerenadaSessionHandle } from './types.js';
2
+ import { SerenadaSession } from './SerenadaSession.js';
3
+ import { createRoomId } from './api/roomApi.js';
4
+ import { buildRoomUrl } from './serverUrls.js';
5
+ import type { ResolvedSerenadaConfig } from './configValidation.js';
6
+ import { requireServerHost, resolveSerenadaConfig } from './configValidation.js';
7
+ import { SerenadaServerProvider } from './SerenadaServerProvider.js';
8
+ import type { PeerMessage, SignalingProvider } from './SignalingProvider.js';
9
+ import {
10
+ clearRecoveryRecord,
11
+ loadRecoveryRecord,
12
+ type RecoveryRecord,
13
+ } from './recoveryStorage.js';
14
+
15
+ /**
16
+ * Main entry point for the Serenada SDK.
17
+ * Create an instance with a {@link SerenadaConfig}, then use {@link join} or
18
+ * {@link createRoom} to start a call.
19
+ */
20
+ export class SerenadaCore {
21
+ private readonly config: SerenadaConfig;
22
+ private readonly resolvedConfig: ResolvedSerenadaConfig;
23
+
24
+ constructor(config: SerenadaConfig) {
25
+ this.config = config;
26
+ this.resolvedConfig = resolveSerenadaConfig(config);
27
+ }
28
+
29
+ /** Check if the current browser supports WebRTC calling. */
30
+ static isSupported(): boolean {
31
+ return typeof RTCPeerConnection !== 'undefined';
32
+ }
33
+
34
+ /**
35
+ * Returns a recoverable session if the previous tab/page session ended
36
+ * abruptly (reload, OS-level crash) while a call was active and the
37
+ * persisted reconnect token is still within its TTL. Host apps should
38
+ * call this on launch and surface a "Rejoin call?" prompt — calling
39
+ * {@link join} with the returned `roomId` reattaches under the same CID.
40
+ *
41
+ * Returns `null` when there is nothing to recover.
42
+ */
43
+ static getRecoverableSession(): RecoveryRecord | null {
44
+ return loadRecoveryRecord();
45
+ }
46
+
47
+ /**
48
+ * Drops any persisted recovery record. Host apps call this when the
49
+ * user explicitly declines to rejoin, so subsequent launches do not
50
+ * keep prompting for the same dead session.
51
+ */
52
+ static discardRecoverableSession(): void {
53
+ clearRecoveryRecord();
54
+ }
55
+
56
+ /** Join an existing call by URL. Returns a session handle. */
57
+ join(url: string, options?: { displayName?: string; peerId?: string }): SerenadaSessionHandle;
58
+ /** Join an existing call by room ID. Returns a session handle. */
59
+ join(options: { roomId: string; displayName?: string; peerId?: string }): SerenadaSessionHandle;
60
+ join(
61
+ urlOrOptions: string | { roomId: string; displayName?: string; peerId?: string },
62
+ extraOptions?: { displayName?: string; peerId?: string },
63
+ ): SerenadaSessionHandle {
64
+ if (!SerenadaCore.isSupported()) {
65
+ return this.createUnsupportedSession();
66
+ }
67
+ const signalingProvider = this.createSignalingProvider();
68
+ if (typeof urlOrOptions === 'string') {
69
+ const roomId = this.parseRoomIdFromUrl(urlOrOptions);
70
+ return new SerenadaSession(this.config, roomId, urlOrOptions, signalingProvider, {
71
+ displayName: extraOptions?.displayName,
72
+ peerId: extraOptions?.peerId,
73
+ });
74
+ }
75
+ const roomUrl = this.resolvedConfig.serverHost
76
+ ? buildRoomUrl(this.resolvedConfig.serverHost, urlOrOptions.roomId)
77
+ : null;
78
+ return new SerenadaSession(this.config, urlOrOptions.roomId, roomUrl, signalingProvider, {
79
+ displayName: urlOrOptions.displayName,
80
+ peerId: urlOrOptions.peerId,
81
+ });
82
+ }
83
+
84
+ /** Create a new room. Returns the room URL and ID. Call {@link join} to start the call. */
85
+ async createRoom(): Promise<CreateRoomResult> {
86
+ const serverHost = requireServerHost(this.config);
87
+ const roomId = await createRoomId(serverHost);
88
+ const url = buildRoomUrl(serverHost, roomId);
89
+ return { url, roomId };
90
+ }
91
+
92
+ private createUnsupportedSession(): SerenadaSessionHandle {
93
+ const errorState: CallState = {
94
+ phase: 'error',
95
+ roomId: null,
96
+ roomUrl: null,
97
+ localParticipant: null,
98
+ remoteParticipants: [],
99
+ connectionStatus: 'connected',
100
+ signalingState: { kind: 'failed', reason: 'webrtcUnavailable' },
101
+ activeTransport: null,
102
+ requiredPermissions: null,
103
+ error: { code: 'webrtcUnavailable', message: 'WebRTC is not supported in this browser' },
104
+ };
105
+ const noop = () => {};
106
+ const noopAsync = async () => {};
107
+ const emptyMap = new Map<string, MediaStream>();
108
+ return {
109
+ get state() { return errorState; },
110
+ subscribe(_cb: (state: CallState) => void) { return noop; },
111
+ onPeerMessage(_cb: (message: PeerMessage) => void) { return noop; },
112
+ leave: noop,
113
+ end: noop,
114
+ toggleAudio: noop,
115
+ toggleVideo: noop,
116
+ flipCamera: noopAsync,
117
+ setAudioEnabled: noop,
118
+ setVideoEnabled: noop,
119
+ setCameraMode: noop,
120
+ startScreenShare: noopAsync,
121
+ stopScreenShare: noopAsync,
122
+ resumeJoin: noopAsync,
123
+ cancelJoin: noop,
124
+ destroy: noop,
125
+ get localStream() { return null; },
126
+ get remoteStreams() { return emptyMap; },
127
+ get callStats() { return null; },
128
+ get hasMultipleCameras() { return false; },
129
+ get canScreenShare() { return false; },
130
+ get isSignalingConnected() { return false; },
131
+ get iceConnectionState(): RTCIceConnectionState { return 'closed'; },
132
+ get peerConnectionState(): RTCPeerConnectionState { return 'closed'; },
133
+ get rtcSignalingState(): RTCSignalingState { return 'closed'; },
134
+ onPermissionsRequired: null,
135
+ };
136
+ }
137
+
138
+ private createSignalingProvider(): SignalingProvider {
139
+ if (this.resolvedConfig.serverHost) {
140
+ return new SerenadaServerProvider({
141
+ serverHost: this.resolvedConfig.serverHost,
142
+ transports: this.config.transports,
143
+ logger: this.config.logger,
144
+ });
145
+ }
146
+ return this.resolvedConfig.signalingProvider as SignalingProvider;
147
+ }
148
+
149
+ private parseRoomIdFromUrl(url: string): string {
150
+ try {
151
+ const parsed = new URL(url);
152
+ const parts = parsed.pathname.split('/');
153
+ const callIndex = parts.indexOf('call');
154
+ if (callIndex !== -1 && parts[callIndex + 1]) {
155
+ return parts[callIndex + 1];
156
+ }
157
+ // Fallback: last path segment
158
+ return parts[parts.length - 1] || url;
159
+ } catch {
160
+ return url;
161
+ }
162
+ }
163
+ }