@signalwire/js 4.0.0-beta.12 → 4.0.0-beta.13
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/dist/browser.mjs +1540 -706
- package/dist/browser.mjs.map +1 -1
- package/dist/browser.umd.js +1546 -707
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +1411 -749
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +381 -37
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +381 -37
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1340 -681
- package/dist/index.mjs.map +1 -1
- package/dist/operators/index.cjs +1 -1
- package/dist/operators/index.mjs +1 -1
- package/dist/{operators-D6a2J1KA.cjs → operators-Bn4Ij3VB.cjs} +601 -1
- package/dist/operators-Bn4Ij3VB.cjs.map +1 -0
- package/dist/{operators-CX_lCCJm.mjs → operators-Zxmwpb0j.mjs} +188 -2
- package/dist/operators-Zxmwpb0j.mjs.map +1 -0
- package/package.json +3 -3
- package/dist/operators-CX_lCCJm.mjs.map +0 -1
- package/dist/operators-D6a2J1KA.cjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { $ as DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN, $t as RPC_ERROR_INVALID_PARAMS, A as RequestError, At as DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER, B as ValidationError, Bt as DEVICE_STORAGE_KEY_AUDIO_OUTPUT, C as MediaTrackError, Ct as DEFAULT_RECOVERY_DEBOUNCE_TIME_MS, D as PreflightError, Dt as DEFAULT_REINVITE_TIMEOUT_MS, E as ParticipantNotReadyError, Et as DEFAULT_REINVITE_MAX_ATTEMPTS, F as StorageWriteError, Ft as DEFAULT_STEREO_AUDIO, G as WebSocketTimeoutError, Gt as DEVICE_TOKEN_REFRESH_MAX_RETRIES, H as VertoInviteHandlerError, Ht as DEVICE_TOKEN_DEFAULT_EXPIRE_IN, I as TokenRefreshError, It as DEFAULT_STEREO_MAX_AVERAGE_BITRATE, J as CREDENTIAL_EXPIRY_SKEW_MS, Jt as INVITE_VERSION, K as AUDIO_LEVEL_POLL_INTERVAL_MS, Kt as DEVICE_TOKEN_REFRESH_RETRY_BASE_MS, L as TransportConnectionError, Lt as DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS, M as SerializationError, Mt as DEFAULT_STATS_PACKET_LOSS_THRESHOLD, N as StorageNotAvailableError, Nt as DEFAULT_STATS_POLLING_INTERVAL_MS, O as RPCTimeoutError, Ot as DEFAULT_STATS_BASELINE_SAMPLES, P as StorageReadError, Pt as DEFAULT_STATS_RTT_SPIKE_MULTIPLIER, Q as CREDENTIAL_REFRESH_RETRY_BASE_MS, Qt as RPC_ERROR_AUTHENTICATION_FAILED, R as UnexpectedError, Rt as DEVICE_REFRESH_ENDPOINT, S as MediaAccessError, St as DEFAULT_RECOVERY_COOLDOWN_MS, T as OverconstrainedFallbackError, Tt as DEFAULT_REINVITE_DEBOUNCE_TIME_MS, U as VertoPongError, Ut as DEVICE_TOKEN_ENDPOINT, V as VertoAttachHandlerError, Vt as DEVICE_STORAGE_KEY_VIDEO_INPUT, W as WebSocketConnectionError, Wt as DEVICE_TOKEN_REFRESH_BUFFER_MS, X as CREDENTIAL_REFRESH_MAX_DELAY_MS, Xt as PEER_CONNECTION_RECOVERY_WAIT_MS, Y as CREDENTIAL_REFRESH_BUFFER_MS, Yt as PEER_CONNECTION_RECOVERY_POLL_MS, Z as CREDENTIAL_REFRESH_MAX_RETRIES, Zt as PREFERENCES_STORAGE_KEY, _ as DeserializationError, _t as DEFAULT_MAX_RECOVERY_ATTEMPTS, a as filterNull, at as DEFAULT_DEVICE_POLLING_INTERVAL_MS, b as InvalidParams, bt as DEFAULT_RECONNECT_DELAY_MAX_MS, c as setLogLevel, ct as DEFAULT_ENABLE_RELAY_FALLBACK, d as CallCreateError, dt as DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS, en as RPC_ERROR_REQUESTER_VALIDATION_FAILED, et as DEFAULT_CHECK_CONNECTION_ON_VISIBLE, f as CallNotReadyError, ft as DEFAULT_ICE_GATHERING_TIMEOUT_MS, g as DependencyError, gt as DEFAULT_KEYFRAME_MAX_BURST$1, h as DPoPInitError, ht as DEFAULT_KEYFRAME_COOLDOWN_MS$1, i as getValueFrom, it as DEFAULT_DEVICE_DEBOUNCE_TIME_MS, j as RequestTimeoutError, jt as DEFAULT_STATS_NO_PACKET_THRESHOLD_MS, k as RecoveryError, kt as DEFAULT_STATS_HISTORY_SIZE, l as setLogger, lt as DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION, m as ConversationError, mt as DEFAULT_KEYFRAME_BURST_WINDOW_MS$1, n as filterAs, nn as VAD_HOLD_MS, nt as DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS, o as getLogger, ot as DEFAULT_ENABLE_AUTO_DEGRADATION, p as CollectionFetchError, pt as DEFAULT_ICE_RESTART_TIMEOUT_MS$1, q as CREDENTIAL_ACTIVATE_TIMEOUT_MS, qt as ICE_GATHERING_COMPLETE_TIMEOUT_MS, rn as VAD_THRESHOLD, rt as DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS, s as setDebugOptions, st as DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION, t as throwOnRPCError, tn as SAT_REFRESH_SCOPE, tt as DEFAULT_CONNECTION_TIMEOUT_MS, u as AuthStateHandlerError, ut as DEFAULT_ICE_CANDIDATE_TIMEOUT_MS, v as DeviceTokenError, vt as DEFAULT_PERSIST_DEVICE_SELECTION, w as MessageParseError, wt as DEFAULT_REFRESH_DEVICES_ON_VISIBLE, x as JSONRPCError, xt as DEFAULT_RECONNECT_DELAY_MIN_MS, y as InvalidCredentialsError, yt as DEFAULT_RECONNECT_CALLS_TIMEOUT_MS, z as UnimplementedError, zt as DEVICE_STORAGE_KEY_AUDIO_INPUT } from "./operators-Zxmwpb0j.mjs";
|
|
2
2
|
import { jwtDecode } from "jwt-decode";
|
|
3
|
-
import { BehaviorSubject, EMPTY, NEVER, Observable, ReplaySubject, Subject, TimeoutError, animationFrameScheduler, asapScheduler, auditTime, catchError, combineLatest, debounceTime, defer, distinctUntilChanged, exhaustMap, filter, first, firstValueFrom, from, interval, lastValueFrom, map, merge, mergeMap, observeOn, of, pipe, race, scan, share, shareReplay, skip, skipWhile, startWith, switchMap, take, takeUntil, tap, throwError, timeout, timer, toArray, withLatestFrom } from "rxjs";
|
|
3
|
+
import { BehaviorSubject, EMPTY, EmptyError, NEVER, Observable, ReplaySubject, Subject, TimeoutError, animationFrameScheduler, asapScheduler, auditTime, catchError, combineLatest, debounceTime, defer, distinctUntilChanged, exhaustMap, filter, first, firstValueFrom, from, interval, lastValueFrom, map, merge, mergeMap, observeOn, of, pipe, race, scan, share, shareReplay, skip, skipWhile, startWith, switchMap, take, takeUntil, tap, throwError, timeout, timer, toArray, withLatestFrom } from "rxjs";
|
|
4
4
|
import { v4 } from "uuid";
|
|
5
5
|
import { distinctUntilChanged as distinctUntilChanged$1, map as map$1 } from "rxjs/operators";
|
|
6
6
|
|
|
@@ -131,7 +131,7 @@ const asyncRetry = async ({ asyncCallable, maxRetries: retries = DEFAULT_MAX_RET
|
|
|
131
131
|
|
|
132
132
|
//#endregion
|
|
133
133
|
//#region src/controllers/HTTPRequestController.ts
|
|
134
|
-
const logger$
|
|
134
|
+
const logger$31 = getLogger();
|
|
135
135
|
const GET_PARAMS = {
|
|
136
136
|
method: "GET",
|
|
137
137
|
headers: { Accept: "application/json" }
|
|
@@ -195,7 +195,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
195
195
|
this._responses$.next(response);
|
|
196
196
|
return response;
|
|
197
197
|
} catch (error) {
|
|
198
|
-
logger$
|
|
198
|
+
logger$31.error("[HTTPRequestController] Request error:", error);
|
|
199
199
|
this._status$.next("error");
|
|
200
200
|
const err = error instanceof Error ? error : new Error("HTTP request failed", { cause: error });
|
|
201
201
|
this._errors$.next(err);
|
|
@@ -222,7 +222,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
222
222
|
const url = this.buildURL(request.url);
|
|
223
223
|
const headers = this.buildHeaders(request.headers);
|
|
224
224
|
const timeout$1 = request.timeout ?? this.requestTimeout;
|
|
225
|
-
logger$
|
|
225
|
+
logger$31.debug("[HTTPRequestController] Executing request:", {
|
|
226
226
|
method: request.method,
|
|
227
227
|
url,
|
|
228
228
|
headers: Object.keys(headers).reduce((acc, key) => {
|
|
@@ -242,7 +242,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
242
242
|
});
|
|
243
243
|
clearTimeout(timeoutId);
|
|
244
244
|
const httpResponse = await this.convertResponse(response);
|
|
245
|
-
logger$
|
|
245
|
+
logger$31.debug("[HTTPRequestController] Response received:", {
|
|
246
246
|
status: response.status,
|
|
247
247
|
statusText: response.statusText,
|
|
248
248
|
headers: [...response.headers.entries()],
|
|
@@ -252,7 +252,7 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
252
252
|
} catch (error) {
|
|
253
253
|
clearTimeout(timeoutId);
|
|
254
254
|
if (error instanceof Error && error.name === "AbortError") throw new RequestTimeoutError(`Request timeout after ${timeout$1}ms`, { cause: error });
|
|
255
|
-
logger$
|
|
255
|
+
logger$31.error("[HTTPRequestController] Request failed:", error);
|
|
256
256
|
throw error;
|
|
257
257
|
}
|
|
258
258
|
}
|
|
@@ -266,8 +266,8 @@ var HTTPRequestController = class HTTPRequestController extends Destroyable {
|
|
|
266
266
|
const credential = this.getCredential();
|
|
267
267
|
if (credential.token) {
|
|
268
268
|
headers.Authorization = `Bearer ${credential.token}`;
|
|
269
|
-
logger$
|
|
270
|
-
} else logger$
|
|
269
|
+
logger$31.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
|
|
270
|
+
} else logger$31.warn("[HTTPRequestController] No credentials available for authentication");
|
|
271
271
|
return headers;
|
|
272
272
|
}
|
|
273
273
|
/**
|
|
@@ -404,130 +404,6 @@ var DeviceHistoryManager = class {
|
|
|
404
404
|
}
|
|
405
405
|
};
|
|
406
406
|
|
|
407
|
-
//#endregion
|
|
408
|
-
//#region src/core/constants.ts
|
|
409
|
-
const INVITE_VERSION = 1e3;
|
|
410
|
-
const DEFAULT_ICE_CANDIDATE_TIMEOUT_MS = 600;
|
|
411
|
-
const DEFAULT_ICE_GATHERING_TIMEOUT_MS = 6e3;
|
|
412
|
-
const DEFAULT_RECONNECT_CALLS_TIMEOUT_MS = 300 * 1e3;
|
|
413
|
-
const DEFAULT_CONNECTION_TIMEOUT_MS = 1e4;
|
|
414
|
-
const DEFAULT_RECONNECT_DELAY_MIN_MS = 100;
|
|
415
|
-
const DEFAULT_RECONNECT_DELAY_MAX_MS = 3e3;
|
|
416
|
-
const DEFAULT_DEVICE_DEBOUNCE_TIME_MS = 1500;
|
|
417
|
-
const DEFAULT_DEVICE_POLLING_INTERVAL_MS = 0;
|
|
418
|
-
const PREFERENCES_STORAGE_KEY = "sw:preferences";
|
|
419
|
-
/** Scope value that enables automatic token refresh. */
|
|
420
|
-
const SAT_REFRESH_SCOPE = "sat:refresh";
|
|
421
|
-
/** API endpoints for device token operations. */
|
|
422
|
-
const DEVICE_TOKEN_ENDPOINT = "/api/fabric/subscriber/devices/token";
|
|
423
|
-
const DEVICE_REFRESH_ENDPOINT = "/api/fabric/subscriber/devices/refresh";
|
|
424
|
-
/** Default device token TTL in seconds (15 minutes). */
|
|
425
|
-
const DEVICE_TOKEN_DEFAULT_EXPIRE_IN = 900;
|
|
426
|
-
/** Buffer time in milliseconds before expiry to trigger refresh. */
|
|
427
|
-
const DEVICE_TOKEN_REFRESH_BUFFER_MS = 3e4;
|
|
428
|
-
/** Maximum retry attempts for device token refresh on transient failure. */
|
|
429
|
-
const DEVICE_TOKEN_REFRESH_MAX_RETRIES = 3;
|
|
430
|
-
/** Base delay in milliseconds for exponential backoff on refresh retry. */
|
|
431
|
-
const DEVICE_TOKEN_REFRESH_RETRY_BASE_MS = 1e3;
|
|
432
|
-
/** Maximum retry attempts for developer credential refresh on transient failure. */
|
|
433
|
-
const CREDENTIAL_REFRESH_MAX_RETRIES = 5;
|
|
434
|
-
/** Base delay in milliseconds for exponential backoff on credential refresh retry. */
|
|
435
|
-
const CREDENTIAL_REFRESH_RETRY_BASE_MS = 1e3;
|
|
436
|
-
/** Maximum delay in milliseconds for credential refresh backoff. */
|
|
437
|
-
const CREDENTIAL_REFRESH_MAX_DELAY_MS = 3e4;
|
|
438
|
-
/** Buffer in milliseconds before token expiry to trigger refresh. */
|
|
439
|
-
const CREDENTIAL_REFRESH_BUFFER_MS = 5e3;
|
|
440
|
-
/** JSON-RPC error code for requester validation failure (corrupted auth state). */
|
|
441
|
-
const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
|
|
442
|
-
/** JSON-RPC error code for invalid params (e.g., missing authentication block). */
|
|
443
|
-
const RPC_ERROR_INVALID_PARAMS = -32602;
|
|
444
|
-
/** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
|
|
445
|
-
const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
|
|
446
|
-
/** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
|
|
447
|
-
const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
|
|
448
|
-
/** Number of initial samples used to build a baseline for spike detection. */
|
|
449
|
-
const DEFAULT_STATS_BASELINE_SAMPLES = 10;
|
|
450
|
-
/** Duration in ms with no inbound audio packets before emitting a critical issue. */
|
|
451
|
-
const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
|
|
452
|
-
/** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
|
|
453
|
-
const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
|
|
454
|
-
/** Packet loss fraction (0-1) above which a warning is emitted. */
|
|
455
|
-
const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
|
|
456
|
-
/** Multiplier applied to baseline jitter to detect a jitter spike. */
|
|
457
|
-
const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
|
|
458
|
-
/** Number of seconds of metrics history to retain. */
|
|
459
|
-
const DEFAULT_STATS_HISTORY_SIZE = 30;
|
|
460
|
-
/** Maximum keyframe requests allowed within a single burst window. */
|
|
461
|
-
const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
|
|
462
|
-
/** Duration of the keyframe burst window in milliseconds. */
|
|
463
|
-
const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
|
|
464
|
-
/** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
|
|
465
|
-
const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
|
|
466
|
-
/** Minimum time between re-INVITE attempts in milliseconds. */
|
|
467
|
-
const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
|
|
468
|
-
/** Maximum number of re-INVITE attempts per call. */
|
|
469
|
-
const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
|
|
470
|
-
/** Timeout for a single re-INVITE attempt in milliseconds. */
|
|
471
|
-
const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
|
|
472
|
-
/** Debounce window in ms to collapse multiple detection signals into one trigger. */
|
|
473
|
-
const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
|
|
474
|
-
/** Cooldown period in ms between recovery attempts. */
|
|
475
|
-
const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
|
|
476
|
-
/** Grace period in ms before treating ICE 'disconnected' as a failure. */
|
|
477
|
-
const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
|
|
478
|
-
/** Timeout for a single ICE restart attempt in milliseconds. */
|
|
479
|
-
const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
|
|
480
|
-
/** Maximum recovery attempts before emitting 'max_attempts_reached'. */
|
|
481
|
-
const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
|
|
482
|
-
/** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
|
|
483
|
-
const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
|
|
484
|
-
/** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
|
|
485
|
-
const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
|
|
486
|
-
/** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
|
|
487
|
-
const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
|
|
488
|
-
/** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
|
|
489
|
-
const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
|
|
490
|
-
/** RMS level threshold (0..1) above which the local participant is considered speaking. */
|
|
491
|
-
const VAD_THRESHOLD = .03;
|
|
492
|
-
/** Hold window in ms below the threshold before speaking$ flips back to false. */
|
|
493
|
-
const VAD_HOLD_MS = 250;
|
|
494
|
-
/** Whether to persist device selections to storage by default. */
|
|
495
|
-
const DEFAULT_PERSIST_DEVICE_SELECTION = true;
|
|
496
|
-
/** Whether to auto-apply device changes to active calls by default. */
|
|
497
|
-
const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
|
|
498
|
-
/** Storage keys for persisted device selections. */
|
|
499
|
-
const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
|
|
500
|
-
const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
|
|
501
|
-
const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
|
|
502
|
-
/** Whether to auto-mute video when the tab becomes hidden. */
|
|
503
|
-
const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
|
|
504
|
-
/** Whether to re-enumerate devices when the page becomes visible. */
|
|
505
|
-
const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
|
|
506
|
-
/** Whether to check peer connection health when the page becomes visible. */
|
|
507
|
-
const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
|
|
508
|
-
/** Whether automatic video degradation on low bandwidth is enabled. */
|
|
509
|
-
const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
|
|
510
|
-
/** Bitrate in kbps below which video is automatically disabled. */
|
|
511
|
-
const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
|
|
512
|
-
/** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
|
|
513
|
-
const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
|
|
514
|
-
/** Whether relay-only escalation is enabled as a last-resort recovery tier. */
|
|
515
|
-
const DEFAULT_ENABLE_RELAY_FALLBACK = true;
|
|
516
|
-
/** Whether to listen for browser online/offline/connection events. */
|
|
517
|
-
const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
|
|
518
|
-
/** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
|
|
519
|
-
const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
|
|
520
|
-
/** Default video track constraints applied when video is enabled without explicit constraints. */
|
|
521
|
-
const DEFAULT_VIDEO_CONSTRAINTS = {
|
|
522
|
-
width: { ideal: 1280 },
|
|
523
|
-
height: { ideal: 720 },
|
|
524
|
-
aspectRatio: 16 / 9
|
|
525
|
-
};
|
|
526
|
-
/** Whether stereo Opus is enabled by default. */
|
|
527
|
-
const DEFAULT_STEREO_AUDIO = false;
|
|
528
|
-
/** Max average bitrate for stereo Opus in bits per second. */
|
|
529
|
-
const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
|
|
530
|
-
|
|
531
407
|
//#endregion
|
|
532
408
|
//#region src/utils/time.ts
|
|
533
409
|
function fromSecToMs(seconds) {
|
|
@@ -539,7 +415,7 @@ function fromMsToSec(milliseconds) {
|
|
|
539
415
|
|
|
540
416
|
//#endregion
|
|
541
417
|
//#region src/containers/PreferencesContainer.ts
|
|
542
|
-
const logger$
|
|
418
|
+
const logger$30 = getLogger();
|
|
543
419
|
var PreferencesContainer = class PreferencesContainer {
|
|
544
420
|
static get instance() {
|
|
545
421
|
this._instance ??= new PreferencesContainer();
|
|
@@ -1201,7 +1077,7 @@ var ClientPreferences = class {
|
|
|
1201
1077
|
if (!this._storage) return;
|
|
1202
1078
|
const data = collectStoredPreferences();
|
|
1203
1079
|
this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
|
|
1204
|
-
logger$
|
|
1080
|
+
logger$30.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
|
|
1205
1081
|
});
|
|
1206
1082
|
}
|
|
1207
1083
|
/** Loads preferences from storage and applies them to the container. */
|
|
@@ -1210,7 +1086,7 @@ var ClientPreferences = class {
|
|
|
1210
1086
|
this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
|
|
1211
1087
|
if (stored) applyStoredPreferences(stored);
|
|
1212
1088
|
}).catch((error) => {
|
|
1213
|
-
logger$
|
|
1089
|
+
logger$30.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
|
|
1214
1090
|
});
|
|
1215
1091
|
}
|
|
1216
1092
|
};
|
|
@@ -1231,7 +1107,7 @@ function toError(value) {
|
|
|
1231
1107
|
|
|
1232
1108
|
//#endregion
|
|
1233
1109
|
//#region src/controllers/NavigatorDeviceController.ts
|
|
1234
|
-
const logger$
|
|
1110
|
+
const logger$29 = getLogger();
|
|
1235
1111
|
/** Maps a device kind to its storage key. */
|
|
1236
1112
|
const DEVICE_STORAGE_KEYS = {
|
|
1237
1113
|
audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
|
|
@@ -1253,7 +1129,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1253
1129
|
super();
|
|
1254
1130
|
this.webRTCApiProvider = webRTCApiProvider;
|
|
1255
1131
|
this.deviceChangeHandler = () => {
|
|
1256
|
-
logger$
|
|
1132
|
+
logger$29.debug("[DeviceController] Device change detected");
|
|
1257
1133
|
this.enumerateDevices();
|
|
1258
1134
|
};
|
|
1259
1135
|
this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
|
|
@@ -1318,13 +1194,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1318
1194
|
return this.cachedObservable("videoInputDevices$", () => this._devicesState$.pipe(map((state) => state.videoinput), distinctUntilChanged(), takeUntil(this.destroyed$)));
|
|
1319
1195
|
}
|
|
1320
1196
|
get selectedAudioInputDevice$() {
|
|
1321
|
-
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.audioinput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$
|
|
1197
|
+
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.audioinput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$29.debug("[DeviceController] Selected audio input device changed:", info))));
|
|
1322
1198
|
}
|
|
1323
1199
|
get selectedAudioOutputDevice$() {
|
|
1324
|
-
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.audiooutput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$
|
|
1200
|
+
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.audiooutput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$29.debug("[DeviceController] Selected audio output device changed:", info))));
|
|
1325
1201
|
}
|
|
1326
1202
|
get selectedVideoInputDevice$() {
|
|
1327
|
-
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.videoinput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$
|
|
1203
|
+
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe(map((state) => state.videoinput), distinctUntilChanged(), takeUntil(this.destroyed$), tap((info) => logger$29.debug("[DeviceController] Selected video input device changed:", info))));
|
|
1328
1204
|
}
|
|
1329
1205
|
get selectedAudioInputDevice() {
|
|
1330
1206
|
if (this._audioInputDisabled$.value) return null;
|
|
@@ -1399,7 +1275,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1399
1275
|
if (device) this.persistDeviceSelection("audioinput", device);
|
|
1400
1276
|
}
|
|
1401
1277
|
selectVideoInputDevice(device) {
|
|
1402
|
-
logger$
|
|
1278
|
+
logger$29.debug("[DeviceController] Setting selected video input device:", device);
|
|
1403
1279
|
if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
|
|
1404
1280
|
const previous = this._selectedDevicesState$.value.videoinput;
|
|
1405
1281
|
if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
|
|
@@ -1456,7 +1332,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1456
1332
|
}
|
|
1457
1333
|
const fromHistory = this._deviceHistory.findInHistory(kind, devices);
|
|
1458
1334
|
if (fromHistory) {
|
|
1459
|
-
logger$
|
|
1335
|
+
logger$29.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
|
|
1460
1336
|
this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
|
|
1461
1337
|
return fromHistory;
|
|
1462
1338
|
}
|
|
@@ -1509,7 +1385,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1509
1385
|
try {
|
|
1510
1386
|
await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
|
|
1511
1387
|
} catch (error) {
|
|
1512
|
-
logger$
|
|
1388
|
+
logger$29.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
|
|
1513
1389
|
}
|
|
1514
1390
|
}
|
|
1515
1391
|
async loadPersistedDevices() {
|
|
@@ -1525,7 +1401,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1525
1401
|
[kind]: stored
|
|
1526
1402
|
};
|
|
1527
1403
|
} catch (error) {
|
|
1528
|
-
logger$
|
|
1404
|
+
logger$29.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
|
|
1529
1405
|
}
|
|
1530
1406
|
}
|
|
1531
1407
|
/** Clears device history, persisted selections, and re-enumerates devices. */
|
|
@@ -1543,7 +1419,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1543
1419
|
this.disableDeviceMonitoring();
|
|
1544
1420
|
this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
|
|
1545
1421
|
if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = interval(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
|
|
1546
|
-
logger$
|
|
1422
|
+
logger$29.debug("[DeviceController] Polling devices due to interval");
|
|
1547
1423
|
this.enumerateDevices();
|
|
1548
1424
|
});
|
|
1549
1425
|
this.enumerateDevices();
|
|
@@ -1569,13 +1445,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1569
1445
|
videoinput: []
|
|
1570
1446
|
});
|
|
1571
1447
|
this._devicesState$.next(devicesByKind);
|
|
1572
|
-
logger$
|
|
1448
|
+
logger$29.debug("[DeviceController] Devices enumerated:", {
|
|
1573
1449
|
audioInputs: devicesByKind.audioinput.length,
|
|
1574
1450
|
audioOutputs: devicesByKind.audiooutput.length,
|
|
1575
1451
|
videoInputs: devicesByKind.videoinput.length
|
|
1576
1452
|
});
|
|
1577
1453
|
} catch (error) {
|
|
1578
|
-
logger$
|
|
1454
|
+
logger$29.error("[DeviceController] Failed to enumerate devices:", error);
|
|
1579
1455
|
this._errors$.next(toError(error));
|
|
1580
1456
|
}
|
|
1581
1457
|
}
|
|
@@ -1591,7 +1467,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1591
1467
|
stream.getTracks().forEach((t) => t.stop());
|
|
1592
1468
|
return capabilities;
|
|
1593
1469
|
} catch (error) {
|
|
1594
|
-
logger$
|
|
1470
|
+
logger$29.error("[DeviceController] Failed to get device capabilities:", error);
|
|
1595
1471
|
this._errors$.next(toError(error));
|
|
1596
1472
|
throw error;
|
|
1597
1473
|
}
|
|
@@ -1842,7 +1718,7 @@ var DependencyContainer = class {
|
|
|
1842
1718
|
|
|
1843
1719
|
//#endregion
|
|
1844
1720
|
//#region src/controllers/CryptoController.ts
|
|
1845
|
-
const logger$
|
|
1721
|
+
const logger$28 = getLogger();
|
|
1846
1722
|
const DPOP_DB_NAME = "sw-dpop";
|
|
1847
1723
|
const DPOP_DB_VERSION = 1;
|
|
1848
1724
|
const DPOP_STORE_NAME = "keys";
|
|
@@ -1901,7 +1777,7 @@ async function loadKeyPairFromDB() {
|
|
|
1901
1777
|
tx.oncomplete = () => db.close();
|
|
1902
1778
|
});
|
|
1903
1779
|
} catch (error) {
|
|
1904
|
-
logger$
|
|
1780
|
+
logger$28.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
|
|
1905
1781
|
return null;
|
|
1906
1782
|
}
|
|
1907
1783
|
}
|
|
@@ -1921,7 +1797,7 @@ async function saveKeyPairToDB(keyPair) {
|
|
|
1921
1797
|
};
|
|
1922
1798
|
});
|
|
1923
1799
|
} catch (error) {
|
|
1924
|
-
logger$
|
|
1800
|
+
logger$28.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
|
|
1925
1801
|
}
|
|
1926
1802
|
}
|
|
1927
1803
|
async function deleteKeyPairFromDB() {
|
|
@@ -1940,7 +1816,7 @@ async function deleteKeyPairFromDB() {
|
|
|
1940
1816
|
};
|
|
1941
1817
|
});
|
|
1942
1818
|
} catch (error) {
|
|
1943
|
-
logger$
|
|
1819
|
+
logger$28.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
|
|
1944
1820
|
}
|
|
1945
1821
|
}
|
|
1946
1822
|
/**
|
|
@@ -2000,13 +1876,13 @@ var CryptoController = class {
|
|
|
2000
1876
|
this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
|
|
2001
1877
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
2002
1878
|
this._initialized = true;
|
|
2003
|
-
logger$
|
|
1879
|
+
logger$28.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
|
|
2004
1880
|
return this._fingerprint;
|
|
2005
1881
|
} catch (error) {
|
|
2006
|
-
logger$
|
|
1882
|
+
logger$28.warn("[DPoP] Stored key pair unusable, generating new one:", error);
|
|
2007
1883
|
await deleteKeyPairFromDB();
|
|
2008
1884
|
}
|
|
2009
|
-
logger$
|
|
1885
|
+
logger$28.debug("[DPoP] Generating RSA key pair");
|
|
2010
1886
|
this._keyPair = await crypto.subtle.generateKey({
|
|
2011
1887
|
name: "RSASSA-PKCS1-v1_5",
|
|
2012
1888
|
modulusLength: 2048,
|
|
@@ -2021,7 +1897,7 @@ var CryptoController = class {
|
|
|
2021
1897
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
2022
1898
|
this._initialized = true;
|
|
2023
1899
|
await saveKeyPairToDB(this._keyPair);
|
|
2024
|
-
logger$
|
|
1900
|
+
logger$28.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
|
|
2025
1901
|
return this._fingerprint;
|
|
2026
1902
|
}
|
|
2027
1903
|
/**
|
|
@@ -2087,7 +1963,7 @@ var CryptoController = class {
|
|
|
2087
1963
|
this._fingerprint = null;
|
|
2088
1964
|
this._initialized = false;
|
|
2089
1965
|
deleteKeyPairFromDB();
|
|
2090
|
-
logger$
|
|
1966
|
+
logger$28.debug("[DPoP] Controller destroyed");
|
|
2091
1967
|
}
|
|
2092
1968
|
get publicJwk() {
|
|
2093
1969
|
if (!this._publicJwk) throw new DPoPInitError("CryptoController not initialized. Call init() first.");
|
|
@@ -2110,7 +1986,7 @@ var CryptoController = class {
|
|
|
2110
1986
|
|
|
2111
1987
|
//#endregion
|
|
2112
1988
|
//#region src/controllers/NetworkMonitor.ts
|
|
2113
|
-
const logger$
|
|
1989
|
+
const logger$27 = getLogger();
|
|
2114
1990
|
/**
|
|
2115
1991
|
* Safely check whether we are running in a browser environment
|
|
2116
1992
|
* with `window` and the relevant event targets.
|
|
@@ -2167,7 +2043,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
2167
2043
|
}
|
|
2168
2044
|
attachListeners() {
|
|
2169
2045
|
if (!hasBrowserNetworkEvents()) {
|
|
2170
|
-
logger$
|
|
2046
|
+
logger$27.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
|
|
2171
2047
|
return;
|
|
2172
2048
|
}
|
|
2173
2049
|
window.addEventListener("online", this._onOnline);
|
|
@@ -2175,7 +2051,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
2175
2051
|
const connection = getNetworkConnection();
|
|
2176
2052
|
if (connection) connection.addEventListener("change", this._onConnectionChange);
|
|
2177
2053
|
this._listenersAttached = true;
|
|
2178
|
-
logger$
|
|
2054
|
+
logger$27.debug("NetworkMonitor: event listeners attached");
|
|
2179
2055
|
}
|
|
2180
2056
|
removeListeners() {
|
|
2181
2057
|
if (!this._listenersAttached) return;
|
|
@@ -2186,10 +2062,10 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
2186
2062
|
if (connection) connection.removeEventListener("change", this._onConnectionChange);
|
|
2187
2063
|
}
|
|
2188
2064
|
this._listenersAttached = false;
|
|
2189
|
-
logger$
|
|
2065
|
+
logger$27.debug("NetworkMonitor: event listeners removed");
|
|
2190
2066
|
}
|
|
2191
2067
|
handleOnline() {
|
|
2192
|
-
logger$
|
|
2068
|
+
logger$27.info("NetworkMonitor: browser went online");
|
|
2193
2069
|
this._isOnline$.next(true);
|
|
2194
2070
|
this._networkChange$.next({
|
|
2195
2071
|
type: "online",
|
|
@@ -2198,7 +2074,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
2198
2074
|
});
|
|
2199
2075
|
}
|
|
2200
2076
|
handleOffline() {
|
|
2201
|
-
logger$
|
|
2077
|
+
logger$27.info("NetworkMonitor: browser went offline");
|
|
2202
2078
|
this._isOnline$.next(false);
|
|
2203
2079
|
this._networkChange$.next({
|
|
2204
2080
|
type: "offline",
|
|
@@ -2207,7 +2083,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
2207
2083
|
}
|
|
2208
2084
|
handleConnectionChange() {
|
|
2209
2085
|
const networkType = getNetworkType();
|
|
2210
|
-
logger$
|
|
2086
|
+
logger$27.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
|
|
2211
2087
|
this._networkChange$.next({
|
|
2212
2088
|
type: "connection_change",
|
|
2213
2089
|
timestamp: Date.now(),
|
|
@@ -2322,7 +2198,7 @@ function getNavigatorMediaDevices() {
|
|
|
2322
2198
|
|
|
2323
2199
|
//#endregion
|
|
2324
2200
|
//#region src/controllers/PreflightRunner.ts
|
|
2325
|
-
const logger$
|
|
2201
|
+
const logger$26 = getLogger();
|
|
2326
2202
|
const DEFAULT_MEDIA_TEST_DURATION_S = 10;
|
|
2327
2203
|
const ICE_GATHERING_TIMEOUT_MS = 1e4;
|
|
2328
2204
|
const SIGNALING_RTT_TIMEOUT_MS = 5e3;
|
|
@@ -2371,7 +2247,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
2371
2247
|
if (!this._options.skipMediaTest) try {
|
|
2372
2248
|
bandwidth = await this.testMediaBandwidth(destination);
|
|
2373
2249
|
} catch (error) {
|
|
2374
|
-
logger$
|
|
2250
|
+
logger$26.warn("[PreflightRunner] Media bandwidth test failed:", error);
|
|
2375
2251
|
warnings.push("Media bandwidth test failed");
|
|
2376
2252
|
}
|
|
2377
2253
|
return {
|
|
@@ -2383,7 +2259,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
2383
2259
|
warnings
|
|
2384
2260
|
};
|
|
2385
2261
|
} catch (error) {
|
|
2386
|
-
logger$
|
|
2262
|
+
logger$26.error("[PreflightRunner] Preflight test failed:", error);
|
|
2387
2263
|
throw new PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
|
|
2388
2264
|
} finally {
|
|
2389
2265
|
this.destroy();
|
|
@@ -2414,7 +2290,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
2414
2290
|
if (track.kind === "video" && track.readyState === "live") videoWorking = true;
|
|
2415
2291
|
}
|
|
2416
2292
|
} catch (error) {
|
|
2417
|
-
logger$
|
|
2293
|
+
logger$26.warn("[PreflightRunner] Device test failed:", error);
|
|
2418
2294
|
} finally {
|
|
2419
2295
|
if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
|
|
2420
2296
|
}
|
|
@@ -2472,7 +2348,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
2472
2348
|
rttMs
|
|
2473
2349
|
};
|
|
2474
2350
|
} catch (error) {
|
|
2475
|
-
logger$
|
|
2351
|
+
logger$26.warn("[PreflightRunner] ICE connectivity test failed:", error);
|
|
2476
2352
|
return {
|
|
2477
2353
|
type: "failed",
|
|
2478
2354
|
turnReachable: false,
|
|
@@ -2519,7 +2395,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
2519
2395
|
|
|
2520
2396
|
//#endregion
|
|
2521
2397
|
//#region src/controllers/VisibilityController.ts
|
|
2522
|
-
const logger$
|
|
2398
|
+
const logger$25 = getLogger();
|
|
2523
2399
|
/**
|
|
2524
2400
|
* Checks whether the document visibility API is available.
|
|
2525
2401
|
*/
|
|
@@ -2556,8 +2432,8 @@ var VisibilityController = class extends Destroyable {
|
|
|
2556
2432
|
this._boundHandler = this._handleVisibilityChange.bind(this);
|
|
2557
2433
|
if (this._hasVisibilityApi) {
|
|
2558
2434
|
document.addEventListener("visibilitychange", this._boundHandler);
|
|
2559
|
-
logger$
|
|
2560
|
-
} else logger$
|
|
2435
|
+
logger$25.debug("VisibilityController: listening for visibilitychange events");
|
|
2436
|
+
} else logger$25.debug("VisibilityController: document visibility API not available, defaulting to visible");
|
|
2561
2437
|
}
|
|
2562
2438
|
/**
|
|
2563
2439
|
* Observable of the current visibility state.
|
|
@@ -2582,7 +2458,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
2582
2458
|
destroy() {
|
|
2583
2459
|
if (this._hasVisibilityApi) {
|
|
2584
2460
|
document.removeEventListener("visibilitychange", this._boundHandler);
|
|
2585
|
-
logger$
|
|
2461
|
+
logger$25.debug("VisibilityController: removed visibilitychange listener");
|
|
2586
2462
|
}
|
|
2587
2463
|
super.destroy();
|
|
2588
2464
|
}
|
|
@@ -2600,7 +2476,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
2600
2476
|
timestamp: Date.now()
|
|
2601
2477
|
};
|
|
2602
2478
|
this._visibilityChange$.next(changeEvent);
|
|
2603
|
-
logger$
|
|
2479
|
+
logger$25.debug("VisibilityController: visibility changed", {
|
|
2604
2480
|
from: previousState,
|
|
2605
2481
|
to: newState
|
|
2606
2482
|
});
|
|
@@ -2803,7 +2679,7 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
|
|
|
2803
2679
|
|
|
2804
2680
|
//#endregion
|
|
2805
2681
|
//#region src/managers/AttachManager.ts
|
|
2806
|
-
const logger$
|
|
2682
|
+
const logger$24 = getLogger();
|
|
2807
2683
|
var AttachManager = class {
|
|
2808
2684
|
constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
|
|
2809
2685
|
this.storage = storage;
|
|
@@ -2824,7 +2700,7 @@ var AttachManager = class {
|
|
|
2824
2700
|
try {
|
|
2825
2701
|
return await this.storage.getItem(this.attachKey) ?? {};
|
|
2826
2702
|
} catch (error) {
|
|
2827
|
-
logger$
|
|
2703
|
+
logger$24.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
|
|
2828
2704
|
return {};
|
|
2829
2705
|
}
|
|
2830
2706
|
}
|
|
@@ -2832,7 +2708,7 @@ var AttachManager = class {
|
|
|
2832
2708
|
try {
|
|
2833
2709
|
await this.storage.setItem(this.attachKey, attached);
|
|
2834
2710
|
} catch (error) {
|
|
2835
|
-
logger$
|
|
2711
|
+
logger$24.warn("[AttachManager] Failed to write attached calls to storage", error);
|
|
2836
2712
|
}
|
|
2837
2713
|
}
|
|
2838
2714
|
/**
|
|
@@ -2851,7 +2727,7 @@ var AttachManager = class {
|
|
|
2851
2727
|
}
|
|
2852
2728
|
async attach(call) {
|
|
2853
2729
|
if (!call.to) {
|
|
2854
|
-
logger$
|
|
2730
|
+
logger$24.warn("[AttachManager] Skip attach for calls with no destination");
|
|
2855
2731
|
return;
|
|
2856
2732
|
}
|
|
2857
2733
|
const destination = call.to;
|
|
@@ -2904,15 +2780,15 @@ var AttachManager = class {
|
|
|
2904
2780
|
callId,
|
|
2905
2781
|
...options
|
|
2906
2782
|
});
|
|
2907
|
-
logger$
|
|
2783
|
+
logger$24.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
|
|
2908
2784
|
succeeded = true;
|
|
2909
2785
|
break;
|
|
2910
2786
|
} catch (error) {
|
|
2911
|
-
logger$
|
|
2787
|
+
logger$24.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
|
|
2912
2788
|
if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
|
|
2913
2789
|
}
|
|
2914
2790
|
if (!succeeded) {
|
|
2915
|
-
logger$
|
|
2791
|
+
logger$24.warn(`[AttachManager] Reattach failed after 3 attempts for call ${callId}, removing reference`);
|
|
2916
2792
|
await this.detach({
|
|
2917
2793
|
id: callId,
|
|
2918
2794
|
mediaDirections: attachment.mediaDirections
|
|
@@ -2998,7 +2874,9 @@ const DEFAULT_MEMBER_CAPABILITIES = {
|
|
|
2998
2874
|
position: false,
|
|
2999
2875
|
meta: false,
|
|
3000
2876
|
remove: false,
|
|
3001
|
-
audioFlags: false
|
|
2877
|
+
audioFlags: false,
|
|
2878
|
+
denoise: false,
|
|
2879
|
+
lowbitrate: false
|
|
3002
2880
|
};
|
|
3003
2881
|
/**
|
|
3004
2882
|
* Default call capabilities with no permissions
|
|
@@ -3071,7 +2949,9 @@ function computeMemberCapabilities(flags, memberType) {
|
|
|
3071
2949
|
position: hasBooleanCapability(flags, memberType, null, "position"),
|
|
3072
2950
|
meta: hasBooleanCapability(flags, memberType, null, "meta"),
|
|
3073
2951
|
remove: hasBooleanCapability(flags, memberType, null, "remove"),
|
|
3074
|
-
audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags")
|
|
2952
|
+
audioFlags: hasBooleanCapability(flags, memberType, null, "audioflags"),
|
|
2953
|
+
denoise: hasBooleanCapability(flags, memberType, null, "denoise"),
|
|
2954
|
+
lowbitrate: hasBooleanCapability(flags, memberType, null, "lowbitrate")
|
|
3075
2955
|
};
|
|
3076
2956
|
}
|
|
3077
2957
|
/**
|
|
@@ -3226,7 +3106,7 @@ function toggleHandraiseMethod(is) {
|
|
|
3226
3106
|
|
|
3227
3107
|
//#endregion
|
|
3228
3108
|
//#region src/core/entities/Participant.ts
|
|
3229
|
-
const logger$
|
|
3109
|
+
const logger$23 = getLogger();
|
|
3230
3110
|
const initialState = {};
|
|
3231
3111
|
/**
|
|
3232
3112
|
* Represents a participant in a call.
|
|
@@ -3236,9 +3116,9 @@ const initialState = {};
|
|
|
3236
3116
|
* the local participant with additional device control.
|
|
3237
3117
|
*/
|
|
3238
3118
|
var Participant = class extends Destroyable {
|
|
3239
|
-
constructor(id,
|
|
3119
|
+
constructor(id, callExecuteMethod, deviceController) {
|
|
3240
3120
|
super();
|
|
3241
|
-
this.
|
|
3121
|
+
this.callExecuteMethod = callExecuteMethod;
|
|
3242
3122
|
this.deviceController = deviceController;
|
|
3243
3123
|
this._state$ = this.createBehaviorSubject(initialState);
|
|
3244
3124
|
this.id = id;
|
|
@@ -3452,26 +3332,63 @@ var Participant = class extends Destroyable {
|
|
|
3452
3332
|
get nodeId() {
|
|
3453
3333
|
return this._state$.value.node_id;
|
|
3454
3334
|
}
|
|
3335
|
+
/** Call ID for this participant's leg, or `undefined` if not available. */
|
|
3336
|
+
get callId() {
|
|
3337
|
+
return this._state$.value.call_id;
|
|
3338
|
+
}
|
|
3455
3339
|
/** @internal */
|
|
3456
3340
|
get value() {
|
|
3457
3341
|
return this._state$.value;
|
|
3458
3342
|
}
|
|
3343
|
+
/**
|
|
3344
|
+
* Target triple for member RPCs, built from the participant's own state.
|
|
3345
|
+
* The backend locates the member's session by the target `call_id`/`node_id`,
|
|
3346
|
+
* so this must always be the participant's own call context — never the
|
|
3347
|
+
* local call's id (issue #19400).
|
|
3348
|
+
*
|
|
3349
|
+
* Reading it doubles as a readiness probe: it throws until the first full
|
|
3350
|
+
* member event (`member.joined`/`member.updated` or the `call.joined`
|
|
3351
|
+
* roster) arrives, and never regresses afterwards.
|
|
3352
|
+
*
|
|
3353
|
+
* @throws {ParticipantNotReadyError} If the member state has not been
|
|
3354
|
+
* received yet (e.g. a participant first seen via `member.talking`) — an
|
|
3355
|
+
* empty call context can never address the member, so fail fast instead of
|
|
3356
|
+
* sending a doomed RPC.
|
|
3357
|
+
*/
|
|
3358
|
+
get target() {
|
|
3359
|
+
const { call_id, node_id } = this._state$.value;
|
|
3360
|
+
if (!call_id || !node_id) throw new ParticipantNotReadyError(this.id);
|
|
3361
|
+
return {
|
|
3362
|
+
member_id: this.id,
|
|
3363
|
+
call_id,
|
|
3364
|
+
node_id
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
/**
|
|
3368
|
+
* Executes a member RPC against this participant, injecting its own
|
|
3369
|
+
* {@link target} as the target.
|
|
3370
|
+
*
|
|
3371
|
+
* @throws {ParticipantNotReadyError} Via {@link target}, when the
|
|
3372
|
+
* member state has not been received yet.
|
|
3373
|
+
*/
|
|
3374
|
+
async executeMethod(method, args) {
|
|
3375
|
+
return this.callExecuteMethod(this.target, method, args);
|
|
3376
|
+
}
|
|
3459
3377
|
/** Toggles the deafened state (mutes/unmutes incoming audio). */
|
|
3460
3378
|
async toggleDeaf() {
|
|
3461
|
-
|
|
3462
|
-
await this.executeMethod(this.id, method, {});
|
|
3379
|
+
await this.executeMethod(toggleDeafMethod(this.deaf), {});
|
|
3463
3380
|
}
|
|
3464
3381
|
/** Toggles the hand-raised state. */
|
|
3465
3382
|
async toggleHandraise() {
|
|
3466
|
-
await this.executeMethod(
|
|
3383
|
+
await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
|
|
3467
3384
|
}
|
|
3468
3385
|
/** Mutes the participant's audio. */
|
|
3469
3386
|
async mute() {
|
|
3470
|
-
await this.executeMethod(
|
|
3387
|
+
await this.executeMethod("call.mute", { channels: ["audio"] });
|
|
3471
3388
|
}
|
|
3472
3389
|
/** Unmutes the participant's audio. */
|
|
3473
3390
|
async unmute() {
|
|
3474
|
-
await this.executeMethod(
|
|
3391
|
+
await this.executeMethod("call.unmute", { channels: ["audio"] });
|
|
3475
3392
|
}
|
|
3476
3393
|
/** Toggles the participant's audio mute state. */
|
|
3477
3394
|
async toggleMute() {
|
|
@@ -3479,11 +3396,11 @@ var Participant = class extends Destroyable {
|
|
|
3479
3396
|
}
|
|
3480
3397
|
/** Mutes the participant's video. */
|
|
3481
3398
|
async muteVideo() {
|
|
3482
|
-
await this.executeMethod(
|
|
3399
|
+
await this.executeMethod("call.mute", { channels: ["video"] });
|
|
3483
3400
|
}
|
|
3484
3401
|
/** Unmutes the participant's video. */
|
|
3485
3402
|
async unmuteVideo() {
|
|
3486
|
-
await this.executeMethod(
|
|
3403
|
+
await this.executeMethod("call.unmute", { channels: ["video"] });
|
|
3487
3404
|
}
|
|
3488
3405
|
/** Toggles the participant's video mute state. */
|
|
3489
3406
|
async toggleMuteVideo() {
|
|
@@ -3491,7 +3408,7 @@ var Participant = class extends Destroyable {
|
|
|
3491
3408
|
}
|
|
3492
3409
|
/** Toggles echo cancellation on the audio input. */
|
|
3493
3410
|
async toggleEchoCancellation() {
|
|
3494
|
-
await this.executeMethod(
|
|
3411
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3495
3412
|
echo_cancellation: !this.echoCancellation,
|
|
3496
3413
|
auto_gain: this.autoGain,
|
|
3497
3414
|
noise_suppression: this.noiseSuppression
|
|
@@ -3499,7 +3416,7 @@ var Participant = class extends Destroyable {
|
|
|
3499
3416
|
}
|
|
3500
3417
|
/** Toggles automatic gain control on the audio input. */
|
|
3501
3418
|
async toggleAudioInputAutoGain() {
|
|
3502
|
-
await this.executeMethod(
|
|
3419
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3503
3420
|
echo_cancellation: this.echoCancellation,
|
|
3504
3421
|
auto_gain: !this.autoGain,
|
|
3505
3422
|
noise_suppression: this.noiseSuppression
|
|
@@ -3507,14 +3424,15 @@ var Participant = class extends Destroyable {
|
|
|
3507
3424
|
}
|
|
3508
3425
|
/** Toggles noise suppression on the audio input. */
|
|
3509
3426
|
async toggleNoiseSuppression() {
|
|
3510
|
-
await this.executeMethod(
|
|
3427
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3511
3428
|
echo_cancellation: this.echoCancellation,
|
|
3512
3429
|
auto_gain: this.autoGain,
|
|
3513
3430
|
noise_suppression: !this.noiseSuppression
|
|
3514
3431
|
});
|
|
3515
3432
|
}
|
|
3433
|
+
/** Toggles low-bitrate mode for this participant's media. */
|
|
3516
3434
|
async toggleLowbitrate() {
|
|
3517
|
-
|
|
3435
|
+
await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
|
|
3518
3436
|
}
|
|
3519
3437
|
/**
|
|
3520
3438
|
* Adjusts the **conference-only** microphone energy gate / sensitivity level
|
|
@@ -3530,7 +3448,7 @@ var Participant = class extends Destroyable {
|
|
|
3530
3448
|
* (integer, larger values are more sensitive).
|
|
3531
3449
|
*/
|
|
3532
3450
|
async setAudioInputSensitivity(value) {
|
|
3533
|
-
await this.executeMethod(
|
|
3451
|
+
await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
|
|
3534
3452
|
}
|
|
3535
3453
|
/**
|
|
3536
3454
|
* Sets the **server-side** microphone volume on this participant's bridged
|
|
@@ -3543,7 +3461,7 @@ var Participant = class extends Destroyable {
|
|
|
3543
3461
|
* @param value - Volume level (0-100).
|
|
3544
3462
|
*/
|
|
3545
3463
|
async setAudioInputVolume(value) {
|
|
3546
|
-
await this.executeMethod(
|
|
3464
|
+
await this.executeMethod("call.microphone.volume.set", { volume: value });
|
|
3547
3465
|
}
|
|
3548
3466
|
/**
|
|
3549
3467
|
* Sets the **server-side** speaker volume on this participant's bridged call
|
|
@@ -3557,28 +3475,31 @@ var Participant = class extends Destroyable {
|
|
|
3557
3475
|
* @param value - Volume level (0-100).
|
|
3558
3476
|
*/
|
|
3559
3477
|
async setAudioOutputVolume(value) {
|
|
3560
|
-
await this.executeMethod(
|
|
3478
|
+
await this.executeMethod("call.speaker.volume.set", { volume: value });
|
|
3561
3479
|
}
|
|
3562
3480
|
/**
|
|
3563
3481
|
* Sets the participant's position in the video layout.
|
|
3482
|
+
*
|
|
3483
|
+
* Requires the `member.position` capability. The gateway requires a
|
|
3484
|
+
* `targets` array of `{ target, position }` entries (issue #19400). A
|
|
3485
|
+
* resolved promise does not guarantee a visible change: the backend silently
|
|
3486
|
+
* returns `200` (no-op) for non-conference targets.
|
|
3487
|
+
*
|
|
3564
3488
|
* @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
|
|
3565
3489
|
*/
|
|
3566
3490
|
async setPosition(value) {
|
|
3567
|
-
await this.executeMethod(
|
|
3491
|
+
await this.executeMethod("call.member.position.set", { targets: [{
|
|
3492
|
+
target: this.target,
|
|
3493
|
+
position: value
|
|
3494
|
+
}] });
|
|
3568
3495
|
}
|
|
3569
3496
|
/** Removes this participant from the call. */
|
|
3570
3497
|
async remove() {
|
|
3571
|
-
|
|
3572
|
-
const target = {
|
|
3573
|
-
member_id: this.id,
|
|
3574
|
-
call_id: state.call_id ?? "",
|
|
3575
|
-
node_id: state.node_id ?? ""
|
|
3576
|
-
};
|
|
3577
|
-
await this.executeMethod(target, "call.member.remove", {});
|
|
3498
|
+
await this.executeMethod("call.member.remove", { targets: [this.target] });
|
|
3578
3499
|
}
|
|
3579
3500
|
/** Ends the call for this participant. */
|
|
3580
3501
|
async end() {
|
|
3581
|
-
await this.executeMethod(
|
|
3502
|
+
await this.executeMethod("call.end", {});
|
|
3582
3503
|
}
|
|
3583
3504
|
/**
|
|
3584
3505
|
* Replaces custom metadata for this participant.
|
|
@@ -3598,7 +3519,7 @@ var Participant = class extends Destroyable {
|
|
|
3598
3519
|
}
|
|
3599
3520
|
/** Destroys the participant, releasing all subscriptions and references. */
|
|
3600
3521
|
destroy() {
|
|
3601
|
-
this.
|
|
3522
|
+
this.callExecuteMethod = void 0;
|
|
3602
3523
|
super.destroy();
|
|
3603
3524
|
}
|
|
3604
3525
|
};
|
|
@@ -3610,8 +3531,8 @@ var Participant = class extends Destroyable {
|
|
|
3610
3531
|
*/
|
|
3611
3532
|
var SelfParticipant = class extends Participant {
|
|
3612
3533
|
/** @internal */
|
|
3613
|
-
constructor(id,
|
|
3614
|
-
super(id,
|
|
3534
|
+
constructor(id, callExecuteMethod, vertoManager, deviceController) {
|
|
3535
|
+
super(id, callExecuteMethod, deviceController);
|
|
3615
3536
|
this.vertoManager = vertoManager;
|
|
3616
3537
|
this._studioAudio$ = this.createBehaviorSubject(false);
|
|
3617
3538
|
this.capabilities = new SelfCapabilities();
|
|
@@ -3635,7 +3556,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3635
3556
|
async enableStudioAudio() {
|
|
3636
3557
|
if (this._studioAudio$.value) return;
|
|
3637
3558
|
this._studioAudio$.next(true);
|
|
3638
|
-
await this.executeMethod(
|
|
3559
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3639
3560
|
echo_cancellation: false,
|
|
3640
3561
|
auto_gain: false,
|
|
3641
3562
|
noise_suppression: false
|
|
@@ -3648,18 +3569,27 @@ var SelfParticipant = class extends Participant {
|
|
|
3648
3569
|
async disableStudioAudio() {
|
|
3649
3570
|
if (!this._studioAudio$.value) return;
|
|
3650
3571
|
this._studioAudio$.next(false);
|
|
3651
|
-
await this.executeMethod(
|
|
3572
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3652
3573
|
echo_cancellation: true,
|
|
3653
3574
|
auto_gain: true,
|
|
3654
3575
|
noise_suppression: true
|
|
3655
3576
|
});
|
|
3656
3577
|
}
|
|
3657
|
-
/**
|
|
3578
|
+
/**
|
|
3579
|
+
* Starts sharing the local screen.
|
|
3580
|
+
*
|
|
3581
|
+
* The call is unaffected when acquisition fails.
|
|
3582
|
+
*
|
|
3583
|
+
* @throws The raw `getDisplayMedia` error. A dismissed picker or a
|
|
3584
|
+
* permission denial rejects with a `NotAllowedError` `DOMException` —
|
|
3585
|
+
* inspect `error.name` to tell benign cancels apart from real failures.
|
|
3586
|
+
*/
|
|
3658
3587
|
async startScreenShare() {
|
|
3659
3588
|
try {
|
|
3660
3589
|
await this.vertoManager.addScreenMedia();
|
|
3661
3590
|
} catch (error) {
|
|
3662
|
-
logger$
|
|
3591
|
+
logger$23.error("[Participant.startScreenShare] Screen share error:", error);
|
|
3592
|
+
throw error;
|
|
3663
3593
|
}
|
|
3664
3594
|
}
|
|
3665
3595
|
/** Observable of the current screen share status. */
|
|
@@ -3674,12 +3604,20 @@ var SelfParticipant = class extends Participant {
|
|
|
3674
3604
|
async stopScreenShare() {
|
|
3675
3605
|
return this.vertoManager.removeScreenMedia();
|
|
3676
3606
|
}
|
|
3677
|
-
/**
|
|
3607
|
+
/**
|
|
3608
|
+
* Adds an additional media input device to the call.
|
|
3609
|
+
*
|
|
3610
|
+
* The call is unaffected when acquisition fails.
|
|
3611
|
+
*
|
|
3612
|
+
* @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
|
|
3613
|
+
* permission denial) — inspect `error.name` to decide how to react.
|
|
3614
|
+
*/
|
|
3678
3615
|
async addAdditionalDevice(options) {
|
|
3679
3616
|
try {
|
|
3680
3617
|
await this.vertoManager.addInputDevice(options);
|
|
3681
3618
|
} catch (error) {
|
|
3682
|
-
logger$
|
|
3619
|
+
logger$23.error("[Participant.addAdditionalDevice] Additional device error:", error);
|
|
3620
|
+
throw error;
|
|
3683
3621
|
}
|
|
3684
3622
|
}
|
|
3685
3623
|
/** Removes an additional media input device by ID. */
|
|
@@ -3741,7 +3679,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3741
3679
|
*/
|
|
3742
3680
|
exitStudioModeIfActive() {
|
|
3743
3681
|
if (this._studioAudio$.value) {
|
|
3744
|
-
logger$
|
|
3682
|
+
logger$23.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
|
|
3745
3683
|
this._studioAudio$.next(false);
|
|
3746
3684
|
}
|
|
3747
3685
|
}
|
|
@@ -3765,7 +3703,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3765
3703
|
try {
|
|
3766
3704
|
await super.mute();
|
|
3767
3705
|
} catch (error) {
|
|
3768
|
-
logger$
|
|
3706
|
+
logger$23.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
|
|
3769
3707
|
} finally {
|
|
3770
3708
|
this.vertoManager.muteMainAudioInputDevice();
|
|
3771
3709
|
}
|
|
@@ -3775,7 +3713,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3775
3713
|
try {
|
|
3776
3714
|
await super.unmute();
|
|
3777
3715
|
} catch (error) {
|
|
3778
|
-
logger$
|
|
3716
|
+
logger$23.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
|
|
3779
3717
|
} finally {
|
|
3780
3718
|
await this.vertoManager.unmuteMainAudioInputDevice();
|
|
3781
3719
|
}
|
|
@@ -3785,7 +3723,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3785
3723
|
try {
|
|
3786
3724
|
await super.muteVideo();
|
|
3787
3725
|
} catch (error) {
|
|
3788
|
-
logger$
|
|
3726
|
+
logger$23.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
|
|
3789
3727
|
} finally {
|
|
3790
3728
|
this.vertoManager.muteMainVideoInputDevice();
|
|
3791
3729
|
}
|
|
@@ -3795,7 +3733,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3795
3733
|
try {
|
|
3796
3734
|
await super.unmuteVideo();
|
|
3797
3735
|
} catch (error) {
|
|
3798
|
-
logger$
|
|
3736
|
+
logger$23.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
|
|
3799
3737
|
} finally {
|
|
3800
3738
|
await this.vertoManager.unmuteMainVideoInputDevice();
|
|
3801
3739
|
}
|
|
@@ -3889,7 +3827,7 @@ function isLayoutChangedPayload(value) {
|
|
|
3889
3827
|
|
|
3890
3828
|
//#endregion
|
|
3891
3829
|
//#region src/managers/CallEventsManager.ts
|
|
3892
|
-
const logger$
|
|
3830
|
+
const logger$22 = getLogger();
|
|
3893
3831
|
const initialSessionState = {};
|
|
3894
3832
|
/** @internal */
|
|
3895
3833
|
var CallEventsManager = class extends Destroyable {
|
|
@@ -3993,7 +3931,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
3993
3931
|
}
|
|
3994
3932
|
initSubscriptions() {
|
|
3995
3933
|
this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
|
|
3996
|
-
logger$
|
|
3934
|
+
logger$22.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
|
|
3997
3935
|
callId: callJoinedEvent.call_id,
|
|
3998
3936
|
roomSessionId: callJoinedEvent.room_session_id
|
|
3999
3937
|
});
|
|
@@ -4020,19 +3958,19 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4020
3958
|
if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
|
|
4021
3959
|
});
|
|
4022
3960
|
this.subscribeTo(this.memberUpdates$, (member) => {
|
|
4023
|
-
logger$
|
|
3961
|
+
logger$22.debug("[CallEventsManager] Handling member update event for member ID:", member);
|
|
4024
3962
|
this.upsertParticipant(member);
|
|
4025
3963
|
});
|
|
4026
3964
|
this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
|
|
4027
|
-
logger$
|
|
3965
|
+
logger$22.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
|
|
4028
3966
|
const participants = { ...this._participants$.value };
|
|
4029
3967
|
if (memberLeftEvent.member.member_id in participants) {
|
|
4030
3968
|
delete participants[memberLeftEvent.member.member_id];
|
|
4031
3969
|
this._participants$.next(participants);
|
|
4032
|
-
} else logger$
|
|
3970
|
+
} else logger$22.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
|
|
4033
3971
|
});
|
|
4034
3972
|
this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
|
|
4035
|
-
logger$
|
|
3973
|
+
logger$22.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
|
|
4036
3974
|
const roomSession = callUpdatedEvent.room_session;
|
|
4037
3975
|
this._sessionState$.next({
|
|
4038
3976
|
...this._sessionState$.value,
|
|
@@ -4047,7 +3985,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4047
3985
|
});
|
|
4048
3986
|
});
|
|
4049
3987
|
this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
|
|
4050
|
-
logger$
|
|
3988
|
+
logger$22.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
|
|
4051
3989
|
this._sessionState$.next({
|
|
4052
3990
|
...this._sessionState$.value,
|
|
4053
3991
|
layout_name: layoutChangedEvent.id,
|
|
@@ -4057,10 +3995,10 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4057
3995
|
});
|
|
4058
3996
|
}
|
|
4059
3997
|
updateParticipantPositions(layoutChangedEvent) {
|
|
4060
|
-
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$
|
|
3998
|
+
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$22.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
|
|
4061
3999
|
layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
|
|
4062
4000
|
if (!(layer.member_id in this._participants$.value)) {
|
|
4063
|
-
logger$
|
|
4001
|
+
logger$22.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
|
|
4064
4002
|
return false;
|
|
4065
4003
|
}
|
|
4066
4004
|
return true;
|
|
@@ -4083,7 +4021,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4083
4021
|
layouts: response.result.layouts
|
|
4084
4022
|
});
|
|
4085
4023
|
}).catch((error) => {
|
|
4086
|
-
logger$
|
|
4024
|
+
logger$22.error("[CallEventsManager] Error fetching layouts:", error);
|
|
4087
4025
|
});
|
|
4088
4026
|
}
|
|
4089
4027
|
updateParticipants(members) {
|
|
@@ -4099,7 +4037,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4099
4037
|
}
|
|
4100
4038
|
const participant = this._participants$.value[member.member_id];
|
|
4101
4039
|
const oldValue = participant.value;
|
|
4102
|
-
logger$
|
|
4040
|
+
logger$22.debug("[CallEventsManager] Updating participant:", member.member_id, {
|
|
4103
4041
|
oldValue,
|
|
4104
4042
|
newValue: member
|
|
4105
4043
|
});
|
|
@@ -4112,17 +4050,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4112
4050
|
}
|
|
4113
4051
|
get callJoinedEvent$() {
|
|
4114
4052
|
return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filter(isCallJoinedPayload), tap((event) => {
|
|
4115
|
-
logger$
|
|
4053
|
+
logger$22.debug("[CallEventsManager] Call joined event:", event);
|
|
4116
4054
|
})));
|
|
4117
4055
|
}
|
|
4118
4056
|
get layoutChangedEvent$() {
|
|
4119
4057
|
return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(filterAs(isLayoutChangedPayload, "layout"), tap((event) => {
|
|
4120
|
-
logger$
|
|
4058
|
+
logger$22.debug("[CallEventsManager] Layout changed event:", event);
|
|
4121
4059
|
})));
|
|
4122
4060
|
}
|
|
4123
4061
|
get memberUpdates$() {
|
|
4124
4062
|
return this.cachedObservable("memberUpdates$", () => merge(this.webRtcCallSession.memberJoined$, this.webRtcCallSession.memberUpdated$, this.webRtcCallSession.memberTalking$).pipe(map((event) => event.member), tap((event) => {
|
|
4125
|
-
logger$
|
|
4063
|
+
logger$22.debug("[CallEventsManager] Member update event:", event);
|
|
4126
4064
|
})));
|
|
4127
4065
|
}
|
|
4128
4066
|
destroy() {
|
|
@@ -4378,7 +4316,7 @@ function appendStereoParams(fmtpLine, maxBitrate) {
|
|
|
4378
4316
|
|
|
4379
4317
|
//#endregion
|
|
4380
4318
|
//#region src/controllers/ICEGatheringController.ts
|
|
4381
|
-
const logger$
|
|
4319
|
+
const logger$21 = getLogger();
|
|
4382
4320
|
var ICEGatheringController = class extends Destroyable {
|
|
4383
4321
|
constructor(peerConnection, peerConnectionControllerNegotiating$, options = {}) {
|
|
4384
4322
|
super();
|
|
@@ -4386,23 +4324,23 @@ var ICEGatheringController = class extends Destroyable {
|
|
|
4386
4324
|
this.peerConnectionControllerNegotiating$ = peerConnectionControllerNegotiating$;
|
|
4387
4325
|
this.onicegatheringstatechangeHandler = () => {
|
|
4388
4326
|
const { iceGatheringState } = this.peerConnection;
|
|
4389
|
-
logger$
|
|
4327
|
+
logger$21.debug(`[ICEGatheringController] ICE gathering state changed to: ${iceGatheringState}`);
|
|
4390
4328
|
if (iceGatheringState === "gathering") this._iceCandidatesState.next({
|
|
4391
4329
|
state: "gathering",
|
|
4392
4330
|
validSDP: false
|
|
4393
4331
|
});
|
|
4394
4332
|
};
|
|
4395
4333
|
this.onicecandidateHandler = (event) => {
|
|
4396
|
-
logger$
|
|
4334
|
+
logger$21.debug("[ICEGatheringController] ICE candidate event received:", event.candidate);
|
|
4397
4335
|
this.removeTimer("iceCandidateTimer");
|
|
4398
4336
|
if (event.candidate) this.iceCandidateTimer = setTimeout(() => {
|
|
4399
4337
|
if (this.peerConnection.iceGatheringState !== "complete") {
|
|
4400
|
-
logger$
|
|
4338
|
+
logger$21.warn("[ICEGatheringController] ICE candidate timeout, using current SDP");
|
|
4401
4339
|
this.handleICECandidateTimeout();
|
|
4402
4340
|
}
|
|
4403
4341
|
}, this.iceCandidateTimeout);
|
|
4404
4342
|
else {
|
|
4405
|
-
logger$
|
|
4343
|
+
logger$21.debug("[ICEGatheringController] ICE gathering completed: null candidate received");
|
|
4406
4344
|
this.removeTimer("iceGatheringTimer");
|
|
4407
4345
|
this.handleICEGatheringComplete();
|
|
4408
4346
|
}
|
|
@@ -4420,7 +4358,7 @@ var ICEGatheringController = class extends Destroyable {
|
|
|
4420
4358
|
this.setupEventListeners();
|
|
4421
4359
|
this.iceGatheringTimer = setTimeout(() => {
|
|
4422
4360
|
if (this.peerConnection.iceGatheringState !== "complete") {
|
|
4423
|
-
logger$
|
|
4361
|
+
logger$21.warn("[ICEGatheringController] ICE gathering timeout, using current SDP");
|
|
4424
4362
|
this.handleICEGatheringTimeout();
|
|
4425
4363
|
}
|
|
4426
4364
|
}, this.iceGatheringTimeout);
|
|
@@ -4447,9 +4385,9 @@ var ICEGatheringController = class extends Destroyable {
|
|
|
4447
4385
|
this.relayOnly = value;
|
|
4448
4386
|
}
|
|
4449
4387
|
handleICEGatheringComplete() {
|
|
4450
|
-
logger$
|
|
4451
|
-
logger$
|
|
4452
|
-
logger$
|
|
4388
|
+
logger$21.debug("[ICEGatheringController] Handling ICE gathering complete");
|
|
4389
|
+
logger$21.debug(`[ICEGatheringController] Checking ICE gathering state: ${this.peerConnection.iceGatheringState}`);
|
|
4390
|
+
logger$21.debug("[ICEGatheringController] ICE gathering complete");
|
|
4453
4391
|
this._iceCandidatesState.next({
|
|
4454
4392
|
state: "complete",
|
|
4455
4393
|
validSDP: this.hasValidLocalDescriptionSDP
|
|
@@ -4465,21 +4403,21 @@ var ICEGatheringController = class extends Destroyable {
|
|
|
4465
4403
|
this.removeTimer("iceGatheringTimer");
|
|
4466
4404
|
const validSDP = this.hasValidLocalDescriptionSDP;
|
|
4467
4405
|
if (validSDP) {
|
|
4468
|
-
logger$
|
|
4406
|
+
logger$21.debug("[ICEGatheringController] Local SDP is valid");
|
|
4469
4407
|
this._iceCandidatesState.next({
|
|
4470
4408
|
state: "timeout",
|
|
4471
4409
|
validSDP
|
|
4472
4410
|
});
|
|
4473
4411
|
this.stopGathering();
|
|
4474
|
-
} else logger$
|
|
4412
|
+
} else logger$21.debug("### ICE gathering timeout\n", this.peerConnection.localDescription?.sdp);
|
|
4475
4413
|
}
|
|
4476
4414
|
handleICECandidateTimeout() {
|
|
4477
4415
|
if (this.iceCandidateTimer) this.removeTimer("iceCandidateTimer");
|
|
4478
|
-
logger$
|
|
4416
|
+
logger$21.warn("[ICEGatheringController] ICE candidate timeout");
|
|
4479
4417
|
const validSDP = this.hasValidLocalDescriptionSDP;
|
|
4480
4418
|
if (!validSDP && !this.relayOnly) this.restartICEGatheringWithRelayOnly();
|
|
4481
4419
|
else {
|
|
4482
|
-
logger$
|
|
4420
|
+
logger$21.debug("[ICEGatheringController] Using current SDP due to ICE candidate timeout");
|
|
4483
4421
|
this._iceCandidatesState.next({
|
|
4484
4422
|
state: "timeout",
|
|
4485
4423
|
validSDP
|
|
@@ -4488,7 +4426,7 @@ var ICEGatheringController = class extends Destroyable {
|
|
|
4488
4426
|
}
|
|
4489
4427
|
}
|
|
4490
4428
|
restartICEGatheringWithRelayOnly() {
|
|
4491
|
-
logger$
|
|
4429
|
+
logger$21.debug("[ICEGatheringController] Restarting ICE gathering with relay-only candidates");
|
|
4492
4430
|
this.relayOnly = true;
|
|
4493
4431
|
this.peerConnection.setConfiguration({
|
|
4494
4432
|
...this.peerConnection.getConfiguration(),
|
|
@@ -4503,7 +4441,7 @@ var ICEGatheringController = class extends Destroyable {
|
|
|
4503
4441
|
}
|
|
4504
4442
|
}
|
|
4505
4443
|
clearAllTimers() {
|
|
4506
|
-
logger$
|
|
4444
|
+
logger$21.debug("[ICEGatheringController] Clearing all timers");
|
|
4507
4445
|
this.removeTimer("iceGatheringTimer");
|
|
4508
4446
|
this.removeTimer("iceCandidateTimer");
|
|
4509
4447
|
}
|
|
@@ -4512,7 +4450,7 @@ var ICEGatheringController = class extends Destroyable {
|
|
|
4512
4450
|
this.peerConnection.removeEventListener("icecandidate", this.onicecandidateHandler);
|
|
4513
4451
|
}
|
|
4514
4452
|
destroy() {
|
|
4515
|
-
logger$
|
|
4453
|
+
logger$21.debug("[ICEGatheringController] Destroying ICEGatheringController");
|
|
4516
4454
|
this.clearAllTimers();
|
|
4517
4455
|
this.removeEventListeners();
|
|
4518
4456
|
super.destroy();
|
|
@@ -4521,7 +4459,7 @@ var ICEGatheringController = class extends Destroyable {
|
|
|
4521
4459
|
|
|
4522
4460
|
//#endregion
|
|
4523
4461
|
//#region src/controllers/LocalAudioPipeline.ts
|
|
4524
|
-
const logger$
|
|
4462
|
+
const logger$20 = getLogger();
|
|
4525
4463
|
/**
|
|
4526
4464
|
* Web Audio pipeline for the local microphone stream.
|
|
4527
4465
|
*
|
|
@@ -4633,7 +4571,7 @@ var LocalAudioPipeline = class extends Destroyable {
|
|
|
4633
4571
|
try {
|
|
4634
4572
|
this._inputSource.disconnect();
|
|
4635
4573
|
} catch (error) {
|
|
4636
|
-
logger$
|
|
4574
|
+
logger$20.debug("[LocalAudioPipeline] input disconnect warning:", error);
|
|
4637
4575
|
}
|
|
4638
4576
|
this._inputSource = null;
|
|
4639
4577
|
}
|
|
@@ -4643,7 +4581,7 @@ var LocalAudioPipeline = class extends Destroyable {
|
|
|
4643
4581
|
this._inputSource = this._audioContext.createMediaStreamSource(this._inputStream);
|
|
4644
4582
|
this._inputSource.connect(this._gainNode);
|
|
4645
4583
|
if (this._audioContext.state === "suspended") this._audioContext.resume().catch((error) => {
|
|
4646
|
-
logger$
|
|
4584
|
+
logger$20.warn("[LocalAudioPipeline] AudioContext resume failed:", error);
|
|
4647
4585
|
});
|
|
4648
4586
|
}
|
|
4649
4587
|
destroy() {
|
|
@@ -4658,7 +4596,7 @@ var LocalAudioPipeline = class extends Destroyable {
|
|
|
4658
4596
|
this._analyser.disconnect();
|
|
4659
4597
|
} catch {}
|
|
4660
4598
|
this._audioContext.close().catch((error) => {
|
|
4661
|
-
logger$
|
|
4599
|
+
logger$20.debug("[LocalAudioPipeline] audio context close warning:", error);
|
|
4662
4600
|
});
|
|
4663
4601
|
super.destroy();
|
|
4664
4602
|
}
|
|
@@ -4684,7 +4622,7 @@ var LocalAudioPipeline = class extends Destroyable {
|
|
|
4684
4622
|
|
|
4685
4623
|
//#endregion
|
|
4686
4624
|
//#region src/controllers/LocalStreamController.ts
|
|
4687
|
-
const logger$
|
|
4625
|
+
const logger$19 = getLogger();
|
|
4688
4626
|
var LocalStreamController = class extends Destroyable {
|
|
4689
4627
|
constructor(options) {
|
|
4690
4628
|
super();
|
|
@@ -4722,28 +4660,30 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4722
4660
|
* Build the local media stream based on the provided options.
|
|
4723
4661
|
*/
|
|
4724
4662
|
async buildLocalStream() {
|
|
4725
|
-
logger$
|
|
4663
|
+
logger$19.debug("[LocalStreamController] Building local media stream.");
|
|
4726
4664
|
let stream;
|
|
4727
4665
|
if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
|
|
4728
4666
|
const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
|
|
4729
4667
|
stream = new MediaStream(tracks);
|
|
4730
4668
|
} else if (this.options.propose === "screenshare") {
|
|
4731
|
-
logger$
|
|
4669
|
+
logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", Boolean(this.options.inputAudioDeviceConstraints));
|
|
4732
4670
|
stream = await this.options.getDisplayMedia({
|
|
4733
4671
|
video: true,
|
|
4734
4672
|
audio: Boolean(this.options.inputAudioDeviceConstraints)
|
|
4735
4673
|
});
|
|
4736
|
-
logger$
|
|
4674
|
+
logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
|
|
4737
4675
|
} else {
|
|
4738
4676
|
const constraints = {
|
|
4739
4677
|
audio: this.options.inputAudioDeviceConstraints,
|
|
4740
4678
|
video: this.options.inputVideoDeviceConstraints
|
|
4741
4679
|
};
|
|
4742
|
-
logger$
|
|
4680
|
+
logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
|
|
4743
4681
|
stream = await this.options.getUserMedia(constraints);
|
|
4744
|
-
logger$
|
|
4682
|
+
logger$19.debug("[LocalStreamController] User media obtained:", stream);
|
|
4745
4683
|
}
|
|
4746
4684
|
this._localStream$.next(stream);
|
|
4685
|
+
this._localAudioTracks$.next(stream.getAudioTracks());
|
|
4686
|
+
this._localVideoTracks$.next(stream.getVideoTracks());
|
|
4747
4687
|
return stream;
|
|
4748
4688
|
}
|
|
4749
4689
|
/**
|
|
@@ -4758,7 +4698,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4758
4698
|
this._localStream$.next(localStream);
|
|
4759
4699
|
if (track.kind === "video") this._localVideoTracks$.next(localStream.getVideoTracks());
|
|
4760
4700
|
else this._localAudioTracks$.next(localStream.getAudioTracks());
|
|
4761
|
-
logger$
|
|
4701
|
+
logger$19.debug(`[LocalStreamController] ${track.kind} track added:`, track.id);
|
|
4762
4702
|
return localStream;
|
|
4763
4703
|
}
|
|
4764
4704
|
/**
|
|
@@ -4770,7 +4710,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4770
4710
|
const stream = this._localStream$.value;
|
|
4771
4711
|
const track = stream?.getTracks().find((t) => t.id === trackId);
|
|
4772
4712
|
if (!track) {
|
|
4773
|
-
logger$
|
|
4713
|
+
logger$19.debug(`[LocalStreamController] track not found: ${trackId}`);
|
|
4774
4714
|
return;
|
|
4775
4715
|
}
|
|
4776
4716
|
track.removeEventListener("ended", this.mediaTrackEndedHandler);
|
|
@@ -4779,7 +4719,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4779
4719
|
this._localStream$.next(stream);
|
|
4780
4720
|
if (track.kind === "video") this._localVideoTracks$.next(stream?.getVideoTracks() ?? []);
|
|
4781
4721
|
else this._localAudioTracks$.next(stream?.getAudioTracks() ?? []);
|
|
4782
|
-
logger$
|
|
4722
|
+
logger$19.debug(`[LocalStreamController] ${track.kind} track removed:`, trackId);
|
|
4783
4723
|
return track;
|
|
4784
4724
|
}
|
|
4785
4725
|
/**
|
|
@@ -4814,7 +4754,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4814
4754
|
*/
|
|
4815
4755
|
stopAllTracks() {
|
|
4816
4756
|
this._localStream$.value?.getTracks().forEach((track) => {
|
|
4817
|
-
logger$
|
|
4757
|
+
logger$19.debug(`[LocalStreamController] Stopping local track: ${track.kind}`);
|
|
4818
4758
|
track.removeEventListener("ended", this.mediaTrackEndedHandler);
|
|
4819
4759
|
track.stop();
|
|
4820
4760
|
});
|
|
@@ -4830,7 +4770,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4830
4770
|
|
|
4831
4771
|
//#endregion
|
|
4832
4772
|
//#region src/controllers/TransceiverController.ts
|
|
4833
|
-
const logger$
|
|
4773
|
+
const logger$18 = getLogger();
|
|
4834
4774
|
const getDirection = (send, recv) => {
|
|
4835
4775
|
if (send && recv) return "sendrecv";
|
|
4836
4776
|
else if (send && !recv) return "sendonly";
|
|
@@ -4911,6 +4851,12 @@ var TransceiverController = class extends Destroyable {
|
|
|
4911
4851
|
scaleResolutionDownBy: Number(rid) * 6 || 1
|
|
4912
4852
|
}));
|
|
4913
4853
|
}
|
|
4854
|
+
/**
|
|
4855
|
+
* Resolve the current MediaTrackConstraints for an input kind, normalising
|
|
4856
|
+
* boolean shorthand to an empty object. Public so the surrounding
|
|
4857
|
+
* RTCPeerConnectionController can drive its own pipeline-aware getUserMedia
|
|
4858
|
+
* call with the same effective constraints the transceiver would have used.
|
|
4859
|
+
*/
|
|
4914
4860
|
getConstraintsFor(kind) {
|
|
4915
4861
|
const constraints = kind === "audio" ? this.inputAudioDeviceConstraints : this.inputVideoDeviceConstraints;
|
|
4916
4862
|
return typeof constraints === "boolean" ? {} : constraints;
|
|
@@ -4932,7 +4878,7 @@ var TransceiverController = class extends Destroyable {
|
|
|
4932
4878
|
sendEncodings: isAudio ? void 0 : this.sendEncodings,
|
|
4933
4879
|
streams: direction === "recvonly" ? void 0 : [localStream]
|
|
4934
4880
|
};
|
|
4935
|
-
logger$
|
|
4881
|
+
logger$18.debug(`[TransceiverController] Setting up transceiver sender for local ${track.kind} track:`, {
|
|
4936
4882
|
transceiver,
|
|
4937
4883
|
transceiverParams
|
|
4938
4884
|
});
|
|
@@ -4940,11 +4886,11 @@ var TransceiverController = class extends Destroyable {
|
|
|
4940
4886
|
await transceiver.sender.replaceTrack(track);
|
|
4941
4887
|
transceiver.direction = transceiverParams.direction;
|
|
4942
4888
|
if (transceiverParams.streams?.some((stream) => Boolean(stream))) {
|
|
4943
|
-
logger$
|
|
4889
|
+
logger$18.debug(`[TransceiverController] Setting streams for transceiver sender for local ${track.kind} track:`, transceiverParams.streams);
|
|
4944
4890
|
transceiver.sender.setStreams(...transceiverParams.streams);
|
|
4945
4891
|
}
|
|
4946
4892
|
} else {
|
|
4947
|
-
logger$
|
|
4893
|
+
logger$18.debug(`[TransceiverController] Adding new transceiver for local ${track.kind} track:`, track.id);
|
|
4948
4894
|
this.peerConnection.addTransceiver(track, transceiverParams);
|
|
4949
4895
|
}
|
|
4950
4896
|
}
|
|
@@ -4958,13 +4904,13 @@ var TransceiverController = class extends Destroyable {
|
|
|
4958
4904
|
if (options.updateTransceiverDirection) transceiver.direction = "inactive";
|
|
4959
4905
|
}
|
|
4960
4906
|
} catch (error) {
|
|
4961
|
-
logger$
|
|
4907
|
+
logger$18.error("[TransceiverController] stopTrackSender error", kind, error);
|
|
4962
4908
|
this.options.onError?.(new MediaTrackError("stopTrackSender", kind, error));
|
|
4963
4909
|
}
|
|
4964
4910
|
}
|
|
4965
4911
|
async restoreTrackSender(kind) {
|
|
4966
4912
|
try {
|
|
4967
|
-
logger$
|
|
4913
|
+
logger$18.debug("[TransceiverController] restoreTrackSender called", kind);
|
|
4968
4914
|
const constraints = {};
|
|
4969
4915
|
const transceivers = this.transceiverByKind(kind);
|
|
4970
4916
|
for (const transceiver of transceivers) {
|
|
@@ -4974,23 +4920,23 @@ var TransceiverController = class extends Destroyable {
|
|
|
4974
4920
|
if (trackKind === "audio" || trackKind === "video") constraints[trackKind] = this.getConstraintsFor(trackKind);
|
|
4975
4921
|
}
|
|
4976
4922
|
}
|
|
4977
|
-
logger$
|
|
4923
|
+
logger$18.debug("[TransceiverController] restoreTrackSender constraints:", constraints);
|
|
4978
4924
|
if (Object.keys(constraints).length === 0) {
|
|
4979
|
-
logger$
|
|
4925
|
+
logger$18.warn("[TransceiverController] restoreTrackSender: no tracks need restoration", kind);
|
|
4980
4926
|
return;
|
|
4981
4927
|
}
|
|
4982
4928
|
const newTracks = (await this.options.getUserMedia(constraints)).getTracks();
|
|
4983
|
-
logger$
|
|
4929
|
+
logger$18.debug("[TransceiverController] restoreTrackSender new tracks:", newTracks);
|
|
4984
4930
|
for (const newTrack of newTracks) {
|
|
4985
4931
|
this.options.localStreamController.addTrack(newTrack);
|
|
4986
4932
|
const trackKind = newTrack.kind;
|
|
4987
4933
|
const transceiverOfKind = this.transceiverByKind(trackKind)[0];
|
|
4988
4934
|
transceiverOfKind.direction = trackKind === "audio" ? this.audioDirection : this.videoDirection;
|
|
4989
|
-
logger$
|
|
4935
|
+
logger$18.debug("[TransceiverController] restoreTrackSender setting direction for", trackKind, transceiverOfKind.direction);
|
|
4990
4936
|
await transceiverOfKind.sender.replaceTrack(newTrack);
|
|
4991
4937
|
}
|
|
4992
4938
|
} catch (error) {
|
|
4993
|
-
logger$
|
|
4939
|
+
logger$18.error("[TransceiverController] restoreTrackSender error", kind, error);
|
|
4994
4940
|
this.options.onError?.(new MediaTrackError("restoreTrackSender", kind, error));
|
|
4995
4941
|
}
|
|
4996
4942
|
}
|
|
@@ -5031,14 +4977,14 @@ var TransceiverController = class extends Destroyable {
|
|
|
5031
4977
|
};
|
|
5032
4978
|
try {
|
|
5033
4979
|
await track.applyConstraints(constraintsToApply);
|
|
5034
|
-
logger$
|
|
5035
|
-
logger$
|
|
4980
|
+
logger$18.debug(`[TransceiverController] Updated ${kind} sender constraints:`, constraintsToApply);
|
|
4981
|
+
logger$18.debug(`[TransceiverController] Updated ${kind} sender constraints:`, track.getConstraints());
|
|
5036
4982
|
} catch (error) {
|
|
5037
|
-
logger$
|
|
4983
|
+
logger$18.warn(`[TransceiverController] applyConstraints failed for ${kind} track ${track.id}, attempting track replacement fallback:`, error);
|
|
5038
4984
|
try {
|
|
5039
4985
|
await this.replaceTrackFallback(sender, track, kind, constraintsToApply);
|
|
5040
4986
|
} catch (fallbackError) {
|
|
5041
|
-
logger$
|
|
4987
|
+
logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
|
|
5042
4988
|
this.options.onError?.(new MediaTrackError("updateSendersConstraints", kind, fallbackError));
|
|
5043
4989
|
}
|
|
5044
4990
|
}
|
|
@@ -5066,7 +5012,7 @@ var TransceiverController = class extends Destroyable {
|
|
|
5066
5012
|
if (!newTrack) throw new MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
5067
5013
|
await sender.replaceTrack(newTrack);
|
|
5068
5014
|
this.options.localStreamController.addTrack(newTrack);
|
|
5069
|
-
logger$
|
|
5015
|
+
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
|
|
5070
5016
|
}
|
|
5071
5017
|
getMediaDirections() {
|
|
5072
5018
|
if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
|
|
@@ -5096,7 +5042,7 @@ var TransceiverController = class extends Destroyable {
|
|
|
5096
5042
|
|
|
5097
5043
|
//#endregion
|
|
5098
5044
|
//#region src/controllers/RTCPeerConnectionController.ts
|
|
5099
|
-
const logger$
|
|
5045
|
+
const logger$17 = getLogger();
|
|
5100
5046
|
var RTCPeerConnectionController = class extends Destroyable {
|
|
5101
5047
|
constructor(options = {}, remoteSessionDescription, deviceController) {
|
|
5102
5048
|
super();
|
|
@@ -5112,43 +5058,43 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5112
5058
|
this.oniceconnectionstatechangeHandler = () => {
|
|
5113
5059
|
if (this.peerConnection) {
|
|
5114
5060
|
const { iceConnectionState } = this.peerConnection;
|
|
5115
|
-
logger$
|
|
5061
|
+
logger$17.debug(`[RTCPeerConnectionController] ICE connection state changed to: ${iceConnectionState}`);
|
|
5116
5062
|
this._iceConnectionState$.next(this.peerConnection.iceConnectionState);
|
|
5117
5063
|
}
|
|
5118
5064
|
};
|
|
5119
5065
|
this.onconnectionstatechangeHandler = () => {
|
|
5120
5066
|
if (this.peerConnection) {
|
|
5121
5067
|
const { connectionState } = this.peerConnection;
|
|
5122
|
-
logger$
|
|
5068
|
+
logger$17.debug(`[RTCPeerConnectionController] Connection state changed to: ${connectionState}`);
|
|
5123
5069
|
if (connectionState === "connected") this.removeConnectionTimer();
|
|
5124
5070
|
this._connectionState$.next(this.peerConnection.connectionState);
|
|
5125
5071
|
}
|
|
5126
5072
|
};
|
|
5127
5073
|
this.onsignalingstatechangeHandler = () => {
|
|
5128
|
-
logger$
|
|
5074
|
+
logger$17.debug(`[RTCPeerConnectionController] Signaling state changed to: ${this.peerConnection?.signalingState}`);
|
|
5129
5075
|
};
|
|
5130
5076
|
this.onicegatheringstatechangeHandler = () => {
|
|
5131
5077
|
if (this.peerConnection) this._iceGatheringState$.next(this.peerConnection.iceGatheringState);
|
|
5132
5078
|
};
|
|
5133
5079
|
this.onnegotiationneededHandler = (event) => {
|
|
5134
|
-
logger$
|
|
5080
|
+
logger$17.debug("[RTCPeerConnectionController] Negotiation needed event received.", event);
|
|
5135
5081
|
this.negotiationNeeded$.next();
|
|
5136
5082
|
};
|
|
5137
5083
|
this.updateSelectedInputDevice = async (kind, deviceInfo) => {
|
|
5138
5084
|
try {
|
|
5139
5085
|
const { localStream } = this;
|
|
5140
5086
|
if (!localStream) {
|
|
5141
|
-
logger$
|
|
5087
|
+
logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
|
|
5142
5088
|
return;
|
|
5143
5089
|
}
|
|
5144
|
-
logger$
|
|
5090
|
+
logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
|
|
5145
5091
|
const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
|
|
5146
5092
|
if (track) {
|
|
5147
5093
|
this.transceiverController?.stopTrackSender(kind);
|
|
5148
5094
|
this.localStreamController.removeTrack(track.id);
|
|
5149
|
-
logger$
|
|
5095
|
+
logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
|
|
5150
5096
|
if (!deviceInfo) {
|
|
5151
|
-
logger$
|
|
5097
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
5152
5098
|
return;
|
|
5153
5099
|
}
|
|
5154
5100
|
const streamTrack = (await this.getUserMedia({ [kind]: {
|
|
@@ -5156,16 +5102,16 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5156
5102
|
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
5157
5103
|
} })).getTracks().find((t) => t.kind === kind);
|
|
5158
5104
|
if (streamTrack) {
|
|
5159
|
-
logger$
|
|
5105
|
+
logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
|
|
5160
5106
|
this.localStreamController.addTrack(streamTrack);
|
|
5161
5107
|
await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
|
|
5162
|
-
logger$
|
|
5108
|
+
logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
|
|
5163
5109
|
}
|
|
5164
5110
|
}
|
|
5165
|
-
logger$
|
|
5111
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
|
|
5166
5112
|
} catch (error) {
|
|
5167
|
-
logger$
|
|
5168
|
-
this._errors$.next(
|
|
5113
|
+
logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
|
|
5114
|
+
this._errors$.next(new MediaTrackError("updateSelectedInputDevice", kind, error));
|
|
5169
5115
|
throw error;
|
|
5170
5116
|
}
|
|
5171
5117
|
};
|
|
@@ -5236,7 +5182,19 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5236
5182
|
return this._memberId;
|
|
5237
5183
|
}
|
|
5238
5184
|
stopTrackSender(kind, options = { updateTransceiverDirection: false }) {
|
|
5239
|
-
|
|
5185
|
+
const audioCovered = kind === "audio" || kind === "both";
|
|
5186
|
+
if (audioCovered && this._localAudioPipeline) this.stopRawAudioInputForPipeline();
|
|
5187
|
+
if (!audioCovered) this.transceiverController?.stopTrackSender(kind, options);
|
|
5188
|
+
else if (kind === "both") this.transceiverController?.stopTrackSender("video", options);
|
|
5189
|
+
else if (!this._localAudioPipeline) this.transceiverController?.stopTrackSender(kind, options);
|
|
5190
|
+
}
|
|
5191
|
+
stopRawAudioInputForPipeline() {
|
|
5192
|
+
const rawTracks = this.localStreamController.localAudioTracks;
|
|
5193
|
+
for (const track of rawTracks) if (track.readyState === "live") {
|
|
5194
|
+
track.stop();
|
|
5195
|
+
this.localStreamController.removeTrack(track.id);
|
|
5196
|
+
}
|
|
5197
|
+
this._localAudioPipeline?.setInputTrack(null);
|
|
5240
5198
|
}
|
|
5241
5199
|
get isNegotiating$() {
|
|
5242
5200
|
return this._isNegotiating$.asObservable();
|
|
@@ -5375,7 +5333,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5375
5333
|
case "main":
|
|
5376
5334
|
default: return {
|
|
5377
5335
|
...options,
|
|
5378
|
-
offerToReceiveAudio: true,
|
|
5336
|
+
offerToReceiveAudio: this.options.receiveAudio ?? true,
|
|
5379
5337
|
offerToReceiveVideo: this.options.receiveVideo ?? Boolean(this.inputVideoDeviceConstraints)
|
|
5380
5338
|
};
|
|
5381
5339
|
}
|
|
@@ -5402,15 +5360,15 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5402
5360
|
this.setupPeerConnection();
|
|
5403
5361
|
this.subscribeTo(this.negotiationNeeded$.pipe(auditTime(0), exhaustMap(async () => this.startNegotiation())), {
|
|
5404
5362
|
next: () => {
|
|
5405
|
-
logger$
|
|
5363
|
+
logger$17.debug("[RTCPeerConnectionController] Start Negotiation completed successfully");
|
|
5406
5364
|
},
|
|
5407
5365
|
error: (error) => {
|
|
5408
|
-
logger$
|
|
5366
|
+
logger$17.error("[RTCPeerConnectionController] Start Negotiation error:", error);
|
|
5409
5367
|
this._errors$.next(toError(error));
|
|
5410
5368
|
}
|
|
5411
5369
|
});
|
|
5412
5370
|
this.subscribeTo(merge(this.deviceController.selectedAudioInputDevice$.pipe(map((deviceInfo) => ["audio", deviceInfo])), this.deviceController.selectedVideoInputDevice$.pipe(map((deviceInfo) => ["video", deviceInfo]))).pipe(skipWhile(() => !this.localStreamController.localStream)), async ([kind, deviceInfo]) => {
|
|
5413
|
-
logger$
|
|
5371
|
+
logger$17.debug(`[RTCPeerConnectionController] Selected input device changed for:`, {
|
|
5414
5372
|
kind,
|
|
5415
5373
|
deviceInfo
|
|
5416
5374
|
});
|
|
@@ -5427,7 +5385,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5427
5385
|
this._initialized$.next(true);
|
|
5428
5386
|
}
|
|
5429
5387
|
} catch (error) {
|
|
5430
|
-
logger$
|
|
5388
|
+
logger$17.error("[RTCPeerConnectionController] Initialization error:", error);
|
|
5431
5389
|
this._errors$.next(toError(error));
|
|
5432
5390
|
this.destroy();
|
|
5433
5391
|
}
|
|
@@ -5459,22 +5417,22 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5459
5417
|
}
|
|
5460
5418
|
async startNegotiation() {
|
|
5461
5419
|
if (this.isNegotiating) {
|
|
5462
|
-
logger$
|
|
5420
|
+
logger$17.debug("[RTCPeerConnectionController] Negotiation already in progress, skipping.");
|
|
5463
5421
|
return;
|
|
5464
5422
|
}
|
|
5465
5423
|
this.setupEventListeners();
|
|
5466
5424
|
if (this.type === "answer") {
|
|
5467
|
-
logger$
|
|
5425
|
+
logger$17.debug("[RTCPeerConnectionController] This is an answer type still, skipping offer creation.");
|
|
5468
5426
|
return;
|
|
5469
5427
|
}
|
|
5470
5428
|
this._isNegotiating$.next(true);
|
|
5471
|
-
logger$
|
|
5429
|
+
logger$17.debug("[RTCPeerConnectionController] Starting negotiation.");
|
|
5472
5430
|
try {
|
|
5473
5431
|
const { offerOptions } = this;
|
|
5474
|
-
logger$
|
|
5432
|
+
logger$17.debug("[RTCPeerConnectionController] Creating offer with options:", offerOptions);
|
|
5475
5433
|
await this.createOffer(offerOptions);
|
|
5476
5434
|
} catch (error) {
|
|
5477
|
-
logger$
|
|
5435
|
+
logger$17.error("[RTCPeerConnectionController] Error during negotiation:", error);
|
|
5478
5436
|
this._errors$.next(toError(error));
|
|
5479
5437
|
}
|
|
5480
5438
|
}
|
|
@@ -5490,14 +5448,14 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5490
5448
|
let readyToConnect = status !== "failed";
|
|
5491
5449
|
try {
|
|
5492
5450
|
if (status === "received" && sdp) {
|
|
5493
|
-
logger$
|
|
5451
|
+
logger$17.debug("[RTCPeerConnectionController] Received answer SDP:", sdp);
|
|
5494
5452
|
await this._setRemoteDescription({
|
|
5495
5453
|
type: "answer",
|
|
5496
5454
|
sdp
|
|
5497
5455
|
});
|
|
5498
5456
|
}
|
|
5499
5457
|
} catch (error) {
|
|
5500
|
-
logger$
|
|
5458
|
+
logger$17.error("[RTCPeerConnectionController] Error updating answer status:", error);
|
|
5501
5459
|
this._errors$.next(toError(error));
|
|
5502
5460
|
readyToConnect = false;
|
|
5503
5461
|
} finally {
|
|
@@ -5516,7 +5474,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5516
5474
|
await this.handleOfferReceived();
|
|
5517
5475
|
break;
|
|
5518
5476
|
case "failed":
|
|
5519
|
-
logger$
|
|
5477
|
+
logger$17.error("[RTCPeerConnectionController] Offer failed to be processed by remote.");
|
|
5520
5478
|
break;
|
|
5521
5479
|
case "sent":
|
|
5522
5480
|
default:
|
|
@@ -5529,13 +5487,14 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5529
5487
|
*/
|
|
5530
5488
|
async acceptInbound(mediaOverrides) {
|
|
5531
5489
|
if (mediaOverrides) {
|
|
5532
|
-
const { audio, video, receiveAudio, receiveVideo } = mediaOverrides;
|
|
5490
|
+
const { audio, video, receiveAudio, receiveVideo, fallbackToReceiveOnly } = mediaOverrides;
|
|
5533
5491
|
this.options = {
|
|
5534
5492
|
...this.options,
|
|
5535
5493
|
...audio !== void 0 ? { audio } : {},
|
|
5536
5494
|
...video !== void 0 ? { video } : {},
|
|
5537
5495
|
...receiveAudio !== void 0 ? { receiveAudio } : {},
|
|
5538
|
-
...receiveVideo !== void 0 ? { receiveVideo } : {}
|
|
5496
|
+
...receiveVideo !== void 0 ? { receiveVideo } : {},
|
|
5497
|
+
...fallbackToReceiveOnly !== void 0 ? { fallbackToReceiveOnly } : {}
|
|
5539
5498
|
};
|
|
5540
5499
|
this.transceiverController?.updateOptions({
|
|
5541
5500
|
receiveAudio: this.receiveAudio,
|
|
@@ -5548,7 +5507,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5548
5507
|
}
|
|
5549
5508
|
await this.setupLocalTracks();
|
|
5550
5509
|
const { answerOptions } = this;
|
|
5551
|
-
logger$
|
|
5510
|
+
logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
|
|
5552
5511
|
await this.createAnswer(answerOptions);
|
|
5553
5512
|
}
|
|
5554
5513
|
async handleOfferReceived() {
|
|
@@ -5556,7 +5515,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5556
5515
|
this._isNegotiating$.next(true);
|
|
5557
5516
|
await this._setRemoteDescription(this.sdpInit);
|
|
5558
5517
|
const { answerOptions } = this;
|
|
5559
|
-
logger$
|
|
5518
|
+
logger$17.debug("[RTCPeerConnectionController] Creating answer with options:", answerOptions);
|
|
5560
5519
|
await this.createAnswer(answerOptions);
|
|
5561
5520
|
}
|
|
5562
5521
|
readyToConnect() {
|
|
@@ -5564,7 +5523,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5564
5523
|
this.connectionTimer = setTimeout(() => {
|
|
5565
5524
|
this.removeConnectionTimer();
|
|
5566
5525
|
if (this.peerConnection?.connectionState !== "connected") {
|
|
5567
|
-
logger$
|
|
5526
|
+
logger$17.debug("[RTCPeerConnectionController] Connection timeout, restarting ICE gathering with relay only.");
|
|
5568
5527
|
this.iceGatheringController.restartICEGatheringWithRelayOnly();
|
|
5569
5528
|
}
|
|
5570
5529
|
}, this.connectionTimeout);
|
|
@@ -5586,14 +5545,14 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5586
5545
|
const stereo = this.options.stereo ?? PreferencesContainer.instance.stereoAudio;
|
|
5587
5546
|
if (preferredAudioCodecs.length > 0 || preferredVideoCodecs.length > 0) {
|
|
5588
5547
|
result = setCodecPreferences(result, preferredAudioCodecs, preferredVideoCodecs);
|
|
5589
|
-
logger$
|
|
5548
|
+
logger$17.debug("[RTCPeerConnectionController] Applied codec preferences to SDP", {
|
|
5590
5549
|
preferredAudioCodecs,
|
|
5591
5550
|
preferredVideoCodecs
|
|
5592
5551
|
});
|
|
5593
5552
|
}
|
|
5594
5553
|
if (stereo) {
|
|
5595
5554
|
result = enableStereoOpus(result);
|
|
5596
|
-
logger$
|
|
5555
|
+
logger$17.debug("[RTCPeerConnectionController] Applied stereo Opus to SDP");
|
|
5597
5556
|
}
|
|
5598
5557
|
return Promise.resolve(result);
|
|
5599
5558
|
}
|
|
@@ -5649,25 +5608,25 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5649
5608
|
...this.peerConnection.getConfiguration(),
|
|
5650
5609
|
iceTransportPolicy: "relay"
|
|
5651
5610
|
});
|
|
5652
|
-
logger$
|
|
5611
|
+
logger$17.debug("[RTCPeerConnectionController] ICE transport policy set to relay-only");
|
|
5653
5612
|
} catch (error) {
|
|
5654
|
-
logger$
|
|
5613
|
+
logger$17.warn("[RTCPeerConnectionController] Failed to set relay-only policy:", error);
|
|
5655
5614
|
}
|
|
5656
5615
|
this.setupEventListeners();
|
|
5657
5616
|
this._isNegotiating$.next(true);
|
|
5658
|
-
logger$
|
|
5617
|
+
logger$17.debug(`[RTCPeerConnectionController] Triggering ICE restart${relayOnly ? " (relay-only)" : ""}.`);
|
|
5659
5618
|
try {
|
|
5660
5619
|
const offer = await this.peerConnection.createOffer({ iceRestart: true });
|
|
5661
5620
|
await this.setLocalDescription(offer);
|
|
5662
5621
|
} catch (error) {
|
|
5663
|
-
logger$
|
|
5622
|
+
logger$17.error("[RTCPeerConnectionController] ICE restart offer failed:", error);
|
|
5664
5623
|
this._errors$.next(toError(error));
|
|
5665
5624
|
this.negotiationEnded();
|
|
5666
5625
|
if (policyChanged) this.restoreIceTransportPolicy();
|
|
5667
5626
|
throw error;
|
|
5668
5627
|
}
|
|
5669
5628
|
if (policyChanged) firstValueFrom(race(this._iceGatheringState$.pipe(filter((state) => state === "complete"), take(1)), timer(ICE_GATHERING_COMPLETE_TIMEOUT_MS).pipe(map(() => "timeout")))).then(() => this.restoreIceTransportPolicy()).catch((error) => {
|
|
5670
|
-
logger$
|
|
5629
|
+
logger$17.warn("[RTCPeerConnectionController] Error waiting for ICE gathering to complete:", error);
|
|
5671
5630
|
this.restoreIceTransportPolicy();
|
|
5672
5631
|
});
|
|
5673
5632
|
}
|
|
@@ -5677,9 +5636,9 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5677
5636
|
...this.peerConnection.getConfiguration(),
|
|
5678
5637
|
iceTransportPolicy: this.options.relayOnly ? "relay" : "all"
|
|
5679
5638
|
});
|
|
5680
|
-
logger$
|
|
5639
|
+
logger$17.debug("[RTCPeerConnectionController] ICE transport policy restored");
|
|
5681
5640
|
} catch (error) {
|
|
5682
|
-
logger$
|
|
5641
|
+
logger$17.warn("[RTCPeerConnectionController] Failed to restore ICE transport policy:", error);
|
|
5683
5642
|
}
|
|
5684
5643
|
}
|
|
5685
5644
|
/**
|
|
@@ -5691,13 +5650,25 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5691
5650
|
await this.setupRemoteTracks();
|
|
5692
5651
|
}
|
|
5693
5652
|
async setupLocalTracks() {
|
|
5694
|
-
logger$
|
|
5695
|
-
|
|
5653
|
+
logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
|
|
5654
|
+
if (this.hasNoLocalMediaToSend()) {
|
|
5655
|
+
if (!this.receiveAudio && !this.receiveVideo) throw new InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
|
|
5656
|
+
logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
|
|
5657
|
+
this.setupReceiveOnlyTransceivers();
|
|
5658
|
+
return;
|
|
5659
|
+
}
|
|
5660
|
+
let localStream;
|
|
5661
|
+
try {
|
|
5662
|
+
localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
|
|
5663
|
+
} catch (error) {
|
|
5664
|
+
this.handleLocalMediaFailure(error);
|
|
5665
|
+
return;
|
|
5666
|
+
}
|
|
5696
5667
|
if (this.transceiverController?.useAddStream ?? false) {
|
|
5697
|
-
logger$
|
|
5668
|
+
logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
|
|
5698
5669
|
this.peerConnection?.addStream(localStream);
|
|
5699
5670
|
if (!this.isNegotiating) {
|
|
5700
|
-
logger$
|
|
5671
|
+
logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
|
|
5701
5672
|
this.negotiationNeeded$.next();
|
|
5702
5673
|
}
|
|
5703
5674
|
return;
|
|
@@ -5713,12 +5684,54 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5713
5684
|
const transceivers = (kind === "audio" ? this.transceiverController?.audioTransceivers : this.transceiverController?.videoTransceivers) ?? [];
|
|
5714
5685
|
await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
|
|
5715
5686
|
} else {
|
|
5716
|
-
logger$
|
|
5687
|
+
logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
|
|
5717
5688
|
this.peerConnection?.addTrack(track, localStream);
|
|
5718
5689
|
}
|
|
5719
5690
|
}
|
|
5720
5691
|
}
|
|
5721
5692
|
}
|
|
5693
|
+
/** True for a main connection with no local media to send. */
|
|
5694
|
+
hasNoLocalMediaToSend() {
|
|
5695
|
+
const hasInputStreams = Boolean(this.options.inputAudioStream ?? this.options.inputVideoStream);
|
|
5696
|
+
return this.propose === "main" && !this.localStream && !hasInputStreams && !this.inputAudioDeviceConstraints && !this.inputVideoDeviceConstraints;
|
|
5697
|
+
}
|
|
5698
|
+
/** The media kinds this connection wants to send: 'audiovideo' | 'video' | 'audio'. */
|
|
5699
|
+
get requestedMediaKinds() {
|
|
5700
|
+
const wantsAudio = Boolean(this.inputAudioDeviceConstraints);
|
|
5701
|
+
const wantsVideo = Boolean(this.inputVideoDeviceConstraints);
|
|
5702
|
+
if (wantsAudio && wantsVideo) return "audiovideo";
|
|
5703
|
+
return wantsVideo ? "video" : "audio";
|
|
5704
|
+
}
|
|
5705
|
+
/**
|
|
5706
|
+
* Handle a local media acquisition failure with a typed, semantically
|
|
5707
|
+
* accurate MediaAccessError created at the acquisition site:
|
|
5708
|
+
* - Auxiliary connections (screenshare / additional-device) throw a
|
|
5709
|
+
* non-fatal error — VertoManager surfaces it and the call is unaffected.
|
|
5710
|
+
* - The main connection degrades to receive-only when allowed (default),
|
|
5711
|
+
* otherwise fails with a fatal error.
|
|
5712
|
+
*/
|
|
5713
|
+
handleLocalMediaFailure(error) {
|
|
5714
|
+
if (this.propose === "screenshare") throw new MediaAccessError("startScreenShare", "screen", error, false);
|
|
5715
|
+
if (this.propose === "additional-device") throw new MediaAccessError("addInputDevice", this.requestedMediaKinds, error, false);
|
|
5716
|
+
const canReceive = this.receiveAudio || this.receiveVideo;
|
|
5717
|
+
if (!((this.options.fallbackToReceiveOnly ?? true) && canReceive)) throw new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, true);
|
|
5718
|
+
logger$17.warn("[RTCPeerConnectionController] Local media unavailable; continuing receive-only:", error);
|
|
5719
|
+
this._errors$.next(new MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, false));
|
|
5720
|
+
this.setupReceiveOnlyTransceivers();
|
|
5721
|
+
}
|
|
5722
|
+
/**
|
|
5723
|
+
* Negotiate receive-only m-lines when there are no local tracks to send.
|
|
5724
|
+
* Only offer-type connections add transceivers — answer-type connections
|
|
5725
|
+
* reuse the transceivers created from the remote offer.
|
|
5726
|
+
*/
|
|
5727
|
+
setupReceiveOnlyTransceivers() {
|
|
5728
|
+
if (this.type !== "offer") return;
|
|
5729
|
+
if (this.transceiverController?.useAddTransceivers ?? false) {
|
|
5730
|
+
this.peerConnection?.addTransceiver("audio", { direction: this.receiveAudio ? "recvonly" : "inactive" });
|
|
5731
|
+
this.peerConnection?.addTransceiver("video", { direction: this.receiveVideo ? "recvonly" : "inactive" });
|
|
5732
|
+
}
|
|
5733
|
+
if (!this.isNegotiating) this.negotiationNeeded$.next();
|
|
5734
|
+
}
|
|
5722
5735
|
async getUserMedia(constraints) {
|
|
5723
5736
|
return (this.options.webRTCApiProvider?.mediaDevices ?? navigator.mediaDevices).getUserMedia(constraints);
|
|
5724
5737
|
}
|
|
@@ -5730,7 +5743,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5730
5743
|
async setupRemoteTracks() {
|
|
5731
5744
|
if (!this.peerConnection) throw new DependencyError("RTCPeerConnection is not initialized");
|
|
5732
5745
|
this.peerConnection.ontrack = (event) => {
|
|
5733
|
-
logger$
|
|
5746
|
+
logger$17.debug("[RTCPeerConnectionController] Remote track received:", event.track.kind);
|
|
5734
5747
|
if (event.streams[0]) this._remoteStream$.next(event.streams[0]);
|
|
5735
5748
|
else {
|
|
5736
5749
|
const existingTracks = this._remoteStream$.value?.getTracks() ?? [];
|
|
@@ -5741,8 +5754,27 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5741
5754
|
await this.transceiverController?.setupRemoteTransceivers(this.type);
|
|
5742
5755
|
}
|
|
5743
5756
|
async restoreTrackSender(kind) {
|
|
5744
|
-
|
|
5745
|
-
if (
|
|
5757
|
+
const audioCovered = kind === "audio" || kind === "both";
|
|
5758
|
+
if (audioCovered && this._localAudioPipeline) await this.restoreRawAudioInputForPipeline();
|
|
5759
|
+
if (!audioCovered) await this.transceiverController?.restoreTrackSender(kind);
|
|
5760
|
+
else if (kind === "both") await this.transceiverController?.restoreTrackSender("video");
|
|
5761
|
+
else if (!this._localAudioPipeline) await this.transceiverController?.restoreTrackSender(kind);
|
|
5762
|
+
}
|
|
5763
|
+
async restoreRawAudioInputForPipeline() {
|
|
5764
|
+
if (!this._localAudioPipeline) return;
|
|
5765
|
+
const constraints = this.transceiverController?.getConstraintsFor("audio") ?? {};
|
|
5766
|
+
let stream;
|
|
5767
|
+
try {
|
|
5768
|
+
stream = await this.getUserMedia({ audio: constraints });
|
|
5769
|
+
} catch (error) {
|
|
5770
|
+
logger$17.error("[RTCPeerConnectionController] Failed to re-acquire mic for pipeline restore:", error);
|
|
5771
|
+
this._errors$.next(new MediaTrackError("restoreAudioPipelineInput", "audio", error));
|
|
5772
|
+
return;
|
|
5773
|
+
}
|
|
5774
|
+
const newTrack = stream.getAudioTracks().at(0);
|
|
5775
|
+
if (!newTrack) return;
|
|
5776
|
+
this.localStreamController.addTrack(newTrack);
|
|
5777
|
+
this._localAudioPipeline.setInputTrack(newTrack);
|
|
5746
5778
|
}
|
|
5747
5779
|
/**
|
|
5748
5780
|
* Return the lazily-created {@link LocalAudioPipeline}, constructing it on
|
|
@@ -5757,7 +5789,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5757
5789
|
try {
|
|
5758
5790
|
this._localAudioPipeline = new LocalAudioPipeline();
|
|
5759
5791
|
} catch (error) {
|
|
5760
|
-
logger$
|
|
5792
|
+
logger$17.warn("[RTCPeerConnectionController] Failed to create LocalAudioPipeline:", error);
|
|
5761
5793
|
return null;
|
|
5762
5794
|
}
|
|
5763
5795
|
this.subscribeTo(this.localStreamController.localAudioTracks$, () => {
|
|
@@ -5779,7 +5811,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5779
5811
|
try {
|
|
5780
5812
|
await sender.replaceTrack(this._localAudioPipeline.outputTrack);
|
|
5781
5813
|
} catch (error) {
|
|
5782
|
-
logger$
|
|
5814
|
+
logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
|
|
5783
5815
|
}
|
|
5784
5816
|
}
|
|
5785
5817
|
/**
|
|
@@ -5795,10 +5827,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5795
5827
|
try {
|
|
5796
5828
|
const localStream = this.localStreamController.addTrack(track);
|
|
5797
5829
|
this.peerConnection.addTrack(track, localStream);
|
|
5798
|
-
logger$
|
|
5830
|
+
logger$17.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
|
|
5799
5831
|
} catch (error) {
|
|
5800
|
-
logger$
|
|
5801
|
-
this._errors$.next(
|
|
5832
|
+
logger$17.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
|
|
5833
|
+
this._errors$.next(new MediaTrackError("addLocalTrack", track.kind, error));
|
|
5802
5834
|
throw error;
|
|
5803
5835
|
}
|
|
5804
5836
|
}
|
|
@@ -5814,16 +5846,16 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5814
5846
|
}
|
|
5815
5847
|
const sender = this.peerConnection.getSenders().find((sender$1) => sender$1.track?.id === trackId);
|
|
5816
5848
|
if (!sender) {
|
|
5817
|
-
logger$
|
|
5849
|
+
logger$17.debug(`[RTCPeerConnectionController] track not found: ${trackId}`);
|
|
5818
5850
|
return;
|
|
5819
5851
|
}
|
|
5820
5852
|
try {
|
|
5821
5853
|
this.peerConnection.removeTrack(sender);
|
|
5822
5854
|
this.localStreamController.removeTrack(trackId);
|
|
5823
|
-
logger$
|
|
5855
|
+
logger$17.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
|
|
5824
5856
|
} catch (error) {
|
|
5825
|
-
logger$
|
|
5826
|
-
this._errors$.next(
|
|
5857
|
+
logger$17.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
|
|
5858
|
+
this._errors$.next(new MediaTrackError("removeLocalTrack", sender.track?.kind ?? "unknown", error));
|
|
5827
5859
|
throw error;
|
|
5828
5860
|
}
|
|
5829
5861
|
}
|
|
@@ -5849,7 +5881,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5849
5881
|
async replaceAudioTrackWithConstraints(constraints) {
|
|
5850
5882
|
const senders = this.peerConnection?.getSenders().filter((s) => s.track?.kind === "audio" && s.track.readyState === "live");
|
|
5851
5883
|
if (!senders || senders.length === 0) {
|
|
5852
|
-
logger$
|
|
5884
|
+
logger$17.warn("[RTCPeerConnectionController] No live audio sender to replace");
|
|
5853
5885
|
return;
|
|
5854
5886
|
}
|
|
5855
5887
|
for (const sender of senders) {
|
|
@@ -5867,7 +5899,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5867
5899
|
const newTrack = (await this.getUserMedia({ audio: mergedConstraints })).getAudioTracks()[0];
|
|
5868
5900
|
await sender.replaceTrack(newTrack);
|
|
5869
5901
|
this.localStreamController.addTrack(newTrack);
|
|
5870
|
-
logger$
|
|
5902
|
+
logger$17.debug(`[RTCPeerConnectionController] Audio track replaced for server-pushed params. New track: ${newTrack.id}`);
|
|
5871
5903
|
}
|
|
5872
5904
|
}
|
|
5873
5905
|
/**
|
|
@@ -5875,7 +5907,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5875
5907
|
* Completes all observables to prevent memory leaks.
|
|
5876
5908
|
*/
|
|
5877
5909
|
destroy() {
|
|
5878
|
-
logger$
|
|
5910
|
+
logger$17.debug(`[RTCPeerConnectionController] Destroying RTCPeerConnectionController. ${this.propose}`);
|
|
5879
5911
|
this.removeConnectionTimer();
|
|
5880
5912
|
this._iceGatheringController?.destroy();
|
|
5881
5913
|
this._localAudioPipeline?.destroy();
|
|
@@ -5901,7 +5933,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5901
5933
|
}
|
|
5902
5934
|
stopRemoteTracks() {
|
|
5903
5935
|
this._remoteStream$.value?.getTracks().forEach((track) => {
|
|
5904
|
-
logger$
|
|
5936
|
+
logger$17.debug(`[RTCPeerConnectionController] Stopping remote track: ${track.kind}`);
|
|
5905
5937
|
track.stop();
|
|
5906
5938
|
});
|
|
5907
5939
|
}
|
|
@@ -5918,7 +5950,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5918
5950
|
...params,
|
|
5919
5951
|
sdp: finalRemote
|
|
5920
5952
|
};
|
|
5921
|
-
logger$
|
|
5953
|
+
logger$17.debug("[RTCPeerConnectionController] Setting remote description:", answer);
|
|
5922
5954
|
return this.peerConnection.setRemoteDescription(answer);
|
|
5923
5955
|
}
|
|
5924
5956
|
};
|
|
@@ -5933,7 +5965,11 @@ function isVertoInviteMessage(value) {
|
|
|
5933
5965
|
const msg = value;
|
|
5934
5966
|
return msg.method === "verto.invite" && isObject(msg.params) && hasProperty(msg.params, "sdp") && hasProperty(msg.params, "callID");
|
|
5935
5967
|
}
|
|
5936
|
-
|
|
5968
|
+
/**
|
|
5969
|
+
* Guards inbound `verto.bye` frames (server → client). Outbound bye messages are
|
|
5970
|
+
* built locally and never type-guarded, so only the inbound shape is asserted.
|
|
5971
|
+
*/
|
|
5972
|
+
function isVertoByeInboundMessage(value) {
|
|
5937
5973
|
if (!isVertoMethodMessage(value)) return false;
|
|
5938
5974
|
return value.method === "verto.bye";
|
|
5939
5975
|
}
|
|
@@ -5953,10 +5989,18 @@ function isVertoMediaParamsInnerParams(value) {
|
|
|
5953
5989
|
function isVertoPingInnerParams(value) {
|
|
5954
5990
|
return isObject(value) && hasProperty(value, "jsonrpc") && value.jsonrpc === "2.0" && hasProperty(value, "method") && value.method === "verto.ping";
|
|
5955
5991
|
}
|
|
5992
|
+
/**
|
|
5993
|
+
* Guards the params of an inbound `verto.bye` frame (top-level `callID`). Use
|
|
5994
|
+
* this to recognise the bye payload extracted by `vertoBye$`, e.g. when racing
|
|
5995
|
+
* it against a boolean answer/reject signal.
|
|
5996
|
+
*/
|
|
5997
|
+
function isVertoByeInboundParamsGuard(value) {
|
|
5998
|
+
return isObject(value) && hasProperty(value, "callID") && typeof value.callID === "string";
|
|
5999
|
+
}
|
|
5956
6000
|
|
|
5957
6001
|
//#endregion
|
|
5958
6002
|
//#region src/managers/VertoManager.ts
|
|
5959
|
-
const logger$
|
|
6003
|
+
const logger$16 = getLogger();
|
|
5960
6004
|
/**
|
|
5961
6005
|
* Decide what value goes on the `node_id` field of a `webrtc.verto` envelope.
|
|
5962
6006
|
*
|
|
@@ -6012,7 +6056,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6012
6056
|
try {
|
|
6013
6057
|
await this.executeVerto(vertoModifyMessage);
|
|
6014
6058
|
} catch (error) {
|
|
6015
|
-
logger$
|
|
6059
|
+
logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto hold:", error);
|
|
6016
6060
|
throw error;
|
|
6017
6061
|
}
|
|
6018
6062
|
}
|
|
@@ -6025,7 +6069,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6025
6069
|
try {
|
|
6026
6070
|
await this.executeVerto(vertoModifyMessage);
|
|
6027
6071
|
} catch (error) {
|
|
6028
|
-
logger$
|
|
6072
|
+
logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto unhold:", error);
|
|
6029
6073
|
throw error;
|
|
6030
6074
|
}
|
|
6031
6075
|
}
|
|
@@ -6078,25 +6122,25 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6078
6122
|
if (event.member_id) this.setSelfIdIfNull(event.member_id);
|
|
6079
6123
|
});
|
|
6080
6124
|
this.subscribeTo(this.vertoMedia$, (event) => {
|
|
6081
|
-
logger$
|
|
6082
|
-
this._signalingStatus$.next("ringing");
|
|
6125
|
+
logger$16.debug("[WebRTCManager] Received Verto media event (early media SDP):", event);
|
|
6083
6126
|
const { sdp, callID } = event;
|
|
6127
|
+
this.emitMainSignalingStatus(callID, "ringing");
|
|
6084
6128
|
this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
|
|
6085
6129
|
status: "received",
|
|
6086
6130
|
sdp
|
|
6087
6131
|
});
|
|
6088
6132
|
});
|
|
6089
6133
|
this.subscribeTo(this.vertoAnswer$, (event) => {
|
|
6090
|
-
logger$
|
|
6091
|
-
this._signalingStatus$.next("connecting");
|
|
6134
|
+
logger$16.debug("[WebRTCManager] Received Verto answer event:", event);
|
|
6092
6135
|
const { sdp, callID } = event;
|
|
6136
|
+
this.emitMainSignalingStatus(callID, "connecting");
|
|
6093
6137
|
this._rtcPeerConnectionsMap.get(callID)?.updateAnswerStatus({
|
|
6094
6138
|
status: "received",
|
|
6095
6139
|
sdp
|
|
6096
6140
|
});
|
|
6097
6141
|
});
|
|
6098
6142
|
this.subscribeTo(this.vertoMediaParams$, (event) => {
|
|
6099
|
-
logger$
|
|
6143
|
+
logger$16.debug("[WebRTCManager] Received Verto mediaParams event:", event);
|
|
6100
6144
|
const { mediaParams, callID } = event;
|
|
6101
6145
|
const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
|
|
6102
6146
|
const { audio, video } = mediaParams;
|
|
@@ -6110,7 +6154,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6110
6154
|
timestamp: Date.now()
|
|
6111
6155
|
});
|
|
6112
6156
|
} catch (error) {
|
|
6113
|
-
logger$
|
|
6157
|
+
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
|
|
6114
6158
|
this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
6115
6159
|
}
|
|
6116
6160
|
})();
|
|
@@ -6132,13 +6176,13 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6132
6176
|
*/
|
|
6133
6177
|
setNodeIdIfNull(nodeId) {
|
|
6134
6178
|
if (!this._nodeId$.value && nodeId) {
|
|
6135
|
-
logger$
|
|
6179
|
+
logger$16.debug(`[WebRTCManager] Early node_id set: ${nodeId}`);
|
|
6136
6180
|
this._nodeId$.next(nodeId);
|
|
6137
6181
|
}
|
|
6138
6182
|
}
|
|
6139
6183
|
setSelfIdIfNull(selfId) {
|
|
6140
6184
|
if (!this._selfId$.value && selfId) {
|
|
6141
|
-
logger$
|
|
6185
|
+
logger$16.debug(`[WebRTCManager] Early selfId set: ${selfId}`);
|
|
6142
6186
|
this._selfId$.next(selfId);
|
|
6143
6187
|
}
|
|
6144
6188
|
}
|
|
@@ -6147,7 +6191,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6147
6191
|
const vertoPongMessage = VertoPong({ ...vertoPing });
|
|
6148
6192
|
await this.executeVerto(vertoPongMessage);
|
|
6149
6193
|
} catch (error) {
|
|
6150
|
-
logger$
|
|
6194
|
+
logger$16.warn("[WebRTCManager] Call might disconnect, error sending Verto pong:", error);
|
|
6151
6195
|
this.onError?.(new VertoPongError(error));
|
|
6152
6196
|
}
|
|
6153
6197
|
}
|
|
@@ -6157,7 +6201,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6157
6201
|
if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
|
|
6158
6202
|
if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
|
|
6159
6203
|
} catch (error) {
|
|
6160
|
-
logger$
|
|
6204
|
+
logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
|
|
6161
6205
|
this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
6162
6206
|
throw error;
|
|
6163
6207
|
}
|
|
@@ -6187,20 +6231,20 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6187
6231
|
try {
|
|
6188
6232
|
const pc = this.mainPeerConnection.peerConnection;
|
|
6189
6233
|
if (!pc) {
|
|
6190
|
-
logger$
|
|
6234
|
+
logger$16.warn("[WebRTCManager] No peer connection for keyframe request");
|
|
6191
6235
|
return;
|
|
6192
6236
|
}
|
|
6193
6237
|
const videoReceiver = pc.getReceivers().find((r) => r.track.kind === "video");
|
|
6194
6238
|
if (!videoReceiver) {
|
|
6195
|
-
logger$
|
|
6239
|
+
logger$16.warn("[WebRTCManager] No video receiver for keyframe request");
|
|
6196
6240
|
return;
|
|
6197
6241
|
}
|
|
6198
6242
|
if (typeof videoReceiver.requestKeyFrame === "function") {
|
|
6199
6243
|
videoReceiver.requestKeyFrame();
|
|
6200
|
-
logger$
|
|
6201
|
-
} else logger$
|
|
6244
|
+
logger$16.debug("[WebRTCManager] Keyframe requested via RTCRtpReceiver.requestKeyFrame()");
|
|
6245
|
+
} else logger$16.debug("[WebRTCManager] requestKeyFrame() not supported, skipping");
|
|
6202
6246
|
} catch (error) {
|
|
6203
|
-
logger$
|
|
6247
|
+
logger$16.warn("[WebRTCManager] Keyframe request failed (non-fatal):", error);
|
|
6204
6248
|
}
|
|
6205
6249
|
}
|
|
6206
6250
|
/**
|
|
@@ -6218,13 +6262,13 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6218
6262
|
try {
|
|
6219
6263
|
const controller = this.mainPeerConnection;
|
|
6220
6264
|
if (!controller.peerConnection) {
|
|
6221
|
-
logger$
|
|
6265
|
+
logger$16.warn("[WebRTCManager] No peer connection for ICE restart");
|
|
6222
6266
|
return;
|
|
6223
6267
|
}
|
|
6224
6268
|
await controller.triggerIceRestart(relayOnly);
|
|
6225
|
-
logger$
|
|
6269
|
+
logger$16.info(`[WebRTCManager] ICE restart initiated${relayOnly ? " (relay-only)" : ""}`);
|
|
6226
6270
|
} catch (error) {
|
|
6227
|
-
logger$
|
|
6271
|
+
logger$16.error("[WebRTCManager] ICE restart failed:", error);
|
|
6228
6272
|
throw error;
|
|
6229
6273
|
}
|
|
6230
6274
|
}
|
|
@@ -6242,13 +6286,13 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6242
6286
|
const entries = Array.from(this._rtcPeerConnectionsMap.entries());
|
|
6243
6287
|
for (const [id, controller] of entries) try {
|
|
6244
6288
|
if (!controller.peerConnection) {
|
|
6245
|
-
logger$
|
|
6289
|
+
logger$16.debug(`[WebRTCManager] No peer connection for leg ${id}, skipping ICE restart`);
|
|
6246
6290
|
continue;
|
|
6247
6291
|
}
|
|
6248
6292
|
await controller.triggerIceRestart(relayOnly);
|
|
6249
|
-
logger$
|
|
6293
|
+
logger$16.info(`[WebRTCManager] ICE restart initiated for leg ${id}${relayOnly ? " (relay-only)" : ""}`);
|
|
6250
6294
|
} catch (error) {
|
|
6251
|
-
logger$
|
|
6295
|
+
logger$16.warn(`[WebRTCManager] ICE restart failed for leg ${id}:`, error);
|
|
6252
6296
|
}
|
|
6253
6297
|
}
|
|
6254
6298
|
/**
|
|
@@ -6260,7 +6304,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6260
6304
|
requestKeyframeAll() {
|
|
6261
6305
|
for (const [id, controller] of this._rtcPeerConnectionsMap) {
|
|
6262
6306
|
if (controller.isScreenShare) {
|
|
6263
|
-
logger$
|
|
6307
|
+
logger$16.debug(`[WebRTCManager] Skipping keyframe for send-only screen share leg ${id}`);
|
|
6264
6308
|
continue;
|
|
6265
6309
|
}
|
|
6266
6310
|
try {
|
|
@@ -6270,10 +6314,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6270
6314
|
if (!videoReceiver) continue;
|
|
6271
6315
|
if (typeof videoReceiver.requestKeyFrame === "function") {
|
|
6272
6316
|
videoReceiver.requestKeyFrame();
|
|
6273
|
-
logger$
|
|
6317
|
+
logger$16.debug(`[WebRTCManager] Keyframe requested for leg ${id}`);
|
|
6274
6318
|
}
|
|
6275
6319
|
} catch (error) {
|
|
6276
|
-
logger$
|
|
6320
|
+
logger$16.warn(`[WebRTCManager] Keyframe request failed for leg ${id} (non-fatal):`, error);
|
|
6277
6321
|
}
|
|
6278
6322
|
}
|
|
6279
6323
|
}
|
|
@@ -6290,7 +6334,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6290
6334
|
return this.cachedObservable("vertoMediaParams$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoMediaParamsInnerParams, "params"), takeUntil(this.destroyed$)));
|
|
6291
6335
|
}
|
|
6292
6336
|
get vertoBye$() {
|
|
6293
|
-
return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(
|
|
6337
|
+
return this.cachedObservable("vertoBye$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoByeInboundMessage, "params"), takeUntil(this.destroyed$)));
|
|
6294
6338
|
}
|
|
6295
6339
|
get vertoAttach$() {
|
|
6296
6340
|
return this.cachedObservable("vertoAttach$", () => this.webRtcCallSession.webrtcMessages$.pipe(filterAs(isVertoAttachMessage, "params"), takeUntil(this.destroyed$)));
|
|
@@ -6334,7 +6378,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6334
6378
|
default:
|
|
6335
6379
|
}
|
|
6336
6380
|
} catch (error) {
|
|
6337
|
-
logger$
|
|
6381
|
+
logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
|
|
6338
6382
|
this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
6339
6383
|
if (vertoMethod === "verto.modify") this.onModifyFailed?.();
|
|
6340
6384
|
}
|
|
@@ -6349,19 +6393,33 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6349
6393
|
sdp
|
|
6350
6394
|
});
|
|
6351
6395
|
} catch (error) {
|
|
6352
|
-
logger$
|
|
6396
|
+
logger$16.warn("[WebRTCManager] Error processing modify response:", error);
|
|
6353
6397
|
const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
6354
6398
|
this.onError?.(modifyError);
|
|
6355
6399
|
}
|
|
6356
6400
|
}
|
|
6357
6401
|
}
|
|
6402
|
+
emitMainSignalingStatus(callId, status) {
|
|
6403
|
+
const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callId);
|
|
6404
|
+
if (!rtcPeerConnController) {
|
|
6405
|
+
const signalingError = new DependencyError(`Cannot emit signaling status, RTCPeerConnectionController not found for callID: ${callId}`);
|
|
6406
|
+
logger$16.error("[WebRTCManager] Failed to emit signaling status:", {
|
|
6407
|
+
callId,
|
|
6408
|
+
status,
|
|
6409
|
+
signalingError
|
|
6410
|
+
});
|
|
6411
|
+
this.onError?.(signalingError);
|
|
6412
|
+
return;
|
|
6413
|
+
}
|
|
6414
|
+
if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
|
|
6415
|
+
}
|
|
6358
6416
|
processInviteResponse(response, rtcPeerConnController) {
|
|
6359
6417
|
if (!response.error && getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
|
|
6360
|
-
this.
|
|
6418
|
+
this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
|
|
6361
6419
|
this._nodeId$.next(getValueFrom(response, "result.node_id") ?? null);
|
|
6362
6420
|
const memberId = getValueFrom(response, "result.result.result.memberID") ?? null;
|
|
6363
6421
|
const callId = getValueFrom(response, "result.result.result.callID") ?? null;
|
|
6364
|
-
logger$
|
|
6422
|
+
logger$16.debug("[WebRTCManager] Verto invite response:", {
|
|
6365
6423
|
callId,
|
|
6366
6424
|
memberId,
|
|
6367
6425
|
response
|
|
@@ -6371,14 +6429,14 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6371
6429
|
if (callId) {
|
|
6372
6430
|
this.webRtcCallSession.addCallId(callId);
|
|
6373
6431
|
this.attachManager.attach(this.buildAttachableCall(callId));
|
|
6374
|
-
} else logger$
|
|
6432
|
+
} else logger$16.warn("[WebRTCManager] Cannot attach call, missing callId:", {
|
|
6375
6433
|
nodeId: this.nodeId,
|
|
6376
6434
|
callId
|
|
6377
6435
|
});
|
|
6378
|
-
logger$
|
|
6379
|
-
logger$
|
|
6436
|
+
logger$16.info("[WebRTCManager] Verto invite successful");
|
|
6437
|
+
logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
|
|
6380
6438
|
} else {
|
|
6381
|
-
logger$
|
|
6439
|
+
logger$16.error("[WebRTCManager] Verto invite failed:", response);
|
|
6382
6440
|
const inviteError = response.error ? new JSONRPCError(response.error.code, response.error.message, response.error.data) : /* @__PURE__ */ new Error("Verto invite failed: unexpected response");
|
|
6383
6441
|
this.onError?.(inviteError);
|
|
6384
6442
|
}
|
|
@@ -6405,6 +6463,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6405
6463
|
inputVideoStream: options.inputVideoStream,
|
|
6406
6464
|
receiveAudio: options.receiveAudio,
|
|
6407
6465
|
receiveVideo: options.receiveVideo,
|
|
6466
|
+
fallbackToReceiveOnly: options.fallbackToReceiveOnly,
|
|
6408
6467
|
webRTCApiProvider: this.webRTCApiProvider,
|
|
6409
6468
|
preferredVideoCodecs: options.preferredVideoCodecs,
|
|
6410
6469
|
preferredAudioCodecs: options.preferredAudioCodecs,
|
|
@@ -6423,17 +6482,17 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6423
6482
|
if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
|
|
6424
6483
|
}
|
|
6425
6484
|
async handleInboundAnswer(rtcPeerConnController) {
|
|
6426
|
-
logger$
|
|
6485
|
+
logger$16.debug("[WebRTCManager] Waiting for inbound call to be accepted or rejected");
|
|
6427
6486
|
const vertoByeOrAccepted = await firstValueFrom(race(this.vertoBye$, this.webRtcCallSession.answered$).pipe(takeUntil(this.destroyed$))).catch(() => null);
|
|
6428
6487
|
if (vertoByeOrAccepted === null) {
|
|
6429
|
-
logger$
|
|
6488
|
+
logger$16.debug("[WebRTCManager] Inbound answer handler aborted (destroyed).");
|
|
6430
6489
|
return;
|
|
6431
6490
|
}
|
|
6432
|
-
if (
|
|
6433
|
-
logger$
|
|
6491
|
+
if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
|
|
6492
|
+
logger$16.info("[WebRTCManager] Inbound call ended by remote before answer.");
|
|
6434
6493
|
this.callSession?.destroy();
|
|
6435
6494
|
} else if (!vertoByeOrAccepted) {
|
|
6436
|
-
logger$
|
|
6495
|
+
logger$16.info("[WebRTCManager] Inbound call rejected by user.");
|
|
6437
6496
|
try {
|
|
6438
6497
|
await this.bye("USER_BUSY");
|
|
6439
6498
|
} finally {
|
|
@@ -6441,19 +6500,19 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6441
6500
|
this.callSession?.destroy();
|
|
6442
6501
|
}
|
|
6443
6502
|
} else {
|
|
6444
|
-
logger$
|
|
6503
|
+
logger$16.debug("[WebRTCManager] Inbound call accepted, creating SDP answer");
|
|
6445
6504
|
const answerOptions = this.webRtcCallSession.answerMediaOptions;
|
|
6446
6505
|
try {
|
|
6447
6506
|
await rtcPeerConnController.acceptInbound(answerOptions);
|
|
6448
6507
|
} catch (error) {
|
|
6449
|
-
logger$
|
|
6508
|
+
logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
|
|
6450
6509
|
this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
6451
6510
|
}
|
|
6452
6511
|
}
|
|
6453
6512
|
}
|
|
6454
6513
|
setupVertoAttachHandler() {
|
|
6455
6514
|
this.subscribeTo(this.vertoAttach$, async (vertoAttach) => {
|
|
6456
|
-
logger$
|
|
6515
|
+
logger$16.debug("[WebRTCManager] Received Verto attach event for existing call:", vertoAttach);
|
|
6457
6516
|
const { callID } = vertoAttach;
|
|
6458
6517
|
await this.attachManager.attach({
|
|
6459
6518
|
nodeId: this.nodeId ?? void 0,
|
|
@@ -6525,17 +6584,17 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6525
6584
|
};
|
|
6526
6585
|
}
|
|
6527
6586
|
async sendLocalDescriptionOnceAccepted(vertoMessageRequest, rtcPeerConnectionController) {
|
|
6528
|
-
logger$
|
|
6587
|
+
logger$16.debug("[WebRTCManager] Waiting for call to be accepted or ended before sending answer");
|
|
6529
6588
|
const vertoByeOrAccepted = await firstValueFrom(race(this.vertoBye$, this.webRtcCallSession.answered$).pipe(takeUntil(this.destroyed$))).catch(() => null);
|
|
6530
6589
|
if (vertoByeOrAccepted === null) {
|
|
6531
|
-
logger$
|
|
6590
|
+
logger$16.debug("[WebRTCManager] Destroyed while waiting for call acceptance");
|
|
6532
6591
|
return;
|
|
6533
6592
|
}
|
|
6534
|
-
if (
|
|
6535
|
-
logger$
|
|
6593
|
+
if (isVertoByeInboundParamsGuard(vertoByeOrAccepted)) {
|
|
6594
|
+
logger$16.info("[WebRTCManager] Call ended before answer was sent.");
|
|
6536
6595
|
this.callSession?.destroy();
|
|
6537
6596
|
} else if (!vertoByeOrAccepted) {
|
|
6538
|
-
logger$
|
|
6597
|
+
logger$16.info("[WebRTCManager] Call was not accepted, sending verto.bye.");
|
|
6539
6598
|
try {
|
|
6540
6599
|
await this.bye("USER_BUSY");
|
|
6541
6600
|
} finally {
|
|
@@ -6543,14 +6602,14 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6543
6602
|
this.callSession?.destroy();
|
|
6544
6603
|
}
|
|
6545
6604
|
} else {
|
|
6546
|
-
logger$
|
|
6605
|
+
logger$16.debug("[WebRTCManager] Call accepted, sending answer");
|
|
6547
6606
|
try {
|
|
6548
|
-
this.
|
|
6607
|
+
this.emitMainSignalingStatus(rtcPeerConnectionController.id, "connecting");
|
|
6549
6608
|
await this.sendLocalDescription(vertoMessageRequest, rtcPeerConnectionController);
|
|
6550
6609
|
await rtcPeerConnectionController.updateAnswerStatus({ status: "sent" });
|
|
6551
6610
|
await this.attachManager.attach(this.buildAttachableCall());
|
|
6552
6611
|
} catch (error) {
|
|
6553
|
-
logger$
|
|
6612
|
+
logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
|
|
6554
6613
|
this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
6555
6614
|
await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
|
|
6556
6615
|
}
|
|
@@ -6631,9 +6690,11 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6631
6690
|
await this.initAdditionalPeerConnection("screenshare", options);
|
|
6632
6691
|
}
|
|
6633
6692
|
async initAdditionalPeerConnection(propose, options) {
|
|
6693
|
+
const isScreenShare = propose === "screenshare";
|
|
6694
|
+
let firstPeerConnectionError;
|
|
6634
6695
|
let rtcPeerConnController = null;
|
|
6635
6696
|
try {
|
|
6636
|
-
this._screenShareStatus$.next("starting");
|
|
6697
|
+
if (isScreenShare) this._screenShareStatus$.next("starting");
|
|
6637
6698
|
rtcPeerConnController = new RTCPeerConnectionController({
|
|
6638
6699
|
...options,
|
|
6639
6700
|
...this.RTCPeerConnectionConfig,
|
|
@@ -6641,21 +6702,27 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6641
6702
|
webRTCApiProvider: this.webRTCApiProvider
|
|
6642
6703
|
}, void 0, this.deviceController);
|
|
6643
6704
|
this.setupLocalDescriptionHandler(rtcPeerConnController);
|
|
6644
|
-
if (
|
|
6705
|
+
if (isScreenShare) this._screenShareId = rtcPeerConnController.id;
|
|
6645
6706
|
this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
|
|
6646
6707
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
6647
6708
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
6648
|
-
|
|
6709
|
+
firstPeerConnectionError ??= error;
|
|
6710
|
+
this.onError?.(error, { fatal: false });
|
|
6649
6711
|
});
|
|
6650
6712
|
await firstValueFrom(rtcPeerConnController.connectionState$.pipe(filter((state) => state === "connected"), take(1), timeout(this._screenShareTimeoutMs), takeUntil(this.destroyed$)));
|
|
6651
|
-
this._screenShareStatus$.next("started");
|
|
6652
|
-
logger$
|
|
6713
|
+
if (isScreenShare) this._screenShareStatus$.next("started");
|
|
6714
|
+
logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
|
|
6653
6715
|
return rtcPeerConnController.id;
|
|
6654
6716
|
} catch (error) {
|
|
6655
|
-
logger$
|
|
6656
|
-
this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
6717
|
+
logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
|
|
6657
6718
|
if (rtcPeerConnController) rtcPeerConnController.destroy();
|
|
6658
|
-
this._screenShareStatus$.next("none");
|
|
6719
|
+
if (isScreenShare) this._screenShareStatus$.next("none");
|
|
6720
|
+
if (firstPeerConnectionError) throw firstPeerConnectionError instanceof MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
|
|
6721
|
+
if (error instanceof EmptyError) {
|
|
6722
|
+
logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
|
|
6723
|
+
return;
|
|
6724
|
+
}
|
|
6725
|
+
throw error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
6659
6726
|
}
|
|
6660
6727
|
}
|
|
6661
6728
|
async removeInputDevices(id) {
|
|
@@ -6671,9 +6738,9 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6671
6738
|
if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
|
|
6672
6739
|
}
|
|
6673
6740
|
async removeScreenMedia() {
|
|
6674
|
-
if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$
|
|
6741
|
+
if (!["starting", "started"].includes(this._screenShareStatus$.value)) logger$16.warn("[WebRTCManager] No active screen share to stop.");
|
|
6675
6742
|
if (!this._screenShareId) {
|
|
6676
|
-
logger$
|
|
6743
|
+
logger$16.debug("[WebRTCManager] No screen share peer connection found.");
|
|
6677
6744
|
return;
|
|
6678
6745
|
}
|
|
6679
6746
|
this._screenShareStatus$.next("stopping");
|
|
@@ -6702,7 +6769,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6702
6769
|
dialogParams: this.dialogParams(rtcPeerConnController)
|
|
6703
6770
|
}));
|
|
6704
6771
|
} catch (error) {
|
|
6705
|
-
logger$
|
|
6772
|
+
logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
|
|
6706
6773
|
throw error;
|
|
6707
6774
|
}
|
|
6708
6775
|
}
|
|
@@ -6720,7 +6787,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6720
6787
|
try {
|
|
6721
6788
|
await this.executeVerto(vertoInfoMessage);
|
|
6722
6789
|
} catch (error) {
|
|
6723
|
-
logger$
|
|
6790
|
+
logger$16.warn("[WebRTCManager] Error sending DTMF digits:", error);
|
|
6724
6791
|
throw error;
|
|
6725
6792
|
}
|
|
6726
6793
|
}
|
|
@@ -6731,10 +6798,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6731
6798
|
action: "transfer"
|
|
6732
6799
|
});
|
|
6733
6800
|
try {
|
|
6734
|
-
logger$
|
|
6801
|
+
logger$16.debug("[WebRTCManager] Transferring call with options:", options);
|
|
6735
6802
|
await this.executeVerto(message);
|
|
6736
6803
|
} catch (error) {
|
|
6737
|
-
logger$
|
|
6804
|
+
logger$16.error("[WebRTCManager] Error transferring call:", error);
|
|
6738
6805
|
throw error;
|
|
6739
6806
|
}
|
|
6740
6807
|
}
|
|
@@ -6750,7 +6817,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6750
6817
|
|
|
6751
6818
|
//#endregion
|
|
6752
6819
|
//#region src/controllers/RemoteAudioMeter.ts
|
|
6753
|
-
const logger$
|
|
6820
|
+
const logger$15 = getLogger();
|
|
6754
6821
|
/**
|
|
6755
6822
|
* Read-only audio level meter for a remote MediaStream. Attaches an
|
|
6756
6823
|
* AnalyserNode to a MediaStreamAudioSourceNode so it observes the stream
|
|
@@ -6785,7 +6852,7 @@ var RemoteAudioMeter = class extends Destroyable {
|
|
|
6785
6852
|
try {
|
|
6786
6853
|
this._source.disconnect();
|
|
6787
6854
|
} catch (error) {
|
|
6788
|
-
logger$
|
|
6855
|
+
logger$15.debug("[RemoteAudioMeter] source disconnect warning:", error);
|
|
6789
6856
|
}
|
|
6790
6857
|
this._source = null;
|
|
6791
6858
|
this._stream = null;
|
|
@@ -6802,7 +6869,7 @@ var RemoteAudioMeter = class extends Destroyable {
|
|
|
6802
6869
|
this._source = null;
|
|
6803
6870
|
}
|
|
6804
6871
|
this._audioContext.close().catch((error) => {
|
|
6805
|
-
logger$
|
|
6872
|
+
logger$15.debug("[RemoteAudioMeter] audio context close warning:", error);
|
|
6806
6873
|
});
|
|
6807
6874
|
super.destroy();
|
|
6808
6875
|
}
|
|
@@ -6826,7 +6893,7 @@ var RemoteAudioMeter = class extends Destroyable {
|
|
|
6826
6893
|
*
|
|
6827
6894
|
* Extends Destroyable for automatic RxJS cleanup.
|
|
6828
6895
|
*/
|
|
6829
|
-
const logger$
|
|
6896
|
+
const logger$14 = getLogger();
|
|
6830
6897
|
const DEFAULT_POLLING_INTERVAL_MS = 1e3;
|
|
6831
6898
|
const DEFAULT_BASELINE_SAMPLES = 10;
|
|
6832
6899
|
const DEFAULT_NO_AUDIO_PACKET_THRESHOLD_MS = 2e3;
|
|
@@ -6916,9 +6983,9 @@ var RTCStatsMonitor = class extends Destroyable {
|
|
|
6916
6983
|
const now = Date.now();
|
|
6917
6984
|
this.lastAudioPacketChangeTime = now;
|
|
6918
6985
|
this.lastVideoPacketChangeTime = now;
|
|
6919
|
-
logger$
|
|
6986
|
+
logger$14.debug("[RTCStatsMonitor] Starting stats monitoring");
|
|
6920
6987
|
this.subscribeTo(interval(this.pollingIntervalMs).pipe(filter(() => this.running), switchMap(() => from(this.peerConnection.getStats()).pipe(catchError((err) => {
|
|
6921
|
-
logger$
|
|
6988
|
+
logger$14.warn("[RTCStatsMonitor] Failed to get stats:", err);
|
|
6922
6989
|
return EMPTY;
|
|
6923
6990
|
}))), filter(() => this.running), map((report) => this.extractSample(report))), (sample) => this._sample$.next(sample));
|
|
6924
6991
|
this.subscribeTo(this._sample$.pipe(take(this.baselineSampleCount), toArray(), map((samples) => ({
|
|
@@ -6926,7 +6993,7 @@ var RTCStatsMonitor = class extends Destroyable {
|
|
|
6926
6993
|
jitter: samples.reduce((s, b) => s + b.audioJitter, 0) / samples.length,
|
|
6927
6994
|
ready: true
|
|
6928
6995
|
}))), (baseline) => {
|
|
6929
|
-
logger$
|
|
6996
|
+
logger$14.debug(`[RTCStatsMonitor] Baseline established: rtt=${baseline.rtt.toFixed(1)}ms, jitter=${baseline.jitter.toFixed(1)}ms`);
|
|
6930
6997
|
this._baseline$.next(baseline);
|
|
6931
6998
|
});
|
|
6932
6999
|
this.subscribeTo(this._sample$.pipe(scan((acc, sample) => ({
|
|
@@ -6963,10 +7030,10 @@ var RTCStatsMonitor = class extends Destroyable {
|
|
|
6963
7030
|
stop() {
|
|
6964
7031
|
if (!this.running) return;
|
|
6965
7032
|
this.running = false;
|
|
6966
|
-
logger$
|
|
7033
|
+
logger$14.debug("[RTCStatsMonitor] Stopping stats monitoring");
|
|
6967
7034
|
}
|
|
6968
7035
|
destroy() {
|
|
6969
|
-
logger$
|
|
7036
|
+
logger$14.debug("[RTCStatsMonitor] Destroying RTCStatsMonitor");
|
|
6970
7037
|
this.stop();
|
|
6971
7038
|
super.destroy();
|
|
6972
7039
|
}
|
|
@@ -7094,7 +7161,7 @@ var RTCStatsMonitor = class extends Destroyable {
|
|
|
7094
7161
|
|
|
7095
7162
|
//#endregion
|
|
7096
7163
|
//#region src/managers/CallRecoveryManager.ts
|
|
7097
|
-
const logger$
|
|
7164
|
+
const logger$13 = getLogger();
|
|
7098
7165
|
const DEFAULT_DEBOUNCE_TIME_MS = 2e3;
|
|
7099
7166
|
const DEFAULT_COOLDOWN_MS = 1e4;
|
|
7100
7167
|
const DEFAULT_ICE_GRACE_PERIOD_MS = 3e3;
|
|
@@ -7191,10 +7258,10 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7191
7258
|
*/
|
|
7192
7259
|
async requestIceRestart() {
|
|
7193
7260
|
if (this._recoveryState$.value === "recovering") {
|
|
7194
|
-
logger$
|
|
7261
|
+
logger$13.info("CallRecoveryManager: manual ICE restart skipped — recovery already in progress");
|
|
7195
7262
|
return;
|
|
7196
7263
|
}
|
|
7197
|
-
logger$
|
|
7264
|
+
logger$13.info("CallRecoveryManager: manual ICE restart requested");
|
|
7198
7265
|
this.transitionTo("recovering");
|
|
7199
7266
|
await this.executeIceRestart(false);
|
|
7200
7267
|
this.startCooldown();
|
|
@@ -7210,7 +7277,7 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7210
7277
|
* WebSocket reconnect or call state recovers to 'connected'.
|
|
7211
7278
|
*/
|
|
7212
7279
|
reset() {
|
|
7213
|
-
logger$
|
|
7280
|
+
logger$13.info("CallRecoveryManager: resetting counters");
|
|
7214
7281
|
this._attemptCount = 0;
|
|
7215
7282
|
this._keyframeBurstCount = 0;
|
|
7216
7283
|
this._keyframeBurstStart = 0;
|
|
@@ -7225,7 +7292,7 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7225
7292
|
*/
|
|
7226
7293
|
notifyModifyFailed() {
|
|
7227
7294
|
if (this._recoveryState$.value === "cooldown" || this._recoveryState$.value === "idle") {
|
|
7228
|
-
logger$
|
|
7295
|
+
logger$13.info("CallRecoveryManager: verto.modify failed — re-entering recovery");
|
|
7229
7296
|
this._cooldownUntil = 0;
|
|
7230
7297
|
this.transitionTo("idle");
|
|
7231
7298
|
this.pushTrigger({
|
|
@@ -7249,7 +7316,7 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7249
7316
|
reason: `bandwidth ${bitrateKbps}kbps below threshold ${this._config.degradationBitrateThreshold}kbps`,
|
|
7250
7317
|
timestamp: Date.now()
|
|
7251
7318
|
});
|
|
7252
|
-
logger$
|
|
7319
|
+
logger$13.warn(`CallRecoveryManager: disabling video — bandwidth ${bitrateKbps}kbps < ${this._config.degradationBitrateThreshold}kbps`);
|
|
7253
7320
|
} else if (wasConstrained && bitrateKbps >= this._config.degradationRecoveryThreshold) {
|
|
7254
7321
|
this._bandwidthConstrained$.next(false);
|
|
7255
7322
|
this._callbacks.enableVideo();
|
|
@@ -7258,7 +7325,7 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7258
7325
|
reason: `bandwidth ${bitrateKbps}kbps recovered above ${this._config.degradationRecoveryThreshold}kbps`,
|
|
7259
7326
|
timestamp: Date.now()
|
|
7260
7327
|
});
|
|
7261
|
-
logger$
|
|
7328
|
+
logger$13.info(`CallRecoveryManager: restoring video — bandwidth ${bitrateKbps}kbps >= ${this._config.degradationRecoveryThreshold}kbps`);
|
|
7262
7329
|
}
|
|
7263
7330
|
}
|
|
7264
7331
|
/**
|
|
@@ -7277,14 +7344,14 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7277
7344
|
handleWebSocketReconnect() {
|
|
7278
7345
|
const pcState = this._callbacks.getPeerConnectionState();
|
|
7279
7346
|
if (pcState === "connected" || pcState === "completed") {
|
|
7280
|
-
logger$
|
|
7347
|
+
logger$13.info("CallRecoveryManager: signal-only reconnect — peer connection still alive");
|
|
7281
7348
|
this.emitEvent({
|
|
7282
7349
|
action: "signal_reconnect",
|
|
7283
7350
|
reason: "WebSocket reconnected, peer connection still connected",
|
|
7284
7351
|
timestamp: Date.now()
|
|
7285
7352
|
});
|
|
7286
7353
|
} else {
|
|
7287
|
-
logger$
|
|
7354
|
+
logger$13.info("CallRecoveryManager: full reconnect — peer connection also down");
|
|
7288
7355
|
this.emitEvent({
|
|
7289
7356
|
action: "full_reconnect",
|
|
7290
7357
|
reason: "WebSocket reconnected, peer connection not connected — ICE restart needed",
|
|
@@ -7308,7 +7375,7 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7308
7375
|
}), debounceTime(this._config.debounceTimeMs), withLatestFrom(this._inputs.signalingReady$), filter(([, signalingReady]) => this.passGateChecks(signalingReady)), map(([trigger]) => trigger), exhaustMap((trigger) => this.executeTieredRecovery(trigger)), takeUntil(merge(this._destroyed$, this._pipelineStop$))), {
|
|
7309
7376
|
next: () => {},
|
|
7310
7377
|
error: (err) => {
|
|
7311
|
-
logger$
|
|
7378
|
+
logger$13.error("CallRecoveryManager: pipeline error", err);
|
|
7312
7379
|
this.transitionTo("idle");
|
|
7313
7380
|
}
|
|
7314
7381
|
});
|
|
@@ -7335,27 +7402,27 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7335
7402
|
reason: `no packet loss for ${this._config.packetLossRecoveryDelaySec}s — restoring video`,
|
|
7336
7403
|
timestamp: Date.now()
|
|
7337
7404
|
});
|
|
7338
|
-
logger$
|
|
7405
|
+
logger$13.info(`CallRecoveryManager: restoring video — no packet loss for ${this._config.packetLossRecoveryDelaySec}s`);
|
|
7339
7406
|
});
|
|
7340
7407
|
}
|
|
7341
7408
|
passGateChecks(signalingReady) {
|
|
7342
7409
|
if (this._callbacks.isNegotiating()) {
|
|
7343
|
-
logger$
|
|
7410
|
+
logger$13.debug("CallRecoveryManager: gate blocked — negotiation in progress");
|
|
7344
7411
|
this.transitionTo("idle");
|
|
7345
7412
|
return false;
|
|
7346
7413
|
}
|
|
7347
7414
|
if (!signalingReady) {
|
|
7348
|
-
logger$
|
|
7415
|
+
logger$13.debug("CallRecoveryManager: gate blocked — signaling not ready");
|
|
7349
7416
|
this.transitionTo("idle");
|
|
7350
7417
|
return false;
|
|
7351
7418
|
}
|
|
7352
7419
|
if (!this._callbacks.isCallConnected()) {
|
|
7353
|
-
logger$
|
|
7420
|
+
logger$13.debug("CallRecoveryManager: gate blocked — call not connected");
|
|
7354
7421
|
this.transitionTo("idle");
|
|
7355
7422
|
return false;
|
|
7356
7423
|
}
|
|
7357
7424
|
if (this.isCooldownActive()) {
|
|
7358
|
-
logger$
|
|
7425
|
+
logger$13.debug("CallRecoveryManager: gate blocked — cooldown active");
|
|
7359
7426
|
this.transitionTo("cooldown");
|
|
7360
7427
|
return false;
|
|
7361
7428
|
}
|
|
@@ -7366,9 +7433,9 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7366
7433
|
}
|
|
7367
7434
|
executeTieredRecovery(trigger) {
|
|
7368
7435
|
this.transitionTo("recovering");
|
|
7369
|
-
logger$
|
|
7436
|
+
logger$13.info(`CallRecoveryManager: starting tiered recovery — source=${trigger.source} detail=${trigger.detail}`);
|
|
7370
7437
|
return from(this.runTiers(trigger)).pipe(tap(() => this.startCooldown()), catchError((err) => {
|
|
7371
|
-
logger$
|
|
7438
|
+
logger$13.error("CallRecoveryManager: tiered recovery failed", err);
|
|
7372
7439
|
this.startCooldown();
|
|
7373
7440
|
return EMPTY;
|
|
7374
7441
|
}));
|
|
@@ -7376,7 +7443,7 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7376
7443
|
async runTiers(trigger) {
|
|
7377
7444
|
this.executeKeyframe(trigger.detail);
|
|
7378
7445
|
if (trigger.issueType && DEGRADATION_ONLY_ISSUES.has(trigger.issueType)) {
|
|
7379
|
-
logger$
|
|
7446
|
+
logger$13.debug(`CallRecoveryManager: degradation-only issue (${trigger.issueType}) — Tier 1 only, skipping ICE restart`);
|
|
7380
7447
|
return;
|
|
7381
7448
|
}
|
|
7382
7449
|
if (this._attemptCount < this._config.maxAttempts) {
|
|
@@ -7393,13 +7460,13 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7393
7460
|
maxAttempts: this._config.maxAttempts,
|
|
7394
7461
|
timestamp: Date.now()
|
|
7395
7462
|
});
|
|
7396
|
-
logger$
|
|
7463
|
+
logger$13.warn("CallRecoveryManager: max recovery attempts reached");
|
|
7397
7464
|
}
|
|
7398
7465
|
}
|
|
7399
7466
|
executeKeyframe(reason) {
|
|
7400
7467
|
const now = Date.now();
|
|
7401
7468
|
if (now < this._keyframeCooldownUntil) {
|
|
7402
|
-
logger$
|
|
7469
|
+
logger$13.debug("CallRecoveryManager: keyframe request skipped — cooldown active");
|
|
7403
7470
|
return;
|
|
7404
7471
|
}
|
|
7405
7472
|
if (now - this._keyframeBurstStart > this._config.keyframeBurstWindowMs) {
|
|
@@ -7408,7 +7475,7 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7408
7475
|
}
|
|
7409
7476
|
if (this._keyframeBurstCount >= this._config.keyframeMaxBurst) {
|
|
7410
7477
|
this._keyframeCooldownUntil = now + this._config.keyframeCooldownMs;
|
|
7411
|
-
logger$
|
|
7478
|
+
logger$13.debug(`CallRecoveryManager: keyframe burst limit reached (${this._config.keyframeMaxBurst}), cooldown until ${this._keyframeCooldownUntil}`);
|
|
7412
7479
|
return;
|
|
7413
7480
|
}
|
|
7414
7481
|
this._keyframeBurstCount += 1;
|
|
@@ -7418,12 +7485,12 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7418
7485
|
reason,
|
|
7419
7486
|
timestamp: now
|
|
7420
7487
|
});
|
|
7421
|
-
logger$
|
|
7488
|
+
logger$13.debug(`CallRecoveryManager: keyframe requested (burst ${this._keyframeBurstCount}/${this._config.keyframeMaxBurst})`);
|
|
7422
7489
|
}
|
|
7423
7490
|
async executeIceRestart(relayOnly) {
|
|
7424
7491
|
this._attemptCount += 1;
|
|
7425
7492
|
const tier = relayOnly ? "Tier 3 (relay-only)" : "Tier 2 (standard)";
|
|
7426
|
-
logger$
|
|
7493
|
+
logger$13.info(`CallRecoveryManager: ${tier} ICE restart — attempt ${this._attemptCount}/${this._config.maxAttempts}`);
|
|
7427
7494
|
this.emitEvent({
|
|
7428
7495
|
action: "reinvite_started",
|
|
7429
7496
|
reason: `${tier} ICE restart`,
|
|
@@ -7440,7 +7507,7 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7440
7507
|
maxAttempts: this._config.maxAttempts,
|
|
7441
7508
|
timestamp: Date.now()
|
|
7442
7509
|
});
|
|
7443
|
-
logger$
|
|
7510
|
+
logger$13.info(`CallRecoveryManager: ${tier} ICE restart succeeded`);
|
|
7444
7511
|
this._attemptCount = 0;
|
|
7445
7512
|
return true;
|
|
7446
7513
|
}
|
|
@@ -7451,7 +7518,7 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7451
7518
|
maxAttempts: this._config.maxAttempts,
|
|
7452
7519
|
timestamp: Date.now()
|
|
7453
7520
|
});
|
|
7454
|
-
logger$
|
|
7521
|
+
logger$13.warn(`CallRecoveryManager: ${tier} ICE restart failed`);
|
|
7455
7522
|
return false;
|
|
7456
7523
|
} catch {
|
|
7457
7524
|
this.emitEvent({
|
|
@@ -7461,7 +7528,7 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7461
7528
|
maxAttempts: this._config.maxAttempts,
|
|
7462
7529
|
timestamp: Date.now()
|
|
7463
7530
|
});
|
|
7464
|
-
logger$
|
|
7531
|
+
logger$13.warn(`CallRecoveryManager: ${tier} ICE restart timed out`);
|
|
7465
7532
|
return false;
|
|
7466
7533
|
}
|
|
7467
7534
|
}
|
|
@@ -7482,7 +7549,7 @@ var CallRecoveryManager = class extends Destroyable {
|
|
|
7482
7549
|
transitionTo(state) {
|
|
7483
7550
|
const prev = this._recoveryState$.value;
|
|
7484
7551
|
if (prev !== state) {
|
|
7485
|
-
logger$
|
|
7552
|
+
logger$13.debug(`CallRecoveryManager: state ${prev} -> ${state}`);
|
|
7486
7553
|
this._recoveryState$.next(state);
|
|
7487
7554
|
}
|
|
7488
7555
|
}
|
|
@@ -7584,7 +7651,7 @@ function mosToQualityLevel(mos) {
|
|
|
7584
7651
|
|
|
7585
7652
|
//#endregion
|
|
7586
7653
|
//#region src/core/entities/Call.ts
|
|
7587
|
-
const logger$
|
|
7654
|
+
const logger$12 = getLogger();
|
|
7588
7655
|
/**
|
|
7589
7656
|
* Ratio between the critical and warning RTT spike multipliers.
|
|
7590
7657
|
* Warning threshold = baseline * warningMultiplier (default 3x)
|
|
@@ -7602,7 +7669,7 @@ const fromDestinationParams = (destination) => {
|
|
|
7602
7669
|
});
|
|
7603
7670
|
return params;
|
|
7604
7671
|
} catch (error) {
|
|
7605
|
-
logger$
|
|
7672
|
+
logger$12.warn(`Failed to parse destination URI: ${destination}`, error);
|
|
7606
7673
|
return {};
|
|
7607
7674
|
}
|
|
7608
7675
|
};
|
|
@@ -7736,7 +7803,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7736
7803
|
/** Toggles the call lock state, preventing or allowing new participants from joining. */
|
|
7737
7804
|
async toggleLock() {
|
|
7738
7805
|
const method = this.locked ? "call.unlock" : "call.lock";
|
|
7739
|
-
await this.executeMethod(this.
|
|
7806
|
+
await this.executeMethod(this.callSelf, method, {});
|
|
7740
7807
|
}
|
|
7741
7808
|
/**
|
|
7742
7809
|
* Toggles the hold state of the call (pauses/resumes local media transmission).
|
|
@@ -7785,14 +7852,24 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7785
7852
|
*
|
|
7786
7853
|
* Constructs call context (node_id, call_id, member_id) and sends the RPC request.
|
|
7787
7854
|
*
|
|
7788
|
-
* @param target - Target
|
|
7855
|
+
* @param target - Target {@link MemberTarget} triple, or the local member's
|
|
7856
|
+
* ID string for self-operations (any other string is rejected — a bare
|
|
7857
|
+
* member id cannot carry the remote member's own call context).
|
|
7789
7858
|
* @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
|
|
7790
7859
|
* @param args - Parameters for the RPC method.
|
|
7791
7860
|
* @returns The RPC response.
|
|
7861
|
+
* @throws {CallNotReadyError} If the call has no self member context yet.
|
|
7862
|
+
* @throws {InvalidParams} If a string target is not the local member's ID.
|
|
7792
7863
|
* @throws {JSONRPCError} If the RPC call returns an error.
|
|
7793
7864
|
*/
|
|
7794
7865
|
async executeMethod(target, method, args) {
|
|
7795
|
-
const
|
|
7866
|
+
const self = this.callSelf;
|
|
7867
|
+
if (typeof target === "string" && target !== self.member_id) throw new InvalidParams(`Target member ID ${target} does not match call's self member ID ${self.member_id}`);
|
|
7868
|
+
const params = {
|
|
7869
|
+
...args,
|
|
7870
|
+
self,
|
|
7871
|
+
target: typeof target === "string" ? self : target
|
|
7872
|
+
};
|
|
7796
7873
|
const request = buildRPCRequest({
|
|
7797
7874
|
method,
|
|
7798
7875
|
params
|
|
@@ -7802,29 +7879,27 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7802
7879
|
if (isJSONRPCErrorResponse(response)) throw new JSONRPCError(parseInt(response.result?.code ?? "0"), `Error response from method ${method}: ${response.result?.code} ${response.result?.message}`, void 0, void 0, request.id);
|
|
7803
7880
|
return response;
|
|
7804
7881
|
} catch (error) {
|
|
7805
|
-
logger$
|
|
7882
|
+
logger$12.error(`[Call] Error executing method ${method} with params`, params, error);
|
|
7806
7883
|
throw error;
|
|
7807
7884
|
}
|
|
7808
7885
|
}
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7886
|
+
/**
|
|
7887
|
+
* The local leg's member triple — sent as `self` in every member RPC
|
|
7888
|
+
* envelope, and as the `target` of call-scoped self-operations (e.g. lock,
|
|
7889
|
+
* layout).
|
|
7890
|
+
*
|
|
7891
|
+
* @throws {CallNotReadyError} Before `call.joined` delivers the self member
|
|
7892
|
+
* context (`selfId`/`nodeId`) — an RPC without it cannot be routed, so fail
|
|
7893
|
+
* fast instead of sending a doomed request.
|
|
7894
|
+
*/
|
|
7895
|
+
get callSelf() {
|
|
7896
|
+
const node_id = this.nodeId;
|
|
7897
|
+
const member_id = this.vertoManager.selfId;
|
|
7898
|
+
if (!node_id || !member_id) throw new CallNotReadyError(this.id);
|
|
7820
7899
|
return {
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
node_id: this.nodeId ?? "",
|
|
7825
|
-
call_id: this.id,
|
|
7826
|
-
member_id: target
|
|
7827
|
-
}
|
|
7900
|
+
node_id,
|
|
7901
|
+
call_id: this.id,
|
|
7902
|
+
member_id
|
|
7828
7903
|
};
|
|
7829
7904
|
}
|
|
7830
7905
|
/** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
|
|
@@ -8005,9 +8080,9 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8005
8080
|
*/
|
|
8006
8081
|
initResilienceSubsystems() {
|
|
8007
8082
|
const pc = this.rtcPeerConnection;
|
|
8008
|
-
logger$
|
|
8083
|
+
logger$12.debug(`[Call] initResilienceSubsystems: pc=${pc ? "exists" : "undefined"}, connectionState=${pc?.connectionState}`);
|
|
8009
8084
|
if (!pc) {
|
|
8010
|
-
logger$
|
|
8085
|
+
logger$12.warn("[Call] No peer connection available, skipping resilience init");
|
|
8011
8086
|
return;
|
|
8012
8087
|
}
|
|
8013
8088
|
try {
|
|
@@ -8042,14 +8117,14 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8042
8117
|
disableVideo: () => {
|
|
8043
8118
|
try {
|
|
8044
8119
|
this.vertoManager.muteMainVideoInputDevice();
|
|
8045
|
-
logger$
|
|
8120
|
+
logger$12.debug("[Call] Recovery manager disabled video");
|
|
8046
8121
|
} catch {
|
|
8047
|
-
logger$
|
|
8122
|
+
logger$12.debug("[Call] Recovery manager failed to disable video");
|
|
8048
8123
|
}
|
|
8049
8124
|
},
|
|
8050
8125
|
enableVideo: () => {
|
|
8051
8126
|
this.vertoManager.unmuteMainVideoInputDevice().catch(() => {
|
|
8052
|
-
logger$
|
|
8127
|
+
logger$12.debug("[Call] Recovery manager failed to enable video");
|
|
8053
8128
|
});
|
|
8054
8129
|
},
|
|
8055
8130
|
isNegotiating: () => this.vertoManager.mainPeerConnection.isNegotiating,
|
|
@@ -8099,7 +8174,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8099
8174
|
this.subscribeTo(this._recoveryManager.recoveryEvent$, (event) => {
|
|
8100
8175
|
this._recoveryEvent$.next(event);
|
|
8101
8176
|
if (event.action === "max_attempts_reached") {
|
|
8102
|
-
logger$
|
|
8177
|
+
logger$12.warn("[Call] All recovery attempts exhausted, terminating call");
|
|
8103
8178
|
this.emitError({
|
|
8104
8179
|
kind: "network",
|
|
8105
8180
|
fatal: true,
|
|
@@ -8119,13 +8194,13 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8119
8194
|
else if (event.type === "online") this._recoveryManager?.handleWebSocketReconnect();
|
|
8120
8195
|
});
|
|
8121
8196
|
this.subscribeTo(this.clientSession.authenticated$.pipe(skip(1), filter(Boolean)), () => {
|
|
8122
|
-
logger$
|
|
8197
|
+
logger$12.debug("[Call] WebSocket reconnected — notifying recovery manager");
|
|
8123
8198
|
this._recoveryManager?.handleWebSocketReconnect();
|
|
8124
8199
|
});
|
|
8125
8200
|
this._statsMonitor.start();
|
|
8126
|
-
logger$
|
|
8201
|
+
logger$12.debug("[Call] Resilience subsystems initialized for call", this.id);
|
|
8127
8202
|
} catch (error) {
|
|
8128
|
-
logger$
|
|
8203
|
+
logger$12.warn("[Call] Failed to initialize resilience subsystems:", error);
|
|
8129
8204
|
}
|
|
8130
8205
|
}
|
|
8131
8206
|
/**
|
|
@@ -8208,19 +8283,19 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8208
8283
|
}
|
|
8209
8284
|
isCallSessionEvent(event) {
|
|
8210
8285
|
try {
|
|
8211
|
-
logger$
|
|
8286
|
+
logger$12.debug("[Call] Checking if event is for this call session:", event);
|
|
8212
8287
|
const callId = getValueFrom(event, "params.params.callID") ?? getValueFrom(event, "params.call_id");
|
|
8213
8288
|
const roomSessionId = getValueFrom(event, "params.room_session_id");
|
|
8214
|
-
logger$
|
|
8289
|
+
logger$12.debug(`[Call] Extracted session identifiers callID: ${callId} and roomSessionID: ${roomSessionId} from event:`);
|
|
8215
8290
|
return callId === this.id || !!callId && this.callEventsManager.isCallIdValid(callId) || !!roomSessionId && this.callEventsManager.isRoomSessionIdValid(roomSessionId);
|
|
8216
8291
|
} catch (error) {
|
|
8217
|
-
logger$
|
|
8292
|
+
logger$12.error("[Call] Error checking if event is for this call session:", error);
|
|
8218
8293
|
return false;
|
|
8219
8294
|
}
|
|
8220
8295
|
}
|
|
8221
8296
|
get callSessionEvents$() {
|
|
8222
8297
|
return this.cachedObservable("callSessionEvents$", () => this.clientSession.signalingEvent$.pipe(filter((event) => this.isCallSessionEvent(event)), tap((event) => {
|
|
8223
|
-
logger$
|
|
8298
|
+
logger$12.debug("[Call] Received call session event:", event);
|
|
8224
8299
|
}), takeUntil(this.destroyed$), share()));
|
|
8225
8300
|
}
|
|
8226
8301
|
/** Observable of call-updated events. */
|
|
@@ -8290,16 +8365,16 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8290
8365
|
this._customSubscriptions.set(eventType, filtered$);
|
|
8291
8366
|
}, (error) => {
|
|
8292
8367
|
this._customSubscriptions.delete(eventType);
|
|
8293
|
-
logger$
|
|
8368
|
+
logger$12.warn(`[Call] verto.subscribe for '${eventType}' failed, not caching:`, error);
|
|
8294
8369
|
});
|
|
8295
8370
|
this._customSubscriptions.set(eventType, filtered$);
|
|
8296
8371
|
return filtered$;
|
|
8297
8372
|
}
|
|
8298
8373
|
get webrtcMessages$() {
|
|
8299
|
-
return this.cachedObservable("webrtcMessages$", () => this.callSessionEvents$.pipe(filterAs(isWebrtcMessageMetadata, "params"), tap((event) => logger$
|
|
8374
|
+
return this.cachedObservable("webrtcMessages$", () => this.callSessionEvents$.pipe(filterAs(isWebrtcMessageMetadata, "params"), tap((event) => logger$12.debug("[Call] Event is a WebRTC message event:", event)), takeUntil(this.destroyed$), share()));
|
|
8300
8375
|
}
|
|
8301
8376
|
get callEvent$() {
|
|
8302
|
-
return this.cachedObservable("callEvent$", () => this.callSessionEvents$.pipe(filterAs(isSignalwireCallMetadata, "params"), tap((event) => logger$
|
|
8377
|
+
return this.cachedObservable("callEvent$", () => this.callSessionEvents$.pipe(filterAs(isSignalwireCallMetadata, "params"), tap((event) => logger$12.debug("[Call] Event is a call event:", event)), takeUntil(this.destroyed$), share()));
|
|
8303
8378
|
}
|
|
8304
8379
|
get layoutEvent$() {
|
|
8305
8380
|
return this.cachedObservable("layoutEvent$", () => this.callEvent$.pipe(filterAs(isLayoutChangedMetadata, "params")));
|
|
@@ -8377,11 +8452,27 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8377
8452
|
return this.deferEmission(this._answered$.asObservable());
|
|
8378
8453
|
}
|
|
8379
8454
|
/**
|
|
8380
|
-
* Sets the call layout and participant positions.
|
|
8455
|
+
* Sets the call layout and, optionally, individual participant positions.
|
|
8456
|
+
*
|
|
8457
|
+
* The gateway `call.layout.set` DTO has **no** `positions` member, so when
|
|
8458
|
+
* `positions` is provided this method issues a `call.member.position.set`
|
|
8459
|
+
* request per member (via {@link Participant.setPosition}, which keys each
|
|
8460
|
+
* position by that member's own call context) alongside `call.layout.set`
|
|
8461
|
+
* (issue #19400, Flag #6).
|
|
8462
|
+
*
|
|
8463
|
+
* **These operations are NOT atomic.** The layout is applied first, then each
|
|
8464
|
+
* member position sequentially, so members may briefly flash into their
|
|
8465
|
+
* default slots before being moved to the requested positions. Targeted
|
|
8466
|
+
* members are validated upfront, though: when any of them has no
|
|
8467
|
+
* {@link Participant.target | member call context} yet, the whole call
|
|
8468
|
+
* rejects before any request is sent and the layout is left unchanged.
|
|
8381
8469
|
*
|
|
8382
8470
|
* @param layout - Layout name (must be one of {@link layouts}).
|
|
8383
|
-
* @param positions -
|
|
8471
|
+
* @param positions - Optional map of member IDs to {@link VideoPosition} values.
|
|
8472
|
+
* When omitted or empty, only the layout is changed.
|
|
8384
8473
|
* @throws {InvalidParams} If the layout is not in the available {@link layouts}.
|
|
8474
|
+
* @throws {ParticipantNotReadyError} If a targeted member's call context has
|
|
8475
|
+
* not been received yet — thrown before any request is sent.
|
|
8385
8476
|
*
|
|
8386
8477
|
* @example
|
|
8387
8478
|
* ```ts
|
|
@@ -8392,11 +8483,19 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8392
8483
|
*/
|
|
8393
8484
|
async setLayout(layout, positions) {
|
|
8394
8485
|
if (!this.layouts.includes(layout)) throw new InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
|
|
8486
|
+
const targets = [];
|
|
8487
|
+
for (const [memberId, position] of Object.entries(positions ?? {})) {
|
|
8488
|
+
const participant = this.participants.find((p) => p.id === memberId);
|
|
8489
|
+
if (!participant) {
|
|
8490
|
+
logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
|
|
8491
|
+
continue;
|
|
8492
|
+
}
|
|
8493
|
+
participant.target;
|
|
8494
|
+
targets.push([participant, position]);
|
|
8495
|
+
}
|
|
8395
8496
|
const selfId = await firstValueFrom(this.selfId$.pipe(filter((id) => id !== null)));
|
|
8396
|
-
await this.executeMethod(selfId, "call.layout.set", {
|
|
8397
|
-
|
|
8398
|
-
positions
|
|
8399
|
-
});
|
|
8497
|
+
await this.executeMethod(selfId, "call.layout.set", { layout });
|
|
8498
|
+
for (const [participant, position] of targets) await participant.setPosition(position);
|
|
8400
8499
|
}
|
|
8401
8500
|
/**
|
|
8402
8501
|
* Transfers the call to another destination.
|
|
@@ -8428,7 +8527,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8428
8527
|
setLocalMicrophoneGain(value) {
|
|
8429
8528
|
const pipeline = this.vertoManager.ensureLocalAudioPipeline();
|
|
8430
8529
|
if (!pipeline) {
|
|
8431
|
-
logger$
|
|
8530
|
+
logger$12.warn("[Call] setLocalMicrophoneGain: audio pipeline unavailable");
|
|
8432
8531
|
return;
|
|
8433
8532
|
}
|
|
8434
8533
|
const percent = Math.max(0, Math.min(200, value));
|
|
@@ -8473,7 +8572,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8473
8572
|
enablePushToTalk() {
|
|
8474
8573
|
const pipeline = this.vertoManager.ensureLocalAudioPipeline();
|
|
8475
8574
|
if (!pipeline) {
|
|
8476
|
-
logger$
|
|
8575
|
+
logger$12.warn("[Call] enablePushToTalk: audio pipeline unavailable");
|
|
8477
8576
|
return;
|
|
8478
8577
|
}
|
|
8479
8578
|
pipeline.setPTTActive(false);
|
|
@@ -8570,6 +8669,7 @@ function inferCallErrorKind(error) {
|
|
|
8570
8669
|
if (error instanceof RPCTimeoutError) return "timeout";
|
|
8571
8670
|
if (error instanceof JSONRPCError) return "signaling";
|
|
8572
8671
|
if (error instanceof MediaTrackError) return "media";
|
|
8672
|
+
if (error instanceof MediaAccessError) return "media";
|
|
8573
8673
|
if (error instanceof WebSocketConnectionError || error instanceof TransportConnectionError) return "network";
|
|
8574
8674
|
return "internal";
|
|
8575
8675
|
}
|
|
@@ -8586,6 +8686,7 @@ const RECOVERABLE_RPC_CODES = new Set([
|
|
|
8586
8686
|
function isFatalError(error) {
|
|
8587
8687
|
if (error instanceof VertoPongError) return false;
|
|
8588
8688
|
if (error instanceof MediaTrackError) return false;
|
|
8689
|
+
if (error instanceof MediaAccessError) return error.fatal;
|
|
8589
8690
|
if (error instanceof RPCTimeoutError) return false;
|
|
8590
8691
|
if (error instanceof JSONRPCError && RECOVERABLE_RPC_CODES.has(error.code)) return false;
|
|
8591
8692
|
return true;
|
|
@@ -8611,10 +8712,10 @@ var CallFactory = class {
|
|
|
8611
8712
|
return {
|
|
8612
8713
|
vertoManager: new WebRTCVertoManager(callInstance, this.attachManager, this.deviceController, this.webRTCApiProvider, {
|
|
8613
8714
|
nodeId: options.nodeId,
|
|
8614
|
-
onError: (error) => {
|
|
8715
|
+
onError: (error, options$1) => {
|
|
8615
8716
|
const callError = {
|
|
8616
8717
|
kind: inferCallErrorKind(error),
|
|
8617
|
-
fatal: isFatalError(error),
|
|
8718
|
+
fatal: options$1?.fatal ?? isFatalError(error),
|
|
8618
8719
|
error,
|
|
8619
8720
|
callId: callInstance.id
|
|
8620
8721
|
};
|
|
@@ -8635,7 +8736,7 @@ var CallFactory = class {
|
|
|
8635
8736
|
|
|
8636
8737
|
//#endregion
|
|
8637
8738
|
//#region src/behaviors/Collection.ts
|
|
8638
|
-
const logger$
|
|
8739
|
+
const logger$11 = getLogger();
|
|
8639
8740
|
var Fetcher = class {
|
|
8640
8741
|
constructor(endpoint, params, http) {
|
|
8641
8742
|
this.endpoint = endpoint;
|
|
@@ -8659,7 +8760,7 @@ var Fetcher = class {
|
|
|
8659
8760
|
this.hasMore = !!this.nextUrl;
|
|
8660
8761
|
return result.data.filter(this.filter).map(this.mapper);
|
|
8661
8762
|
}
|
|
8662
|
-
logger$
|
|
8763
|
+
logger$11.error("Failed to fetch entity");
|
|
8663
8764
|
return [];
|
|
8664
8765
|
}
|
|
8665
8766
|
async id(v) {
|
|
@@ -8735,7 +8836,7 @@ var EntityCollection = class extends Destroyable {
|
|
|
8735
8836
|
this._hasMore$.next(this.fetchController.hasMore ?? false);
|
|
8736
8837
|
this._loading$.next(false);
|
|
8737
8838
|
} catch (error) {
|
|
8738
|
-
logger$
|
|
8839
|
+
logger$11.error(`Failed to fetch initial collection data`, error);
|
|
8739
8840
|
this._hasMore$.next(this.fetchController.hasMore ?? false);
|
|
8740
8841
|
this._loading$.next(false);
|
|
8741
8842
|
this.onError?.(new CollectionFetchError("fetchMore", error));
|
|
@@ -8749,7 +8850,7 @@ var EntityCollection = class extends Destroyable {
|
|
|
8749
8850
|
if (data) this.upsertData(data);
|
|
8750
8851
|
return data;
|
|
8751
8852
|
} catch (error) {
|
|
8752
|
-
logger$
|
|
8853
|
+
logger$11.error(`Failed to fetch data for (${String(key)}:${String(value)}) :`, error);
|
|
8753
8854
|
this._loading$.next(false);
|
|
8754
8855
|
this.onError?.(new CollectionFetchError(`tryFetch(${String(key)})`, error));
|
|
8755
8856
|
}
|
|
@@ -8996,13 +9097,13 @@ var Address = class extends Destroyable {
|
|
|
8996
9097
|
|
|
8997
9098
|
//#endregion
|
|
8998
9099
|
//#region src/core/utils.ts
|
|
8999
|
-
const logger$
|
|
9100
|
+
const logger$10 = getLogger();
|
|
9000
9101
|
const isRPCConnectResult = (e) => {
|
|
9001
|
-
logger$
|
|
9102
|
+
logger$10.debug("isRPCConnectResult check:", e);
|
|
9002
9103
|
if (!e || typeof e !== "object") return false;
|
|
9003
9104
|
const result = e;
|
|
9004
9105
|
const is = typeof result.identity === "string" && typeof result.protocol === "string" && typeof result.authorization === "object" && typeof result.authorization.jti === "string" && typeof result.authorization.project_id === "string" && typeof result.authorization.fabric_subscriber === "object";
|
|
9005
|
-
logger$
|
|
9106
|
+
logger$10.debug("isRPCConnectResult check result:", is);
|
|
9006
9107
|
return is;
|
|
9007
9108
|
};
|
|
9008
9109
|
var PendingRPC = class PendingRPC {
|
|
@@ -9011,7 +9112,7 @@ var PendingRPC = class PendingRPC {
|
|
|
9011
9112
|
}
|
|
9012
9113
|
constructor(request, responses$, options) {
|
|
9013
9114
|
this.id = v4();
|
|
9014
|
-
logger$
|
|
9115
|
+
logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}: method:${request.method}] Creating PendingRPC`);
|
|
9015
9116
|
this.request = request;
|
|
9016
9117
|
const timeoutMs = options?.timeoutMs ?? PendingRPC.defaultTimeoutMs;
|
|
9017
9118
|
const signal = options?.signal;
|
|
@@ -9037,22 +9138,22 @@ var PendingRPC = class PendingRPC {
|
|
|
9037
9138
|
isSettled = true;
|
|
9038
9139
|
if (response.error) {
|
|
9039
9140
|
const rpcError = new JSONRPCError(response.error.code, response.error.message, response.error.data, void 0, request.id);
|
|
9040
|
-
logger$
|
|
9141
|
+
logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with RPC error:`, rpcError);
|
|
9041
9142
|
reject(rpcError);
|
|
9042
9143
|
} else {
|
|
9043
|
-
logger$
|
|
9144
|
+
logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Resolving promise with response:`, response);
|
|
9044
9145
|
resolve(response);
|
|
9045
9146
|
}
|
|
9046
9147
|
subscription.unsubscribe();
|
|
9047
9148
|
},
|
|
9048
9149
|
error: (error) => {
|
|
9049
|
-
logger$
|
|
9150
|
+
logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Rejecting promise with error:`, error);
|
|
9050
9151
|
isSettled = true;
|
|
9051
9152
|
reject(error);
|
|
9052
9153
|
subscription.unsubscribe();
|
|
9053
9154
|
},
|
|
9054
9155
|
complete: () => {
|
|
9055
|
-
logger$
|
|
9156
|
+
logger$10.debug(`[PendingRPC(${this.id}) request:${request.id}] Observable completed`);
|
|
9056
9157
|
if (!isSettled) reject(new RPCTimeoutError(request.id, timeoutMs));
|
|
9057
9158
|
subscription.unsubscribe();
|
|
9058
9159
|
}
|
|
@@ -9072,7 +9173,18 @@ var PendingRPC = class PendingRPC {
|
|
|
9072
9173
|
|
|
9073
9174
|
//#endregion
|
|
9074
9175
|
//#region src/managers/ClientSessionManager.ts
|
|
9075
|
-
const logger$
|
|
9176
|
+
const logger$9 = getLogger();
|
|
9177
|
+
/**
|
|
9178
|
+
* Decide whether an error emitted on `call.errors$` during dial should
|
|
9179
|
+
* abort the dial. A non-fatal MediaAccessError means the call degraded to
|
|
9180
|
+
* receive-only and still connects — everything else rejects `dial()` with
|
|
9181
|
+
* the real cause.
|
|
9182
|
+
*
|
|
9183
|
+
* Pure function — exported for unit testing.
|
|
9184
|
+
*/
|
|
9185
|
+
function shouldAbortDial(callError) {
|
|
9186
|
+
return callError.fatal || !(callError.error instanceof MediaAccessError);
|
|
9187
|
+
}
|
|
9076
9188
|
const getAddressSearchURI = (options) => {
|
|
9077
9189
|
const to = options.to?.split("?")[0];
|
|
9078
9190
|
const from$1 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
|
|
@@ -9170,7 +9282,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9170
9282
|
try {
|
|
9171
9283
|
return await this.transport.execute(request, options);
|
|
9172
9284
|
} catch (error) {
|
|
9173
|
-
logger$
|
|
9285
|
+
logger$9.debug("[Session] Execute Error", error);
|
|
9174
9286
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
9175
9287
|
throw error;
|
|
9176
9288
|
}
|
|
@@ -9184,13 +9296,13 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9184
9296
|
return true;
|
|
9185
9297
|
}
|
|
9186
9298
|
setupMessageHandlers() {
|
|
9187
|
-
logger$
|
|
9299
|
+
logger$9.debug("[Session] Setting up message handlers");
|
|
9188
9300
|
this.subscribeTo(this.authStateEvent$, async (authStateEvent) => {
|
|
9189
|
-
logger$
|
|
9301
|
+
logger$9.debug("[Session] Authorization state event received:", authStateEvent);
|
|
9190
9302
|
try {
|
|
9191
9303
|
await this.updateAuthorizationStateInStorage(authStateEvent.authorization_state);
|
|
9192
9304
|
} catch (error) {
|
|
9193
|
-
logger$
|
|
9305
|
+
logger$9.error("[Session] Failed to handle authorization state update:", error);
|
|
9194
9306
|
this._errors$.next(new AuthStateHandlerError(error));
|
|
9195
9307
|
}
|
|
9196
9308
|
});
|
|
@@ -9198,29 +9310,29 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9198
9310
|
if (this._authState$.value.kind === "authenticated") this._authState$.next({ kind: "unauthenticated" });
|
|
9199
9311
|
});
|
|
9200
9312
|
this.subscribeTo(this.transport.connectionStatus$.pipe(filter((status) => status === "connected"), exhaustMap(() => {
|
|
9201
|
-
logger$
|
|
9313
|
+
logger$9.debug("[Session] Connection established, initiating authentication");
|
|
9202
9314
|
return from(this.authenticate()).pipe(catchError((error) => {
|
|
9203
9315
|
this.handleAuthenticationError(error).catch((err) => {
|
|
9204
|
-
logger$
|
|
9316
|
+
logger$9.error("[Session] Error handling authentication failure:", err);
|
|
9205
9317
|
});
|
|
9206
9318
|
return EMPTY;
|
|
9207
9319
|
}));
|
|
9208
9320
|
})), void 0);
|
|
9209
9321
|
this.subscribeTo(this.vertoInvite$, async (invite) => {
|
|
9210
|
-
logger$
|
|
9322
|
+
logger$9.debug("[Session] Verto invite received:", invite);
|
|
9211
9323
|
try {
|
|
9212
9324
|
await this.createInboundCall(invite);
|
|
9213
9325
|
} catch (error) {
|
|
9214
|
-
logger$
|
|
9326
|
+
logger$9.error("[Session] Error handling Verto invite:", error);
|
|
9215
9327
|
this._errors$.next(new VertoInviteHandlerError(error));
|
|
9216
9328
|
}
|
|
9217
9329
|
});
|
|
9218
9330
|
this.subscribeTo(this.vertoAttach$, async (attach) => {
|
|
9219
|
-
logger$
|
|
9331
|
+
logger$9.debug("[Session] Verto attach received:", attach);
|
|
9220
9332
|
try {
|
|
9221
9333
|
await this.handleVertoAttach(attach);
|
|
9222
9334
|
} catch (error) {
|
|
9223
|
-
logger$
|
|
9335
|
+
logger$9.error("[Session] Error handling Verto attach:", error);
|
|
9224
9336
|
this._errors$.next(new VertoAttachHandlerError(error));
|
|
9225
9337
|
}
|
|
9226
9338
|
});
|
|
@@ -9230,36 +9342,36 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9230
9342
|
const storedState = await this.storage.getItem(this.authorizationStateKey);
|
|
9231
9343
|
this.authorizationState$.next(storedState ?? void 0);
|
|
9232
9344
|
} catch (error) {
|
|
9233
|
-
logger$
|
|
9345
|
+
logger$9.error("Failed to retrieve authorization state from storage:", error);
|
|
9234
9346
|
this.authorizationState$.next(void 0);
|
|
9235
9347
|
}
|
|
9236
9348
|
}
|
|
9237
9349
|
async updateAuthorizationStateInStorage(authorizationState) {
|
|
9238
9350
|
if (!authorizationState) {
|
|
9239
|
-
logger$
|
|
9351
|
+
logger$9.debug("[Session] Removing authorization state from storage");
|
|
9240
9352
|
try {
|
|
9241
9353
|
await this.storage.removeItem(this.authorizationStateKey);
|
|
9242
9354
|
this.authorizationState$.next(void 0);
|
|
9243
9355
|
} catch (error) {
|
|
9244
|
-
logger$
|
|
9356
|
+
logger$9.error("Failed to remove authorization state from storage:", error);
|
|
9245
9357
|
throw error;
|
|
9246
9358
|
}
|
|
9247
9359
|
return;
|
|
9248
9360
|
}
|
|
9249
9361
|
try {
|
|
9250
|
-
logger$
|
|
9362
|
+
logger$9.debug("[Session] Updating authorization state in storage");
|
|
9251
9363
|
await this.storage.setItem(this.authorizationStateKey, authorizationState);
|
|
9252
9364
|
this.authorizationState$.next(authorizationState);
|
|
9253
9365
|
} catch (error) {
|
|
9254
|
-
logger$
|
|
9366
|
+
logger$9.error("Failed to retrieve authorization state from storage:", error);
|
|
9255
9367
|
throw error;
|
|
9256
9368
|
}
|
|
9257
9369
|
}
|
|
9258
9370
|
get authStateEvent$() {
|
|
9259
9371
|
return this.cachedObservable("authStateEvent$", () => this.signalingEvent$.pipe(tap((msg) => {
|
|
9260
|
-
logger$
|
|
9372
|
+
logger$9.debug("[Session] Received incoming message:", msg);
|
|
9261
9373
|
}), filterAs(isSignalwireAuthorizationStateMetadata, "params"), tap((event) => {
|
|
9262
|
-
logger$
|
|
9374
|
+
logger$9.debug("[Session] Authorization state event received:", event.authorization_state);
|
|
9263
9375
|
})));
|
|
9264
9376
|
}
|
|
9265
9377
|
get signalingEvent$() {
|
|
@@ -9297,42 +9409,72 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9297
9409
|
await firstValueFrom(this.authenticated$.pipe(takeUntil(this.destroyed$), filter(Boolean), take(1), timeout({ first: 15e3 })));
|
|
9298
9410
|
}
|
|
9299
9411
|
async handleAuthenticationError(error) {
|
|
9300
|
-
logger$
|
|
9301
|
-
const isRecoverableAuthError = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
9412
|
+
logger$9.error("Authentication error:", error);
|
|
9413
|
+
const isRecoverableAuthError$1 = error instanceof JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
9302
9414
|
const hasStoredState = await firstValueFrom(this.authorizationState$.pipe(take(1))) !== void 0;
|
|
9303
|
-
if (isRecoverableAuthError && hasStoredState) {
|
|
9304
|
-
logger$
|
|
9415
|
+
if (isRecoverableAuthError$1 && hasStoredState) {
|
|
9416
|
+
logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
|
|
9305
9417
|
try {
|
|
9306
9418
|
await this.cleanupStoredConnectionParams();
|
|
9307
9419
|
} catch (cleanupError) {
|
|
9308
|
-
logger$
|
|
9420
|
+
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
9309
9421
|
} finally {
|
|
9310
9422
|
this.transport.reconnect();
|
|
9311
9423
|
}
|
|
9312
9424
|
} else this._errors$.next(error);
|
|
9313
9425
|
}
|
|
9426
|
+
/**
|
|
9427
|
+
* Clear the resume state (authorization_state + protocol) only.
|
|
9428
|
+
*
|
|
9429
|
+
* This is the stale-auth-state recovery helper used by handleAuthError:
|
|
9430
|
+
* the server rejected a reconnect, so the resume state is discarded and a
|
|
9431
|
+
* fresh connect follows. Attach records are deliberately preserved — the
|
|
9432
|
+
* session lives on through the reconnect and reattachCalls() needs the
|
|
9433
|
+
* stored call references afterwards. Do NOT add detachAll() here.
|
|
9434
|
+
*
|
|
9435
|
+
* For public teardown (disconnect/destroy), use {@link teardownSessionState}
|
|
9436
|
+
* instead, which clears the attach records as well.
|
|
9437
|
+
*/
|
|
9314
9438
|
async cleanupStoredConnectionParams() {
|
|
9315
9439
|
await this.transport.setProtocol(void 0);
|
|
9316
9440
|
await this.updateAuthorizationStateInStorage(void 0);
|
|
9317
9441
|
this._authorization$.next(void 0);
|
|
9318
9442
|
}
|
|
9443
|
+
/**
|
|
9444
|
+
* Public-teardown helper for disconnect()/destroy(). Clears the resume
|
|
9445
|
+
* state (authorization_state + protocol) AND the attach records as one
|
|
9446
|
+
* atomic unit.
|
|
9447
|
+
*
|
|
9448
|
+
* The two stores are coupled: the backend only honors attach records
|
|
9449
|
+
* within the session identified by the resume state, so ending the
|
|
9450
|
+
* session must clear both. Clearing one without the other strands records
|
|
9451
|
+
* no future session can honor (disconnect) or revives a session the
|
|
9452
|
+
* developer explicitly ended (destroy).
|
|
9453
|
+
*
|
|
9454
|
+
* Distinct from {@link cleanupStoredConnectionParams}, which keeps the
|
|
9455
|
+
* attach records for the stale-auth-state recovery path.
|
|
9456
|
+
*/
|
|
9457
|
+
async teardownSessionState() {
|
|
9458
|
+
await this.cleanupStoredConnectionParams();
|
|
9459
|
+
await this.attachManager.detachAll();
|
|
9460
|
+
}
|
|
9319
9461
|
async updateAuthState(authorization_state) {
|
|
9320
9462
|
try {
|
|
9321
9463
|
await this.storage.setItem(this.authorizationStateKey, authorization_state);
|
|
9322
9464
|
} catch (error) {
|
|
9323
|
-
logger$
|
|
9465
|
+
logger$9.error("Failed to update authorization state in storage:", error);
|
|
9324
9466
|
this._errors$.next(new AuthStateHandlerError(error));
|
|
9325
9467
|
}
|
|
9326
9468
|
}
|
|
9327
9469
|
async reauthenticate(token, dpopToken, options) {
|
|
9328
|
-
logger$
|
|
9470
|
+
logger$9.debug("[Session] Re-authenticating session");
|
|
9329
9471
|
try {
|
|
9330
9472
|
let resolvedDpopToken = dpopToken;
|
|
9331
9473
|
if (!resolvedDpopToken && this.dpopManager?.initialized) try {
|
|
9332
9474
|
resolvedDpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
|
|
9333
9475
|
} catch (error) {
|
|
9334
9476
|
if (this.clientBound) throw error;
|
|
9335
|
-
logger$
|
|
9477
|
+
logger$9.warn("[Session] Failed to create DPoP proof for reauthenticate:", error);
|
|
9336
9478
|
}
|
|
9337
9479
|
const request = RPCReauthenticate({
|
|
9338
9480
|
project: this._authorization$.value?.project_id ?? "",
|
|
@@ -9340,24 +9482,24 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9340
9482
|
...resolvedDpopToken ? { dpop_token: resolvedDpopToken } : {}
|
|
9341
9483
|
});
|
|
9342
9484
|
await lastValueFrom(from(this.transport.execute(request)).pipe(throwOnRPCError(), take(1), catchError((err) => {
|
|
9343
|
-
logger$
|
|
9485
|
+
logger$9.error("[Session] Re-authentication RPC failed:", err);
|
|
9344
9486
|
throw err;
|
|
9345
9487
|
})));
|
|
9346
9488
|
if (options?.clientBound) this._wasClientBound = true;
|
|
9347
|
-
logger$
|
|
9489
|
+
logger$9.debug("[Session] Re-authentication successful, updating stored auth state");
|
|
9348
9490
|
} catch (error) {
|
|
9349
|
-
logger$
|
|
9491
|
+
logger$9.error("[Session] Re-authentication failed:", error);
|
|
9350
9492
|
this._errors$.next(new AuthStateHandlerError(error));
|
|
9351
9493
|
throw error;
|
|
9352
9494
|
}
|
|
9353
9495
|
}
|
|
9354
9496
|
async authenticate() {
|
|
9355
|
-
logger$
|
|
9497
|
+
logger$9.debug("[Session] Starting authentication process");
|
|
9356
9498
|
const persistedParams = await firstValueFrom(combineLatest({
|
|
9357
9499
|
protocol: this.transport.protocol$,
|
|
9358
9500
|
authorization_state: this.authorizationState$
|
|
9359
9501
|
}).pipe(take(1)));
|
|
9360
|
-
logger$
|
|
9502
|
+
logger$9.debug("[Session] Persisted params:\n", {
|
|
9361
9503
|
protocol: persistedParams.protocol,
|
|
9362
9504
|
authStateLength: persistedParams.authorization_state?.length
|
|
9363
9505
|
});
|
|
@@ -9365,16 +9507,20 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9365
9507
|
const storedToken = this.getCredential().token;
|
|
9366
9508
|
const isReconnect = hasReconnectState && storedToken;
|
|
9367
9509
|
let dpopToken;
|
|
9368
|
-
if (isReconnect) logger$
|
|
9369
|
-
else
|
|
9370
|
-
|
|
9371
|
-
|
|
9510
|
+
if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
|
|
9511
|
+
else {
|
|
9512
|
+
const credential = this.getCredential();
|
|
9513
|
+
const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + CREDENTIAL_EXPIRY_SKEW_MS;
|
|
9514
|
+
if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
|
|
9515
|
+
logger$9.debug("[Session] Refreshing credentials before fresh connect");
|
|
9516
|
+
await this.onBeforeReconnect();
|
|
9517
|
+
}
|
|
9372
9518
|
}
|
|
9373
|
-
if (
|
|
9519
|
+
if (this.dpopManager?.initialized) try {
|
|
9374
9520
|
dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
|
|
9375
9521
|
} catch (error) {
|
|
9376
9522
|
if (this.clientBound) throw error;
|
|
9377
|
-
logger$
|
|
9523
|
+
logger$9.warn("[Session] Failed to create DPoP proof for connect, proceeding without:", error);
|
|
9378
9524
|
}
|
|
9379
9525
|
const rpcConnectRequest = RPCConnect({
|
|
9380
9526
|
authentication: isReconnect ? { jwt_token: storedToken } : this.authentication,
|
|
@@ -9391,26 +9537,27 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9391
9537
|
} : {}
|
|
9392
9538
|
});
|
|
9393
9539
|
const response = await lastValueFrom(from(this.transport.execute(rpcConnectRequest)).pipe(throwOnRPCError(), map((res) => res.result), filter(isRPCConnectResult), tap(() => {
|
|
9394
|
-
logger$
|
|
9540
|
+
logger$9.debug("[Session] Response passed filter, processing authentication result");
|
|
9395
9541
|
}), take(1), catchError((err) => {
|
|
9396
|
-
logger$
|
|
9542
|
+
logger$9.error("[Session] Authentication RPC failed:", err);
|
|
9397
9543
|
throw err;
|
|
9398
9544
|
})));
|
|
9399
|
-
logger$
|
|
9545
|
+
logger$9.debug("[Session] Processing authentication result:", {
|
|
9400
9546
|
hasProtocol: !!response.protocol,
|
|
9401
9547
|
hasAuthorization: !!response.authorization,
|
|
9402
9548
|
hasIceServers: !!response.ice_servers
|
|
9403
9549
|
});
|
|
9404
9550
|
if (response.protocol) await this.transport.setProtocol(response.protocol);
|
|
9405
9551
|
this._authorization$.next(response.authorization);
|
|
9552
|
+
if (response.authorization.cnf?.jkt) this._wasClientBound = true;
|
|
9406
9553
|
this._iceServers$.next(response.ice_servers ?? []);
|
|
9407
9554
|
this._authState$.next({ kind: "authenticated" });
|
|
9408
|
-
logger$
|
|
9555
|
+
logger$9.debug("[Session] Authentication completed successfully");
|
|
9409
9556
|
}
|
|
9410
9557
|
async disconnect() {
|
|
9411
9558
|
this.transport.disconnect();
|
|
9412
9559
|
this._authState$.next({ kind: "unauthenticated" });
|
|
9413
|
-
await this.
|
|
9560
|
+
await this.teardownSessionState();
|
|
9414
9561
|
}
|
|
9415
9562
|
async createInboundCall(invite) {
|
|
9416
9563
|
const callSession = await this.createCall({
|
|
@@ -9441,11 +9588,11 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9441
9588
|
async handleVertoAttach(attach) {
|
|
9442
9589
|
const { callID } = attach;
|
|
9443
9590
|
if (callID in this._calls$.value) {
|
|
9444
|
-
logger$
|
|
9591
|
+
logger$9.debug(`[Session] Verto attach for existing call ${callID}, deferring to per-call handler`);
|
|
9445
9592
|
return;
|
|
9446
9593
|
}
|
|
9447
9594
|
const storedOptions = await this.attachManager.consumePendingAttachment(callID);
|
|
9448
|
-
logger$
|
|
9595
|
+
logger$9.debug(`[Session] Creating reattached call for callID: ${callID}`);
|
|
9449
9596
|
const callSession = await this.createCall({
|
|
9450
9597
|
nodeId: attach.node_id,
|
|
9451
9598
|
callId: callID,
|
|
@@ -9469,14 +9616,14 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9469
9616
|
to: destinationURI,
|
|
9470
9617
|
...options
|
|
9471
9618
|
});
|
|
9472
|
-
await firstValueFrom(race(callSession.selfId$.pipe(filter((id) => Boolean(id)), take(1), timeout(this.callCreateTimeout)), callSession.errors$.pipe(take(1), switchMap((callError) => throwError(() => callError.error)))));
|
|
9619
|
+
await firstValueFrom(race(callSession.selfId$.pipe(filter((id) => Boolean(id)), take(1), timeout(this.callCreateTimeout)), callSession.errors$.pipe(filter(shouldAbortDial), take(1), switchMap((callError) => throwError(() => callError.error)))));
|
|
9473
9620
|
this._calls$.next({
|
|
9474
9621
|
[`${callSession.id}`]: callSession,
|
|
9475
9622
|
...this._calls$.value
|
|
9476
9623
|
});
|
|
9477
9624
|
return callSession;
|
|
9478
9625
|
} catch (error) {
|
|
9479
|
-
logger$
|
|
9626
|
+
logger$9.error("[Session] Error creating outbound call:", error);
|
|
9480
9627
|
callSession?.destroy();
|
|
9481
9628
|
const callError = new CallCreateError(error instanceof TimeoutError ? "Call create timeout" : "Call creation failed", error, "outbound");
|
|
9482
9629
|
this._errors$.next(callError);
|
|
@@ -9494,7 +9641,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9494
9641
|
address = this._directory.get(addressId);
|
|
9495
9642
|
if (!address) throw new DependencyError(`Address ID: ${addressId} not found`);
|
|
9496
9643
|
} catch {
|
|
9497
|
-
logger$
|
|
9644
|
+
logger$9.warn(`[Session] Directory lookup failed for ${addressURI}, proceeding with raw URI`);
|
|
9498
9645
|
}
|
|
9499
9646
|
const callSession = this.callFactory.createCall(address, { ...options });
|
|
9500
9647
|
this.subscribeTo(callSession.status$.pipe(filter((status) => status === "destroyed"), take(1)), () => {
|
|
@@ -9503,7 +9650,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9503
9650
|
});
|
|
9504
9651
|
return callSession;
|
|
9505
9652
|
} catch (error) {
|
|
9506
|
-
logger$
|
|
9653
|
+
logger$9.error("[Session] Error creating call session:", error);
|
|
9507
9654
|
throw new CallCreateError("Call create error", error, options.initOffer ? "inbound" : "outbound");
|
|
9508
9655
|
}
|
|
9509
9656
|
}
|
|
@@ -9522,6 +9669,14 @@ var ClientSessionWrapper = class {
|
|
|
9522
9669
|
get authenticated() {
|
|
9523
9670
|
return this.clientSessionManager.authenticated;
|
|
9524
9671
|
}
|
|
9672
|
+
/**
|
|
9673
|
+
* Whether the session is using a Client Bound SAT (DPoP). Sticky — set
|
|
9674
|
+
* when the binding is established or restored from a resumed session's
|
|
9675
|
+
* server authorization.
|
|
9676
|
+
*/
|
|
9677
|
+
get clientBound() {
|
|
9678
|
+
return this.clientSessionManager.clientBound;
|
|
9679
|
+
}
|
|
9525
9680
|
get signalingEvent$() {
|
|
9526
9681
|
return this.clientSessionManager.signalingEvent$;
|
|
9527
9682
|
}
|
|
@@ -9551,7 +9706,7 @@ const isString = (obj) => typeof obj === "string";
|
|
|
9551
9706
|
|
|
9552
9707
|
//#endregion
|
|
9553
9708
|
//#region src/managers/ConversationsManager.ts
|
|
9554
|
-
const logger$
|
|
9709
|
+
const logger$8 = getLogger();
|
|
9555
9710
|
var ConversationMessagesFetcher = class extends Fetcher {
|
|
9556
9711
|
constructor(groupId, http) {
|
|
9557
9712
|
super(`/api/fabric/conversations/${groupId}/messages`, "page_size=100", http);
|
|
@@ -9591,13 +9746,13 @@ var ConversationsManager = class {
|
|
|
9591
9746
|
}
|
|
9592
9747
|
throw new ConversationError("Join Failed - Unexpected response");
|
|
9593
9748
|
} catch (error) {
|
|
9594
|
-
logger$
|
|
9749
|
+
logger$8.error("[ConversationsManager] Failed to join conversation:", error);
|
|
9595
9750
|
throw error;
|
|
9596
9751
|
}
|
|
9597
9752
|
}
|
|
9598
9753
|
async getConversationMessageCollection(addressId) {
|
|
9599
9754
|
const groupId = this.groupIds.get(addressId) ?? await this.join(addressId);
|
|
9600
|
-
return Promise.resolve(new ConversationMessageCollection(groupId, this.clientSession.signalingEvent$.pipe(filterAs(isConversationMessageMetadata, "params"), tap((event) => logger$
|
|
9755
|
+
return Promise.resolve(new ConversationMessageCollection(groupId, this.clientSession.signalingEvent$.pipe(filterAs(isConversationMessageMetadata, "params"), tap((event) => logger$8.debug("[ConversationsManager ] Conversation Event:", event)), map((params) => ({ ...params }))), this.http, this.onError));
|
|
9601
9756
|
}
|
|
9602
9757
|
async sendText(text, destinationAddressId) {
|
|
9603
9758
|
const groupId = this.groupIds.get(destinationAddressId) ?? await this.join(destinationAddressId);
|
|
@@ -9614,7 +9769,7 @@ var ConversationsManager = class {
|
|
|
9614
9769
|
})).ok) return;
|
|
9615
9770
|
throw new ConversationError("Send Text Failed - Unexpected response");
|
|
9616
9771
|
} catch (error) {
|
|
9617
|
-
logger$
|
|
9772
|
+
logger$8.error("[ConversationsManager] Failed to send text message:", error);
|
|
9618
9773
|
throw error;
|
|
9619
9774
|
}
|
|
9620
9775
|
}
|
|
@@ -9622,7 +9777,7 @@ var ConversationsManager = class {
|
|
|
9622
9777
|
|
|
9623
9778
|
//#endregion
|
|
9624
9779
|
//#region src/managers/DeviceTokenManager.ts
|
|
9625
|
-
const logger$
|
|
9780
|
+
const logger$7 = getLogger();
|
|
9626
9781
|
/**
|
|
9627
9782
|
* Resolves the token expiry timestamp (epoch seconds) using a 3-tier priority chain:
|
|
9628
9783
|
* 1. `data.expires_at` — server-provided absolute timestamp
|
|
@@ -9632,7 +9787,7 @@ const logger$6 = getLogger();
|
|
|
9632
9787
|
function resolveExpiresAt(data) {
|
|
9633
9788
|
if (data.expires_at) return data.expires_at;
|
|
9634
9789
|
if (data.expires_in) return Math.floor(Date.now() / 1e3) + data.expires_in;
|
|
9635
|
-
logger$
|
|
9790
|
+
logger$7.warn("[DeviceToken] Could not determine token expiry, using default");
|
|
9636
9791
|
return Math.floor(Date.now() / 1e3) + DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
|
|
9637
9792
|
}
|
|
9638
9793
|
/**
|
|
@@ -9665,11 +9820,12 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9665
9820
|
this.getCredential = getCredential;
|
|
9666
9821
|
this._currentToken$ = this.createBehaviorSubject(null);
|
|
9667
9822
|
this._refreshInProgress = false;
|
|
9823
|
+
this._paused = false;
|
|
9668
9824
|
this._effectiveExpireIn = DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
|
|
9669
9825
|
this.subscribeTo(this._currentToken$.pipe(filter(Boolean), switchMap((tokenData) => {
|
|
9670
9826
|
const expiresAt = resolveExpiresAt(tokenData);
|
|
9671
9827
|
const refreshIn = Math.max(expiresAt * 1e3 - Date.now() - DEVICE_TOKEN_REFRESH_BUFFER_MS, 1e3);
|
|
9672
|
-
logger$
|
|
9828
|
+
logger$7.debug(`[DeviceToken] Scheduling Client Bound SAT refresh in ${refreshIn}ms`);
|
|
9673
9829
|
return timer(refreshIn);
|
|
9674
9830
|
})), () => {
|
|
9675
9831
|
this.executeRefresh();
|
|
@@ -9683,7 +9839,12 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9683
9839
|
* Activates the Client Bound SAT flow when the user's token has
|
|
9684
9840
|
* `sat:refresh` scope.
|
|
9685
9841
|
*
|
|
9686
|
-
*
|
|
9842
|
+
* Returns an {@link ActivationResult} indicating whether the manager
|
|
9843
|
+
* took ownership of refresh duties. The caller must use the `activated`
|
|
9844
|
+
* boolean to decide whether to keep its own refresh path armed — when
|
|
9845
|
+
* `activated` is `false`, the caller is responsible for refresh.
|
|
9846
|
+
*
|
|
9847
|
+
* Steps on success:
|
|
9687
9848
|
* 1. Check user's `sat_claims` for `sat:refresh` scope
|
|
9688
9849
|
* 2. Call `/api/fabric/subscriber/devices/token` with a DPoP proof
|
|
9689
9850
|
* 3. Reauthenticate the session with the Client Bound SAT + DPoP proof
|
|
@@ -9692,32 +9853,66 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9692
9853
|
async activate(user, session, updateCredential) {
|
|
9693
9854
|
const { satClaims } = user;
|
|
9694
9855
|
if (!satClaims?.scope?.includes(SAT_REFRESH_SCOPE)) {
|
|
9695
|
-
logger$
|
|
9696
|
-
return
|
|
9856
|
+
logger$7.debug("[DeviceToken] No sat:refresh scope, skipping Client Bound SAT activation");
|
|
9857
|
+
return {
|
|
9858
|
+
activated: false,
|
|
9859
|
+
reason: "no-scope"
|
|
9860
|
+
};
|
|
9697
9861
|
}
|
|
9698
9862
|
this._session = session;
|
|
9699
9863
|
this._updateCredential = updateCredential;
|
|
9700
9864
|
try {
|
|
9865
|
+
const cached = this._currentToken$.value;
|
|
9866
|
+
if (cached && this.isTokenFresh(cached)) {
|
|
9867
|
+
logger$7.debug("[DeviceToken] Reusing cached Client Bound SAT — skipping /devices/token");
|
|
9868
|
+
const rpcProof$1 = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
|
|
9869
|
+
await session.reauthenticate(cached.token, rpcProof$1, { clientBound: true });
|
|
9870
|
+
updateCredential({ token: cached.token });
|
|
9871
|
+
return { activated: true };
|
|
9872
|
+
}
|
|
9701
9873
|
const tokenData = await this.obtainToken();
|
|
9702
9874
|
if (!tokenData.expires_at && !tokenData.expires_in && satClaims.expires_at) tokenData.expires_at = satClaims.expires_at;
|
|
9703
9875
|
this._effectiveExpireIn = resolveExpireIn(tokenData);
|
|
9704
9876
|
const rpcProof = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
|
|
9705
9877
|
await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
|
|
9706
9878
|
updateCredential({ token: tokenData.token });
|
|
9707
|
-
logger$
|
|
9708
|
-
this.
|
|
9879
|
+
logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
|
|
9880
|
+
this.emitCurrentToken(tokenData);
|
|
9881
|
+
return { activated: true };
|
|
9709
9882
|
} catch (error) {
|
|
9710
|
-
logger$
|
|
9883
|
+
logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
|
|
9711
9884
|
this.errorHandler(new DPoPInitError(error, "Failed to activate Client Bound SAT"));
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
9715
|
-
|
|
9716
|
-
expires_at: expiresAt
|
|
9717
|
-
});
|
|
9885
|
+
return {
|
|
9886
|
+
activated: false,
|
|
9887
|
+
reason: "endpoint-failed"
|
|
9888
|
+
};
|
|
9718
9889
|
}
|
|
9719
9890
|
}
|
|
9720
9891
|
/**
|
|
9892
|
+
* Emit a freshly received token to the reactive pipeline, stamping an
|
|
9893
|
+
* absolute `expires_at` when the response carried only `expires_in`.
|
|
9894
|
+
* Resolving the expiry at RECEIVE time (not at read time) is what lets
|
|
9895
|
+
* {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
|
|
9896
|
+
* re-resolved later would always compute a full TTL from "now" and never
|
|
9897
|
+
* cross the refresh buffer.
|
|
9898
|
+
*/
|
|
9899
|
+
emitCurrentToken(token) {
|
|
9900
|
+
const stamped = token.expires_at ? token : {
|
|
9901
|
+
...token,
|
|
9902
|
+
expires_at: resolveExpiresAt(token)
|
|
9903
|
+
};
|
|
9904
|
+
this._currentToken$.next(stamped);
|
|
9905
|
+
}
|
|
9906
|
+
/**
|
|
9907
|
+
* Returns true when the cached token has enough headroom before expiry to
|
|
9908
|
+
* be safely reused on reactivation. The headroom matches the refresh
|
|
9909
|
+
* buffer, so a token within the refresh window is treated as stale (the
|
|
9910
|
+
* reactive pipeline is about to refresh it anyway).
|
|
9911
|
+
*/
|
|
9912
|
+
isTokenFresh(token) {
|
|
9913
|
+
return resolveExpiresAt(token) * 1e3 - Date.now() > DEVICE_TOKEN_REFRESH_BUFFER_MS;
|
|
9914
|
+
}
|
|
9915
|
+
/**
|
|
9721
9916
|
* Obtains a Client Bound SAT from `/api/fabric/subscriber/devices/token`.
|
|
9722
9917
|
* Returns the full {@link DeviceTokenResponse} including expiry metadata.
|
|
9723
9918
|
*/
|
|
@@ -9747,7 +9942,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9747
9942
|
* handled by the reactive pipeline).
|
|
9748
9943
|
*/
|
|
9749
9944
|
async refreshToken(session, currentToken, updateCredential) {
|
|
9750
|
-
logger$
|
|
9945
|
+
logger$7.debug("[DeviceToken] Refreshing Client Bound SAT");
|
|
9751
9946
|
const dpopProof = await this.dpopManager.createHttpProof({
|
|
9752
9947
|
method: "POST",
|
|
9753
9948
|
uri: DEVICE_REFRESH_ENDPOINT,
|
|
@@ -9769,7 +9964,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9769
9964
|
const rpcProof = await this.dpopManager.createRpcProof({ method: "signalwire.reauthenticate" });
|
|
9770
9965
|
await session.reauthenticate(data.token, rpcProof);
|
|
9771
9966
|
updateCredential({ token: data.token });
|
|
9772
|
-
logger$
|
|
9967
|
+
logger$7.info("[DeviceToken] Client Bound SAT refreshed successfully");
|
|
9773
9968
|
return data;
|
|
9774
9969
|
}
|
|
9775
9970
|
/**
|
|
@@ -9778,18 +9973,22 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9778
9973
|
* On all retries exhausted, emits to `errorHandler`.
|
|
9779
9974
|
*/
|
|
9780
9975
|
async executeRefresh() {
|
|
9976
|
+
if (this._paused) {
|
|
9977
|
+
logger$7.debug("[DeviceToken] Manager paused, skipping refresh");
|
|
9978
|
+
return;
|
|
9979
|
+
}
|
|
9781
9980
|
if (this._refreshInProgress) {
|
|
9782
|
-
logger$
|
|
9981
|
+
logger$7.debug("[DeviceToken] Refresh already in progress, skipping");
|
|
9783
9982
|
return;
|
|
9784
9983
|
}
|
|
9785
9984
|
const session = this._session;
|
|
9786
9985
|
const updateCredential = this._updateCredential;
|
|
9787
9986
|
if (!session || !updateCredential) {
|
|
9788
|
-
logger$
|
|
9987
|
+
logger$7.warn("[DeviceToken] Cannot refresh: session or updateCredential not set");
|
|
9789
9988
|
return;
|
|
9790
9989
|
}
|
|
9791
9990
|
if (!session.authenticated) {
|
|
9792
|
-
logger$
|
|
9991
|
+
logger$7.debug("[DeviceToken] Session not authenticated, deferring refresh");
|
|
9793
9992
|
return;
|
|
9794
9993
|
}
|
|
9795
9994
|
this._refreshInProgress = true;
|
|
@@ -9797,9 +9996,9 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9797
9996
|
const currentToken = this.getCredential().token;
|
|
9798
9997
|
if (!currentToken) throw new TokenRefreshError("No current token available for refresh");
|
|
9799
9998
|
const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
|
|
9800
|
-
this.
|
|
9999
|
+
this.emitCurrentToken(newTokenData);
|
|
9801
10000
|
} catch (error) {
|
|
9802
|
-
logger$
|
|
10001
|
+
logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
|
|
9803
10002
|
this.errorHandler(error instanceof TokenRefreshError ? error : new TokenRefreshError("Automatic token refresh failed", error));
|
|
9804
10003
|
} finally {
|
|
9805
10004
|
this._refreshInProgress = false;
|
|
@@ -9817,18 +10016,324 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9817
10016
|
lastError = error;
|
|
9818
10017
|
if (attempt < DEVICE_TOKEN_REFRESH_MAX_RETRIES - 1) {
|
|
9819
10018
|
const delay = DEVICE_TOKEN_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt);
|
|
9820
|
-
logger$
|
|
10019
|
+
logger$7.warn(`[DeviceToken] Refresh attempt ${attempt + 1} failed, retrying in ${delay}ms`);
|
|
9821
10020
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
9822
10021
|
}
|
|
9823
10022
|
}
|
|
9824
10023
|
throw lastError instanceof Error ? lastError : new TokenRefreshError("All refresh retries exhausted", lastError);
|
|
9825
10024
|
}
|
|
10025
|
+
/**
|
|
10026
|
+
* Force an immediate refresh when the cached Client Bound SAT is already
|
|
10027
|
+
* past its refresh window. Called on resume from suspension where
|
|
10028
|
+
* background-tab throttling can delay the reactive timer past the buffer.
|
|
10029
|
+
* A no-op when no token is cached or it still has headroom; the normal
|
|
10030
|
+
* {@link executeRefresh} guards (paused / in-progress / unauthenticated)
|
|
10031
|
+
* still apply.
|
|
10032
|
+
*/
|
|
10033
|
+
refreshNowIfDue() {
|
|
10034
|
+
const token = this._currentToken$.value;
|
|
10035
|
+
if (!token) return;
|
|
10036
|
+
if (resolveExpiresAt(token) * 1e3 - Date.now() <= DEVICE_TOKEN_REFRESH_BUFFER_MS) {
|
|
10037
|
+
logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
|
|
10038
|
+
this.executeRefresh();
|
|
10039
|
+
}
|
|
10040
|
+
}
|
|
10041
|
+
/**
|
|
10042
|
+
* Stops the reactive refresh pipeline from firing. Use when the underlying
|
|
10043
|
+
* session is being torn down (e.g., during {@link SignalWire.disconnect})
|
|
10044
|
+
* so a scheduled refresh cannot fire against a destroyed session.
|
|
10045
|
+
*
|
|
10046
|
+
* The manager's state (cached token, effective TTL, subscriptions) is
|
|
10047
|
+
* preserved — call {@link resume} to re-enable firing after reconnect.
|
|
10048
|
+
*/
|
|
10049
|
+
pause() {
|
|
10050
|
+
this._paused = true;
|
|
10051
|
+
}
|
|
10052
|
+
/** Re-enables the reactive refresh pipeline after a previous {@link pause}. */
|
|
10053
|
+
resume() {
|
|
10054
|
+
this._paused = false;
|
|
10055
|
+
}
|
|
9826
10056
|
/** Cleans up the manager, cancelling the reactive pipeline and all subscriptions. */
|
|
9827
10057
|
destroy() {
|
|
9828
10058
|
super.destroy();
|
|
9829
10059
|
}
|
|
9830
10060
|
};
|
|
9831
10061
|
|
|
10062
|
+
//#endregion
|
|
10063
|
+
//#region src/managers/CredentialRefreshCoordinator.ts
|
|
10064
|
+
const logger$6 = getLogger();
|
|
10065
|
+
const defaultDeviceTokenManagerFactory = (dpopManager, http, errorHandler, getCredential) => new DeviceTokenManager(dpopManager, http, errorHandler, getCredential);
|
|
10066
|
+
/**
|
|
10067
|
+
* Centralizes credential-refresh ownership across the two competing
|
|
10068
|
+
* mechanisms — developer-provided `CredentialProvider.refresh()` and the
|
|
10069
|
+
* Client Bound SAT path via {@link DeviceTokenManager}.
|
|
10070
|
+
*
|
|
10071
|
+
* Maintains the invariant: **at most one refresh mechanism is armed at a
|
|
10072
|
+
* time, and at least one is armed whenever the current credential has an
|
|
10073
|
+
* `expiry_at`**.
|
|
10074
|
+
*
|
|
10075
|
+
* Replaces the previous design where refresh state was distributed across
|
|
10076
|
+
* `SignalWire` (timer field, scheduler method, activation helper) and
|
|
10077
|
+
* `DeviceTokenManager` (reactive pipeline). Centralizing the invariant in
|
|
10078
|
+
* one component eliminates the bug class that produced issue #19074.
|
|
10079
|
+
*
|
|
10080
|
+
* Race-safety:
|
|
10081
|
+
* - `_activating` flag prevents overlapping `activate()` calls from racing.
|
|
10082
|
+
* - `_activationGeneration` lets late resolutions detect they've been
|
|
10083
|
+
* preempted by a newer activation (e.g., reconnect during in-flight
|
|
10084
|
+
* `obtainToken`).
|
|
10085
|
+
*/
|
|
10086
|
+
var CredentialRefreshCoordinator = class extends Destroyable {
|
|
10087
|
+
constructor(dpopManager, deps) {
|
|
10088
|
+
super();
|
|
10089
|
+
this.deps = deps;
|
|
10090
|
+
this._activating = false;
|
|
10091
|
+
this._activationGeneration = 0;
|
|
10092
|
+
this._developerRefreshInProgress = false;
|
|
10093
|
+
if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
|
|
10094
|
+
}
|
|
10095
|
+
/** True when the Client Bound SAT path is available (DPoP initialized). */
|
|
10096
|
+
get clientBoundSATAvailable() {
|
|
10097
|
+
return this._deviceTokenManager !== void 0;
|
|
10098
|
+
}
|
|
10099
|
+
/** True when the developer-provided refresh timer is currently armed. */
|
|
10100
|
+
get developerRefreshArmed() {
|
|
10101
|
+
return this._developerTimerId !== void 0;
|
|
10102
|
+
}
|
|
10103
|
+
/**
|
|
10104
|
+
* Arms the developer-provided refresh timer to fire shortly before
|
|
10105
|
+
* `expiresAt`. Replaces any previously scheduled developer refresh.
|
|
10106
|
+
*
|
|
10107
|
+
* Idempotent — multiple calls just reschedule. On retry exhaustion,
|
|
10108
|
+
* invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
|
|
10109
|
+
*/
|
|
10110
|
+
scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
|
|
10111
|
+
this._activeProvider = provider;
|
|
10112
|
+
if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
|
|
10113
|
+
const refreshInterval = attempt === 0 ? Math.max(expiresAt - Date.now() - CREDENTIAL_REFRESH_BUFFER_MS, 1e3) : Math.min(CREDENTIAL_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt) * (.5 + Math.random() * .5), CREDENTIAL_REFRESH_MAX_DELAY_MS);
|
|
10114
|
+
this._developerTimerId = setTimeout(() => {
|
|
10115
|
+
this._developerTimerId = void 0;
|
|
10116
|
+
this.executeDeveloperRefresh(provider, expiresAt, attempt);
|
|
10117
|
+
}, refreshInterval);
|
|
10118
|
+
}
|
|
10119
|
+
/**
|
|
10120
|
+
* Runs the developer-provided refresh once: mints a new credential, stores
|
|
10121
|
+
* and persists it, reauthenticates the live session (via the notifier), and
|
|
10122
|
+
* reschedules against the new expiry. On failure retries with backoff up to
|
|
10123
|
+
* {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
|
|
10124
|
+
*
|
|
10125
|
+
* Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
|
|
10126
|
+
* `_developerRefreshInProgress` guard prevents the two from overlapping.
|
|
10127
|
+
*/
|
|
10128
|
+
async executeDeveloperRefresh(provider, expiresAt, attempt) {
|
|
10129
|
+
if (this._developerRefreshInProgress) {
|
|
10130
|
+
logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
|
|
10131
|
+
return;
|
|
10132
|
+
}
|
|
10133
|
+
this._developerRefreshInProgress = true;
|
|
10134
|
+
try {
|
|
10135
|
+
const newCredentials = await this.refreshCredential(provider);
|
|
10136
|
+
this.deps.store.write(newCredentials);
|
|
10137
|
+
this.deps.store.persist(newCredentials);
|
|
10138
|
+
try {
|
|
10139
|
+
await this.deps.notifier.onCredentialRefreshed(newCredentials);
|
|
10140
|
+
} catch (reauthError) {
|
|
10141
|
+
logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
|
|
10142
|
+
}
|
|
10143
|
+
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
10144
|
+
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
10145
|
+
} catch (error) {
|
|
10146
|
+
const nextAttempt = attempt + 1;
|
|
10147
|
+
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
10148
|
+
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
10149
|
+
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
10150
|
+
else {
|
|
10151
|
+
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
10152
|
+
this.deps.notifier.onError(new TokenRefreshError("Credential refresh failed after max retries"));
|
|
10153
|
+
this.deps.notifier.onRefreshExhausted();
|
|
10154
|
+
}
|
|
10155
|
+
} finally {
|
|
10156
|
+
this._developerRefreshInProgress = false;
|
|
10157
|
+
}
|
|
10158
|
+
}
|
|
10159
|
+
/**
|
|
10160
|
+
* Force an immediate refresh when the current credential is already past its
|
|
10161
|
+
* scheduled refresh window. Called on resume from suspension, where
|
|
10162
|
+
* background-tab timer throttling can delay the armed refresh well past
|
|
10163
|
+
* expiry, leaving the live session stale.
|
|
10164
|
+
*
|
|
10165
|
+
* Routes to whichever mechanism is armed: the developer timer if armed,
|
|
10166
|
+
* otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
|
|
10167
|
+
*/
|
|
10168
|
+
forceRefreshIfDue() {
|
|
10169
|
+
if (this._developerTimerId !== void 0 && this._activeProvider) {
|
|
10170
|
+
const expiry = this.deps.store.read().expiry_at;
|
|
10171
|
+
if (expiry !== void 0 && Date.now() >= expiry - CREDENTIAL_REFRESH_BUFFER_MS) {
|
|
10172
|
+
logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
|
|
10173
|
+
clearTimeout(this._developerTimerId);
|
|
10174
|
+
this._developerTimerId = void 0;
|
|
10175
|
+
this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
|
|
10176
|
+
}
|
|
10177
|
+
return;
|
|
10178
|
+
}
|
|
10179
|
+
this._deviceTokenManager?.refreshNowIfDue();
|
|
10180
|
+
}
|
|
10181
|
+
/**
|
|
10182
|
+
* Sync the credential's expiry from the server-provided authorization (the
|
|
10183
|
+
* `signalwire.connect` result). SATs are opaque JWE, so
|
|
10184
|
+
* `fabric_subscriber.expires_at` is the authoritative expiry of the token
|
|
10185
|
+
* the session actually connected with — the provider-reported `expiry_at`
|
|
10186
|
+
* is only a hint (and may be wrong or absent). Corrects the stored
|
|
10187
|
+
* credential and re-arms the developer refresh timer against the real
|
|
10188
|
+
* deadline when the provider supports `refresh()`.
|
|
10189
|
+
*/
|
|
10190
|
+
syncExpiryFromAuthorization(authorization, provider) {
|
|
10191
|
+
const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
|
|
10192
|
+
if (!expiresAtSec) return;
|
|
10193
|
+
const expiryAt = expiresAtSec * 1e3;
|
|
10194
|
+
const credential = this.deps.store.read();
|
|
10195
|
+
if (credential.expiry_at === expiryAt) return;
|
|
10196
|
+
logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
|
|
10197
|
+
const updated = {
|
|
10198
|
+
...credential,
|
|
10199
|
+
expiry_at: expiryAt
|
|
10200
|
+
};
|
|
10201
|
+
this.deps.store.write(updated);
|
|
10202
|
+
this.deps.store.persist(updated);
|
|
10203
|
+
if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
|
|
10204
|
+
}
|
|
10205
|
+
/**
|
|
10206
|
+
* Invoke `provider.refresh()` deduped against any concurrent developer
|
|
10207
|
+
* refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
|
|
10208
|
+
* and the orchestrator's -32003 recovery / reconnect re-mint — share one
|
|
10209
|
+
* in-flight promise, so a provider backed by one-time-use rotating refresh
|
|
10210
|
+
* tokens is never invoked twice in parallel.
|
|
10211
|
+
*
|
|
10212
|
+
* The caller owns applying the returned credential (store write, session
|
|
10213
|
+
* reauth, rescheduling); this method only serializes the network call.
|
|
10214
|
+
*/
|
|
10215
|
+
async refreshCredential(provider) {
|
|
10216
|
+
if (this._refreshInFlight) return this._refreshInFlight;
|
|
10217
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
10218
|
+
const run = provider.refresh();
|
|
10219
|
+
this._refreshInFlight = run;
|
|
10220
|
+
const clear = () => {
|
|
10221
|
+
if (this._refreshInFlight === run) this._refreshInFlight = void 0;
|
|
10222
|
+
};
|
|
10223
|
+
run.then(clear, clear);
|
|
10224
|
+
return run;
|
|
10225
|
+
}
|
|
10226
|
+
/**
|
|
10227
|
+
* Cancels any scheduled developer-provided refresh. Idempotent.
|
|
10228
|
+
*
|
|
10229
|
+
* @internal Used by the coordinator's own activation flow. External
|
|
10230
|
+
* callers should use {@link suspend} for disconnect-time quiescence —
|
|
10231
|
+
* `suspend()` also pauses the internal Client Bound SAT pipeline.
|
|
10232
|
+
*/
|
|
10233
|
+
cancelDeveloperRefresh() {
|
|
10234
|
+
if (this._developerTimerId !== void 0) {
|
|
10235
|
+
clearTimeout(this._developerTimerId);
|
|
10236
|
+
this._developerTimerId = void 0;
|
|
10237
|
+
}
|
|
10238
|
+
}
|
|
10239
|
+
/**
|
|
10240
|
+
* Suspends both refresh paths — cancels the developer timer and pauses
|
|
10241
|
+
* the internal reactive pipeline. Use when the underlying session is
|
|
10242
|
+
* being torn down (e.g., {@link SignalWire.disconnect}). The next
|
|
10243
|
+
* {@link activate} call re-enables the internal pipeline.
|
|
10244
|
+
*
|
|
10245
|
+
* The internal manager's cached token survives — see
|
|
10246
|
+
* {@link DeviceTokenManager.pause} — so a subsequent reconnect can
|
|
10247
|
+
* skip the `/devices/token` exchange entirely.
|
|
10248
|
+
*/
|
|
10249
|
+
suspend() {
|
|
10250
|
+
this.cancelDeveloperRefresh();
|
|
10251
|
+
this._deviceTokenManager?.pause();
|
|
10252
|
+
}
|
|
10253
|
+
/**
|
|
10254
|
+
* Asks the Client Bound SAT path to take over refresh. If it accepts,
|
|
10255
|
+
* the developer-provided timer (if any) is cancelled. If it declines, the
|
|
10256
|
+
* developer timer remains armed and a `credential_refresh_fallback`
|
|
10257
|
+
* warning is emitted.
|
|
10258
|
+
*
|
|
10259
|
+
* **Idempotent** — re-entrant calls during an in-flight activation are
|
|
10260
|
+
* dropped. Use `_activationGeneration` to detect stale resolutions
|
|
10261
|
+
* (e.g., a reconnect-triggered activate() that races with an earlier one).
|
|
10262
|
+
*/
|
|
10263
|
+
async activate(user, session) {
|
|
10264
|
+
if (this._activating) {
|
|
10265
|
+
logger$6.debug("[Coordinator] activate() in flight; ignoring re-entrant call");
|
|
10266
|
+
return;
|
|
10267
|
+
}
|
|
10268
|
+
if (!this._deviceTokenManager) return;
|
|
10269
|
+
this._deviceTokenManager.resume();
|
|
10270
|
+
const generation = ++this._activationGeneration;
|
|
10271
|
+
this._activating = true;
|
|
10272
|
+
try {
|
|
10273
|
+
const result = await this.withActivationTimeout(this._deviceTokenManager.activate(user, session, (cred) => this.deps.store.merge(cred)));
|
|
10274
|
+
if (generation !== this._activationGeneration) {
|
|
10275
|
+
logger$6.debug("[Coordinator] activate() result discarded (preempted by newer activation)");
|
|
10276
|
+
return;
|
|
10277
|
+
}
|
|
10278
|
+
if (result.activated) {
|
|
10279
|
+
this.cancelDeveloperRefresh();
|
|
10280
|
+
logger$6.debug("[Coordinator] Developer refresh disabled — Client Bound SAT owns refresh");
|
|
10281
|
+
return;
|
|
10282
|
+
}
|
|
10283
|
+
logger$6.warn(`[SignalWire] [SW-REFRESH-FALLBACK] Client Bound SAT declined (reason=${result.reason}); using developer-provided refresh handler.`);
|
|
10284
|
+
this.deps.notifier.onWarning({
|
|
10285
|
+
code: "credential_refresh_fallback",
|
|
10286
|
+
source: "CredentialProvider",
|
|
10287
|
+
reason: result.reason,
|
|
10288
|
+
message: `Client Bound SAT activation declined (${result.reason}); using developer-provided refresh.`
|
|
10289
|
+
});
|
|
10290
|
+
} finally {
|
|
10291
|
+
this._activating = false;
|
|
10292
|
+
}
|
|
10293
|
+
}
|
|
10294
|
+
destroy() {
|
|
10295
|
+
this.cancelDeveloperRefresh();
|
|
10296
|
+
this._deviceTokenManager?.destroy();
|
|
10297
|
+
this._deviceTokenManager = void 0;
|
|
10298
|
+
super.destroy();
|
|
10299
|
+
}
|
|
10300
|
+
/**
|
|
10301
|
+
* Races the manager's `activate()` against a hard timeout. A wedged HTTP
|
|
10302
|
+
* layer (e.g., proxy issues) could otherwise hang the activation
|
|
10303
|
+
* indefinitely, leaving the session with no refresh mechanism while the
|
|
10304
|
+
* `_activating` guard blocks subsequent retries.
|
|
10305
|
+
*
|
|
10306
|
+
* On timeout, the manager is paused immediately. The inner `activate()`
|
|
10307
|
+
* may still complete in the background and emit to `_currentToken$`,
|
|
10308
|
+
* which would normally arm the reactive refresh pipeline; pausing
|
|
10309
|
+
* prevents that pipeline from firing while the developer refresh path
|
|
10310
|
+
* is the active mechanism — preserving the "at most one mechanism
|
|
10311
|
+
* armed" invariant. The next `activate()` call resumes the manager.
|
|
10312
|
+
*/
|
|
10313
|
+
async withActivationTimeout(inner) {
|
|
10314
|
+
return new Promise((resolve) => {
|
|
10315
|
+
const timer$1 = setTimeout(() => {
|
|
10316
|
+
this._deviceTokenManager?.pause();
|
|
10317
|
+
resolve({
|
|
10318
|
+
activated: false,
|
|
10319
|
+
reason: "activation-timeout"
|
|
10320
|
+
});
|
|
10321
|
+
}, CREDENTIAL_ACTIVATE_TIMEOUT_MS);
|
|
10322
|
+
inner.then((result) => {
|
|
10323
|
+
clearTimeout(timer$1);
|
|
10324
|
+
resolve(result);
|
|
10325
|
+
}, (error) => {
|
|
10326
|
+
clearTimeout(timer$1);
|
|
10327
|
+
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error)));
|
|
10328
|
+
resolve({
|
|
10329
|
+
activated: false,
|
|
10330
|
+
reason: "endpoint-failed"
|
|
10331
|
+
});
|
|
10332
|
+
});
|
|
10333
|
+
});
|
|
10334
|
+
}
|
|
10335
|
+
};
|
|
10336
|
+
|
|
9832
10337
|
//#endregion
|
|
9833
10338
|
//#region src/managers/DiagnosticsCollector.ts
|
|
9834
10339
|
const logger$5 = getLogger();
|
|
@@ -10346,6 +10851,10 @@ var TransportManager = class extends Destroyable {
|
|
|
10346
10851
|
if (!isSignalwireRequest(message)) return true;
|
|
10347
10852
|
const eventChannel = message.params.event_channel;
|
|
10348
10853
|
if (!eventChannel) return true;
|
|
10854
|
+
if (message.params.event_type.startsWith("conversation.")) {
|
|
10855
|
+
logger$2.debug(`[Transport] Received conversation event: ${message.params.event_type} (event_channel: ${eventChannel})`);
|
|
10856
|
+
return true;
|
|
10857
|
+
}
|
|
10349
10858
|
const currentProtocol = this._currentProtocol;
|
|
10350
10859
|
if (!currentProtocol) return true;
|
|
10351
10860
|
if (!eventChannel.includes(currentProtocol)) {
|
|
@@ -10489,6 +10998,33 @@ var TransportManager = class extends Destroyable {
|
|
|
10489
10998
|
}
|
|
10490
10999
|
};
|
|
10491
11000
|
|
|
11001
|
+
//#endregion
|
|
11002
|
+
//#region src/utils/authRecovery.ts
|
|
11003
|
+
/**
|
|
11004
|
+
* Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
|
|
11005
|
+
* Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
|
|
11006
|
+
* underlying signaling error is nested. Bounded by a visited set to guard
|
|
11007
|
+
* against cyclic causes.
|
|
11008
|
+
*/
|
|
11009
|
+
function findJSONRPCError(error) {
|
|
11010
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11011
|
+
let current = error;
|
|
11012
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
11013
|
+
seen.add(current);
|
|
11014
|
+
if (current instanceof JSONRPCError) return current;
|
|
11015
|
+
current = current.error ?? current.cause;
|
|
11016
|
+
}
|
|
11017
|
+
}
|
|
11018
|
+
/**
|
|
11019
|
+
* Whether an error is a session-recoverable authentication failure
|
|
11020
|
+
* (`-32002` authentication failed or `-32003` requester validation failed)
|
|
11021
|
+
* that a credential re-mint + retry can heal.
|
|
11022
|
+
*/
|
|
11023
|
+
function isRecoverableAuthError(error) {
|
|
11024
|
+
const rpcError = findJSONRPCError(error);
|
|
11025
|
+
return rpcError !== void 0 && (rpcError.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
11026
|
+
}
|
|
11027
|
+
|
|
10492
11028
|
//#endregion
|
|
10493
11029
|
//#region src/clients/SignalWire.ts
|
|
10494
11030
|
const logger$1 = getLogger();
|
|
@@ -10538,6 +11074,7 @@ var SignalWire = class extends Destroyable {
|
|
|
10538
11074
|
this._isConnected$ = this.createBehaviorSubject(false);
|
|
10539
11075
|
this._isRegistered$ = this.createBehaviorSubject(false);
|
|
10540
11076
|
this._errors$ = this.createReplaySubject(1);
|
|
11077
|
+
this._warnings$ = this.createReplaySubject(10);
|
|
10541
11078
|
this._options = {};
|
|
10542
11079
|
this._deps = new DependencyContainer();
|
|
10543
11080
|
this._credentialProvider = credentialProvider;
|
|
@@ -10597,6 +11134,29 @@ var SignalWire = class extends Destroyable {
|
|
|
10597
11134
|
*/
|
|
10598
11135
|
async resolveCredentials() {
|
|
10599
11136
|
const fingerprint = await this.initDPoP();
|
|
11137
|
+
this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
|
|
11138
|
+
http: this._deps.http,
|
|
11139
|
+
notifier: {
|
|
11140
|
+
onError: (error) => this._errors$.next(error),
|
|
11141
|
+
onWarning: (warning) => this._warnings$.next(warning),
|
|
11142
|
+
onRefreshExhausted: () => void this.disconnect(),
|
|
11143
|
+
onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
|
|
11144
|
+
},
|
|
11145
|
+
store: {
|
|
11146
|
+
read: () => this._deps.credential,
|
|
11147
|
+
write: (credential) => {
|
|
11148
|
+
this._deps.credential = credential;
|
|
11149
|
+
},
|
|
11150
|
+
merge: (partial) => {
|
|
11151
|
+
this._deps.credential = {
|
|
11152
|
+
...this._deps.credential,
|
|
11153
|
+
...partial
|
|
11154
|
+
};
|
|
11155
|
+
this.persistCredential(this._deps.credential);
|
|
11156
|
+
},
|
|
11157
|
+
persist: (credential) => this.persistCredential(credential)
|
|
11158
|
+
}
|
|
11159
|
+
});
|
|
10600
11160
|
if (this._credentialProvider) return this.validateCredentials(this._credentialProvider, void 0, fingerprint);
|
|
10601
11161
|
for (const scope of this._deps.persistSession ? ["local", "session"] : ["session"]) try {
|
|
10602
11162
|
const cached = await this._deps.storage.getItem("sw:cached_credential", scope);
|
|
@@ -10626,11 +11186,32 @@ var SignalWire = class extends Destroyable {
|
|
|
10626
11186
|
logger$1.error("[SignalWire] Provided credentials have expired.");
|
|
10627
11187
|
throw new InvalidCredentialsError("Provided credentials have expired.");
|
|
10628
11188
|
}
|
|
10629
|
-
if (_credentials.expiry_at && credentialProvider?.refresh) this.
|
|
11189
|
+
if (_credentials.expiry_at && credentialProvider?.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(credentialProvider, _credentials.expiry_at);
|
|
11190
|
+
else if (_credentials.expiry_at && !credentialProvider?.refresh) {
|
|
11191
|
+
logger$1.warn(`[SignalWire] [SW-NO-REFRESH-HANDLER] Credential has expiry_at=${_credentials.expiry_at} but no refresh handler. Session will terminate at expiry unless the SAT has 'sat:refresh' scope.`);
|
|
11192
|
+
this._warnings$.next({
|
|
11193
|
+
code: "credential_no_refresh_handler",
|
|
11194
|
+
source: "CredentialProvider",
|
|
11195
|
+
message: "Credential has expiry_at but no refresh handler. Session will terminate at expiry unless the SAT carries 'sat:refresh' scope.",
|
|
11196
|
+
expiresAt: _credentials.expiry_at
|
|
11197
|
+
});
|
|
11198
|
+
}
|
|
10630
11199
|
this._deps.credential = _credentials;
|
|
10631
11200
|
this.persistCredential(_credentials);
|
|
10632
|
-
|
|
10633
|
-
|
|
11201
|
+
await this.reauthenticateLiveSession(_credentials);
|
|
11202
|
+
}
|
|
11203
|
+
/**
|
|
11204
|
+
* Reauthenticate the currently-open session with a freshly obtained
|
|
11205
|
+
* credential so the new token takes effect on the live socket immediately —
|
|
11206
|
+
* not just on the next reconnect. No-op when the session is not
|
|
11207
|
+
* connected/authenticated or the credential carries no token (e.g. an
|
|
11208
|
+
* authorization-state-only refresh). Non-fatal: reauth failures surface on
|
|
11209
|
+
* `errors$` without aborting the refresh that triggered this.
|
|
11210
|
+
*/
|
|
11211
|
+
async reauthenticateLiveSession(credential) {
|
|
11212
|
+
if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
|
|
11213
|
+
try {
|
|
11214
|
+
await this._clientSession.reauthenticate(credential.token);
|
|
10634
11215
|
logger$1.info("[SignalWire] Session refreshed with new credentials.");
|
|
10635
11216
|
} catch (error) {
|
|
10636
11217
|
logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
|
|
@@ -10638,33 +11219,101 @@ var SignalWire = class extends Destroyable {
|
|
|
10638
11219
|
}
|
|
10639
11220
|
}
|
|
10640
11221
|
/**
|
|
10641
|
-
*
|
|
10642
|
-
*
|
|
10643
|
-
*
|
|
11222
|
+
* One-shot recovery for a live session that failed with a recoverable auth
|
|
11223
|
+
* error (`-32002`/`-32003`) because its token went stale — e.g. a refresh
|
|
11224
|
+
* timer that was throttled while the tab was backgrounded.
|
|
11225
|
+
*
|
|
11226
|
+
* Two steps, first that succeeds wins:
|
|
11227
|
+
* 1. Reauthenticate with the in-memory token (a fresh DPoP proof is
|
|
11228
|
+
* generated automatically). Heals a session whose token was already
|
|
11229
|
+
* refreshed by the coordinator but never applied to the open socket,
|
|
11230
|
+
* and a client-bound session whose server-side auth merely drifted.
|
|
11231
|
+
* 2. Re-mint via the developer provider's `refresh()` and reauthenticate.
|
|
11232
|
+
* Skipped for client-bound sessions (the DeviceTokenManager owns their
|
|
11233
|
+
* refresh; re-minting a base SAT here would drop the DPoP binding).
|
|
11234
|
+
*
|
|
11235
|
+
* @param allowRemint - Whether step 2 (provider re-mint) may run. Callers
|
|
11236
|
+
* pass `false` for non-auth failures so a transient network error never
|
|
11237
|
+
* escalates to a token re-mint; step 1 (in-memory reauth) always runs.
|
|
11238
|
+
* @returns `true` if the session was reauthenticated, `false` otherwise.
|
|
11239
|
+
*/
|
|
11240
|
+
async recoverStaleCredential(allowRemint = true) {
|
|
11241
|
+
const currentToken = this._deps.credential.token;
|
|
11242
|
+
if (currentToken) try {
|
|
11243
|
+
await this._clientSession.reauthenticate(currentToken);
|
|
11244
|
+
return true;
|
|
11245
|
+
} catch (error) {
|
|
11246
|
+
logger$1.debug("[SignalWire] In-memory reauth failed during recovery:", error);
|
|
11247
|
+
}
|
|
11248
|
+
if (!allowRemint || this._clientSession.clientBound || !this._credentialProvider?.refresh) return false;
|
|
11249
|
+
try {
|
|
11250
|
+
const newCredentials = await this.remintCredential(this._credentialProvider);
|
|
11251
|
+
if (!newCredentials.token) return false;
|
|
11252
|
+
this._deps.credential = newCredentials;
|
|
11253
|
+
this.persistCredential(newCredentials);
|
|
11254
|
+
await this._clientSession.reauthenticate(newCredentials.token);
|
|
11255
|
+
if (newCredentials.expiry_at) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
11256
|
+
return true;
|
|
11257
|
+
} catch (error) {
|
|
11258
|
+
logger$1.error("[SignalWire] Credential recovery failed:", error);
|
|
11259
|
+
return false;
|
|
11260
|
+
}
|
|
11261
|
+
}
|
|
11262
|
+
/**
|
|
11263
|
+
* Re-mint a credential via `provider.refresh()`, routed through the
|
|
11264
|
+
* coordinator's shared in-flight guard so concurrent re-mint paths (a
|
|
11265
|
+
* scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
|
|
11266
|
+
* second `provider.refresh()` in parallel — which rotating one-time-use
|
|
11267
|
+
* refresh tokens reject. Falls back to a direct call only if the coordinator
|
|
11268
|
+
* has not been constructed yet.
|
|
10644
11269
|
*/
|
|
10645
|
-
|
|
10646
|
-
if (this.
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
|
|
10662
|
-
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
11270
|
+
async remintCredential(provider) {
|
|
11271
|
+
if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
|
|
11272
|
+
if (!provider.refresh) throw new InvalidCredentialsError("Credential provider does not support refresh");
|
|
11273
|
+
return provider.refresh();
|
|
11274
|
+
}
|
|
11275
|
+
/**
|
|
11276
|
+
* Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
|
|
11277
|
+
* The session invokes this only when it is client-bound OR the in-memory
|
|
11278
|
+
* token is expired. The re-mint mechanism depends on the binding:
|
|
11279
|
+
* - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
|
|
11280
|
+
* fresh base SAT the upcoming reconnect can re-bind (the
|
|
11281
|
+
* DeviceTokenManager re-activates afterwards).
|
|
11282
|
+
* - Unbound: the developer's non-interactive `refresh()` handler.
|
|
11283
|
+
* `authenticate()` is deliberately NOT used here — it may be interactive
|
|
11284
|
+
* (a login prompt) and must not fire on a background reconnect.
|
|
11285
|
+
*
|
|
11286
|
+
* Rejects on failure so the session aborts the reconnect rather than
|
|
11287
|
+
* replaying a stale token.
|
|
11288
|
+
*/
|
|
11289
|
+
async refreshCredentialForReconnect() {
|
|
11290
|
+
if (!this._credentialProvider) return;
|
|
11291
|
+
try {
|
|
11292
|
+
let newCredentials;
|
|
11293
|
+
if (this._clientSession.clientBound) {
|
|
11294
|
+
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
11295
|
+
logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
|
|
11296
|
+
newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
|
|
11297
|
+
} else if (this._credentialProvider.refresh) {
|
|
11298
|
+
logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
|
|
11299
|
+
newCredentials = await this.remintCredential(this._credentialProvider);
|
|
11300
|
+
} else {
|
|
11301
|
+
logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
|
|
11302
|
+
return;
|
|
10666
11303
|
}
|
|
10667
|
-
|
|
11304
|
+
if (!newCredentials.token) {
|
|
11305
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
|
|
11306
|
+
return;
|
|
11307
|
+
}
|
|
11308
|
+
this._deps.credential = newCredentials;
|
|
11309
|
+
this.persistCredential(newCredentials);
|
|
11310
|
+
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
11311
|
+
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
11312
|
+
} catch (error) {
|
|
11313
|
+
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
11314
|
+
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
11315
|
+
throw error;
|
|
11316
|
+
}
|
|
10668
11317
|
}
|
|
10669
11318
|
/** Persist credential to localStorage when persistSession is enabled. */
|
|
10670
11319
|
persistCredential(credential) {
|
|
@@ -10752,51 +11401,20 @@ var SignalWire = class extends Destroyable {
|
|
|
10752
11401
|
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
|
|
10753
11402
|
this._clientSession = new ClientSessionManager(() => this._deps.credential, this._transport, this._deps.storage, this._deps.authorizationStateKey, this._deps.deviceController, this._attachManager, this._deps.webRTCApiProvider, this._dpopManager, this._networkMonitor?.networkChange$);
|
|
10754
11403
|
this._publicSession = new ClientSessionWrapper(this._clientSession);
|
|
10755
|
-
this._clientSession.onBeforeReconnect = async () =>
|
|
10756
|
-
if (!this._credentialProvider) return;
|
|
10757
|
-
try {
|
|
10758
|
-
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
10759
|
-
logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
|
|
10760
|
-
const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
|
|
10761
|
-
this._deps.credential = newCredentials;
|
|
10762
|
-
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
10763
|
-
} catch (error) {
|
|
10764
|
-
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
10765
|
-
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
10766
|
-
throw error;
|
|
10767
|
-
}
|
|
10768
|
-
};
|
|
11404
|
+
this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
|
|
10769
11405
|
this.subscribeTo(this._clientSession.errors$, (error) => {
|
|
10770
11406
|
this._errors$.next(error);
|
|
10771
11407
|
});
|
|
11408
|
+
this.subscribeTo(this._clientSession.authorization$, (authorization) => {
|
|
11409
|
+
this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
|
|
11410
|
+
});
|
|
10772
11411
|
await this._clientSession.connect();
|
|
10773
|
-
|
|
10774
|
-
if (this._refreshTimerId) {
|
|
10775
|
-
clearTimeout(this._refreshTimerId);
|
|
10776
|
-
this._refreshTimerId = void 0;
|
|
10777
|
-
logger$1.debug("[SignalWire] Developer refresh disabled — Client Bound SAT activation starting");
|
|
10778
|
-
}
|
|
10779
|
-
this._deviceTokenManager = new DeviceTokenManager(this._dpopManager, this._deps.http, (error) => this._errors$.next(error), () => this._deps.credential);
|
|
10780
|
-
await this._deviceTokenManager.activate(this._deps.user, this._clientSession, (cred) => {
|
|
10781
|
-
this._deps.credential = {
|
|
10782
|
-
...this._deps.credential,
|
|
10783
|
-
...cred
|
|
10784
|
-
};
|
|
10785
|
-
});
|
|
10786
|
-
}
|
|
11412
|
+
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
10787
11413
|
this.subscribeTo(this._clientSession.authenticated$.pipe(skip(1), filter(Boolean)), async () => {
|
|
10788
11414
|
try {
|
|
10789
|
-
|
|
10790
|
-
await this._deviceTokenManager.activate(this._deps.user, this._clientSession, (cred) => {
|
|
10791
|
-
this._deps.credential = {
|
|
10792
|
-
...this._deps.credential,
|
|
10793
|
-
...cred
|
|
10794
|
-
};
|
|
10795
|
-
});
|
|
10796
|
-
logger$1.debug("[SignalWire] Client Bound SAT re-activated after reconnect");
|
|
10797
|
-
}
|
|
11415
|
+
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
10798
11416
|
} catch (error) {
|
|
10799
|
-
logger$1.error("[SignalWire]
|
|
11417
|
+
logger$1.error("[SignalWire] Refresh re-arm after reconnect failed (non-fatal):", error);
|
|
10800
11418
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
10801
11419
|
}
|
|
10802
11420
|
try {
|
|
@@ -10882,6 +11500,19 @@ var SignalWire = class extends Destroyable {
|
|
|
10882
11500
|
get errors$() {
|
|
10883
11501
|
return this.deferEmission(this._errors$.asObservable());
|
|
10884
11502
|
}
|
|
11503
|
+
/**
|
|
11504
|
+
* Observable stream of non-fatal SDK warnings.
|
|
11505
|
+
*
|
|
11506
|
+
* Subscribe to detect SDK behaviors that affect session liveness or developer-facing
|
|
11507
|
+
* contracts but do not warrant disconnection — e.g., a fallback from Client Bound SAT
|
|
11508
|
+
* refresh to the developer-provided `refresh()` because the SAT lacks `sat:refresh`
|
|
11509
|
+
* scope. Discriminated by `code`.
|
|
11510
|
+
*
|
|
11511
|
+
* Independent from {@link errors$}: existing error consumers are not notified.
|
|
11512
|
+
*/
|
|
11513
|
+
get warnings$() {
|
|
11514
|
+
return this.deferEmission(this._warnings$.asObservable());
|
|
11515
|
+
}
|
|
10885
11516
|
/** Platform WebRTC capabilities detected at construction time. */
|
|
10886
11517
|
get platformCapabilities() {
|
|
10887
11518
|
this._platformCapabilities ??= detectPlatformCapabilities(this._options.webRTCApiProvider);
|
|
@@ -10939,6 +11570,13 @@ var SignalWire = class extends Destroyable {
|
|
|
10939
11570
|
}
|
|
10940
11571
|
try {
|
|
10941
11572
|
this._visibilityController = new VisibilityController();
|
|
11573
|
+
this.subscribeTo(this._visibilityController.visibilityChange$.pipe(filter((event) => event.to === "visible")), () => {
|
|
11574
|
+
try {
|
|
11575
|
+
this._refreshCoordinator?.forceRefreshIfDue();
|
|
11576
|
+
} catch (error) {
|
|
11577
|
+
logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
|
|
11578
|
+
}
|
|
11579
|
+
});
|
|
10942
11580
|
this.subscribeTo(this._visibilityController.visibilityChange$.pipe(filter((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
|
|
10943
11581
|
logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
|
|
10944
11582
|
try {
|
|
@@ -10950,7 +11588,7 @@ var SignalWire = class extends Destroyable {
|
|
|
10950
11588
|
logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
|
|
10951
11589
|
}
|
|
10952
11590
|
try {
|
|
10953
|
-
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "
|
|
11591
|
+
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.2" });
|
|
10954
11592
|
} catch (error) {
|
|
10955
11593
|
logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
|
|
10956
11594
|
}
|
|
@@ -10958,14 +11596,19 @@ var SignalWire = class extends Destroyable {
|
|
|
10958
11596
|
/**
|
|
10959
11597
|
* Disconnects the WebSocket and tears down the current session.
|
|
10960
11598
|
*
|
|
11599
|
+
* Ends the session identified by the protocol and clears its persisted
|
|
11600
|
+
* resume state (`authorization_state` + protocol) and attach records
|
|
11601
|
+
* together — a later {@link connect} with the same credentials starts a
|
|
11602
|
+
* fresh session and cannot reattach to the ended session's calls.
|
|
11603
|
+
* Credentials and device preferences are preserved. To temporarily stop
|
|
11604
|
+
* receiving inbound calls while keeping the session alive, use
|
|
11605
|
+
* `unregister()` instead.
|
|
11606
|
+
*
|
|
10961
11607
|
* The client can be reconnected by calling {@link connect} again,
|
|
10962
11608
|
* which creates a fresh transport and session.
|
|
10963
11609
|
*/
|
|
10964
11610
|
async disconnect() {
|
|
10965
|
-
|
|
10966
|
-
clearTimeout(this._refreshTimerId);
|
|
10967
|
-
this._refreshTimerId = void 0;
|
|
10968
|
-
}
|
|
11611
|
+
this._refreshCoordinator?.suspend();
|
|
10969
11612
|
this._diagnosticsCollector?.record("connection", "disconnected");
|
|
10970
11613
|
await this.teardownTransportAndSession();
|
|
10971
11614
|
this._isConnected$.next(false);
|
|
@@ -11016,21 +11659,23 @@ var SignalWire = class extends Destroyable {
|
|
|
11016
11659
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
11017
11660
|
throw error;
|
|
11018
11661
|
}
|
|
11019
|
-
logger$1.debug("[SignalWire] Failed to register user,
|
|
11020
|
-
|
|
11021
|
-
|
|
11022
|
-
logger$1.debug("[SignalWire]
|
|
11662
|
+
logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
|
|
11663
|
+
let failureCause = error;
|
|
11664
|
+
if (await this.recoverStaleCredential(isRecoverableAuthError(error))) try {
|
|
11665
|
+
logger$1.debug("[SignalWire] Recovery succeeded, retrying register()");
|
|
11023
11666
|
await this._transport.execute(RPCExecute({
|
|
11024
11667
|
method: "subscriber.online",
|
|
11025
11668
|
params: {}
|
|
11026
11669
|
}));
|
|
11027
11670
|
this._isRegistered$.next(true);
|
|
11028
|
-
|
|
11029
|
-
|
|
11030
|
-
|
|
11031
|
-
|
|
11032
|
-
throw registerError;
|
|
11671
|
+
return;
|
|
11672
|
+
} catch (retryError) {
|
|
11673
|
+
logger$1.error("[SignalWire] Register retry after recovery failed:", retryError);
|
|
11674
|
+
failureCause = retryError;
|
|
11033
11675
|
}
|
|
11676
|
+
const registerError = new InvalidCredentialsError("Failed to register user, and credential recovery also failed. Please check your credentials.", { cause: failureCause instanceof Error ? failureCause : new Error(String(failureCause), { cause: failureCause }) });
|
|
11677
|
+
this._errors$.next(registerError);
|
|
11678
|
+
throw registerError;
|
|
11034
11679
|
}
|
|
11035
11680
|
}
|
|
11036
11681
|
/**
|
|
@@ -11083,7 +11728,15 @@ var SignalWire = class extends Destroyable {
|
|
|
11083
11728
|
};
|
|
11084
11729
|
await this.waitAuthentication();
|
|
11085
11730
|
logger$1.debug("[SignalWire] Dialing with options:", computed_options);
|
|
11086
|
-
|
|
11731
|
+
try {
|
|
11732
|
+
return await this._clientSession.createOutboundCall(destination, computed_options);
|
|
11733
|
+
} catch (error) {
|
|
11734
|
+
if (!isRecoverableAuthError(error)) throw error;
|
|
11735
|
+
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering credential and retrying once");
|
|
11736
|
+
if (!await this.recoverStaleCredential()) throw error;
|
|
11737
|
+
await this.waitAuthentication();
|
|
11738
|
+
return this._clientSession.createOutboundCall(destination, computed_options);
|
|
11739
|
+
}
|
|
11087
11740
|
}
|
|
11088
11741
|
/**
|
|
11089
11742
|
* Runs a multi-phase connectivity test against the given destination.
|
|
@@ -11352,17 +12005,23 @@ var SignalWire = class extends Destroyable {
|
|
|
11352
12005
|
prefs.preferredVideoInput = null;
|
|
11353
12006
|
await this._deviceController.clearDeviceState();
|
|
11354
12007
|
}
|
|
11355
|
-
/**
|
|
12008
|
+
/**
|
|
12009
|
+
* Destroys the client, clearing timers and releasing all resources.
|
|
12010
|
+
*
|
|
12011
|
+
* Intentionally destroying the client ends its session: the resume state
|
|
12012
|
+
* (`authorization_state` + protocol) and the attach records are both
|
|
12013
|
+
* cleared. Credentials and device preferences are preserved — use
|
|
12014
|
+
* {@link resetToDefaults} for a full wipe. To temporarily stop receiving
|
|
12015
|
+
* inbound calls while keeping the session alive, use `unregister()`.
|
|
12016
|
+
*/
|
|
11356
12017
|
destroy() {
|
|
11357
|
-
|
|
11358
|
-
|
|
11359
|
-
this._refreshTimerId = void 0;
|
|
11360
|
-
}
|
|
11361
|
-
this._deviceTokenManager?.destroy();
|
|
12018
|
+
this._refreshCoordinator?.destroy();
|
|
12019
|
+
this._refreshCoordinator = void 0;
|
|
11362
12020
|
this._dpopManager?.destroy();
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
this.
|
|
12021
|
+
const session = this._clientSession;
|
|
12022
|
+
session?.teardownSessionState();
|
|
12023
|
+
this._transport?.destroy();
|
|
12024
|
+
session?.destroy();
|
|
11366
12025
|
try {
|
|
11367
12026
|
this._networkMonitor?.destroy();
|
|
11368
12027
|
} catch {}
|
|
@@ -11474,7 +12133,7 @@ var StaticCredentialProvider = class {
|
|
|
11474
12133
|
/**
|
|
11475
12134
|
* Library version from package.json, injected at build time.
|
|
11476
12135
|
*/
|
|
11477
|
-
const version = "
|
|
12136
|
+
const version = "4.0.0-rc.2";
|
|
11478
12137
|
/**
|
|
11479
12138
|
* Flag indicating the library has been loaded and is ready to use.
|
|
11480
12139
|
* For UMD builds: `window.SignalWire.ready`
|
|
@@ -11496,12 +12155,12 @@ const ready = true;
|
|
|
11496
12155
|
*/
|
|
11497
12156
|
const emitReadyEvent = () => {
|
|
11498
12157
|
if (typeof window !== "undefined") {
|
|
11499
|
-
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "
|
|
12158
|
+
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.2" } });
|
|
11500
12159
|
window.dispatchEvent(event);
|
|
11501
12160
|
}
|
|
11502
12161
|
};
|
|
11503
12162
|
emitReadyEvent();
|
|
11504
12163
|
|
|
11505
12164
|
//#endregion
|
|
11506
|
-
export { Address, CallCreateError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, PreflightError, RecoveryError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
|
|
12165
|
+
export { Address, CallCreateError, CallNotReadyError, ClientPreferences, CollectionFetchError, DPoPInitError, DeviceTokenError, EmbedTokenCredentialProvider, InvalidCredentialsError, MediaAccessError, MediaTrackError, MessageParseError, OverconstrainedFallbackError, Participant, ParticipantNotReadyError, PreflightError, RecoveryError, SelfCapabilities, SelfParticipant, SignalWire, StaticCredentialProvider, TokenRefreshError, UnexpectedError, User, VertoPongError, WebRTCCall, embeddableCall, getLogger, isSelfParticipant, ready, setDebugOptions, setLogLevel, setLogger, version };
|
|
11507
12166
|
//# sourceMappingURL=index.mjs.map
|