@signalwire/js 4.0.0-rc.1 → 4.0.0-rc.3
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 +1905 -586
- package/dist/browser.mjs.map +1 -1
- package/dist/browser.umd.js +1910 -585
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +1714 -642
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2360 -1817
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2360 -1817
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1637 -571
- package/dist/index.mjs.map +1 -1
- package/dist/operators/index.cjs +1 -1
- package/dist/operators/index.mjs +1 -1
- package/dist/operators-BJ6QW1VP.cjs +1466 -0
- package/dist/operators-BJ6QW1VP.cjs.map +1 -0
- package/dist/operators-BVxi3KpN.mjs +731 -0
- package/dist/operators-BVxi3KpN.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/operators-CX_lCCJm.mjs +0 -478
- package/dist/operators-CX_lCCJm.mjs.map +0 -1
- package/dist/operators-D6a2J1KA.cjs +0 -763
- package/dist/operators-D6a2J1KA.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_operators = require('./operators-
|
|
1
|
+
const require_operators = require('./operators-BJ6QW1VP.cjs');
|
|
2
2
|
let jwt_decode = require("jwt-decode");
|
|
3
3
|
let rxjs = require("rxjs");
|
|
4
4
|
let uuid = require("uuid");
|
|
@@ -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$32 = require_operators.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$32.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$4 = request.timeout ?? this.requestTimeout;
|
|
225
|
-
logger$
|
|
225
|
+
logger$32.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$32.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 require_operators.RequestTimeoutError(`Request timeout after ${timeout$4}ms`, { cause: error });
|
|
255
|
-
logger$
|
|
255
|
+
logger$32.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$32.debug("[HTTPRequestController] Using Bearer token auth, token length:", credential.token.length);
|
|
270
|
+
} else logger$32.warn("[HTTPRequestController] No credentials available for authentication");
|
|
271
271
|
return headers;
|
|
272
272
|
}
|
|
273
273
|
/**
|
|
@@ -404,137 +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
|
-
/**
|
|
441
|
-
* Maximum time the coordinator will wait for `DeviceTokenManager.activate()`
|
|
442
|
-
* to resolve before treating the activation as failed and falling back to
|
|
443
|
-
* the developer-provided refresh path. Prevents a wedged HTTP layer from
|
|
444
|
-
* leaving the session with no active refresh mechanism.
|
|
445
|
-
*/
|
|
446
|
-
const CREDENTIAL_ACTIVATE_TIMEOUT_MS = 3e4;
|
|
447
|
-
/** JSON-RPC error code for requester validation failure (corrupted auth state). */
|
|
448
|
-
const RPC_ERROR_REQUESTER_VALIDATION_FAILED = -32003;
|
|
449
|
-
/** JSON-RPC error code for invalid params (e.g., missing authentication block). */
|
|
450
|
-
const RPC_ERROR_INVALID_PARAMS = -32602;
|
|
451
|
-
/** JSON-RPC error code for authentication failure (invalid token, missing DPoP, etc.). */
|
|
452
|
-
const RPC_ERROR_AUTHENTICATION_FAILED = -32002;
|
|
453
|
-
/** Default polling interval for RTCPeerConnection.getStats() in milliseconds. */
|
|
454
|
-
const DEFAULT_STATS_POLLING_INTERVAL_MS = 1e3;
|
|
455
|
-
/** Number of initial samples used to build a baseline for spike detection. */
|
|
456
|
-
const DEFAULT_STATS_BASELINE_SAMPLES = 10;
|
|
457
|
-
/** Duration in ms with no inbound audio packets before emitting a critical issue. */
|
|
458
|
-
const DEFAULT_STATS_NO_PACKET_THRESHOLD_MS = 2e3;
|
|
459
|
-
/** Multiplier applied to baseline RTT to detect a warning-level RTT spike. */
|
|
460
|
-
const DEFAULT_STATS_RTT_SPIKE_MULTIPLIER = 3;
|
|
461
|
-
/** Packet loss fraction (0-1) above which a warning is emitted. */
|
|
462
|
-
const DEFAULT_STATS_PACKET_LOSS_THRESHOLD = .05;
|
|
463
|
-
/** Multiplier applied to baseline jitter to detect a jitter spike. */
|
|
464
|
-
const DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER = 4;
|
|
465
|
-
/** Number of seconds of metrics history to retain. */
|
|
466
|
-
const DEFAULT_STATS_HISTORY_SIZE = 30;
|
|
467
|
-
/** Maximum keyframe requests allowed within a single burst window. */
|
|
468
|
-
const DEFAULT_KEYFRAME_MAX_BURST$1 = 3;
|
|
469
|
-
/** Duration of the keyframe burst window in milliseconds. */
|
|
470
|
-
const DEFAULT_KEYFRAME_BURST_WINDOW_MS$1 = 3e3;
|
|
471
|
-
/** Cooldown period in ms after burst limit is reached before allowing more keyframes. */
|
|
472
|
-
const DEFAULT_KEYFRAME_COOLDOWN_MS$1 = 1e4;
|
|
473
|
-
/** Minimum time between re-INVITE attempts in milliseconds. */
|
|
474
|
-
const DEFAULT_REINVITE_DEBOUNCE_TIME_MS = 1e4;
|
|
475
|
-
/** Maximum number of re-INVITE attempts per call. */
|
|
476
|
-
const DEFAULT_REINVITE_MAX_ATTEMPTS = 3;
|
|
477
|
-
/** Timeout for a single re-INVITE attempt in milliseconds. */
|
|
478
|
-
const DEFAULT_REINVITE_TIMEOUT_MS = 5e3;
|
|
479
|
-
/** Debounce window in ms to collapse multiple detection signals into one trigger. */
|
|
480
|
-
const DEFAULT_RECOVERY_DEBOUNCE_TIME_MS = 2e3;
|
|
481
|
-
/** Cooldown period in ms between recovery attempts. */
|
|
482
|
-
const DEFAULT_RECOVERY_COOLDOWN_MS = 1e4;
|
|
483
|
-
/** Grace period in ms before treating ICE 'disconnected' as a failure. */
|
|
484
|
-
const DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS = 3e3;
|
|
485
|
-
/** Timeout for a single ICE restart attempt in milliseconds. */
|
|
486
|
-
const DEFAULT_ICE_RESTART_TIMEOUT_MS$1 = 5e3;
|
|
487
|
-
/** Maximum recovery attempts before emitting 'max_attempts_reached'. */
|
|
488
|
-
const DEFAULT_MAX_RECOVERY_ATTEMPTS = 3;
|
|
489
|
-
/** Upper bound in ms for waiting on iceGatheringState === 'complete' after an ICE restart. */
|
|
490
|
-
const ICE_GATHERING_COMPLETE_TIMEOUT_MS = 1e4;
|
|
491
|
-
/** Upper bound in ms for waiting on RTCPeerConnection.connectionState === 'connected' after a recovery ICE restart. */
|
|
492
|
-
const PEER_CONNECTION_RECOVERY_WAIT_MS = 5e3;
|
|
493
|
-
/** Polling interval in ms while waiting for RTCPeerConnection.connectionState to transition. */
|
|
494
|
-
const PEER_CONNECTION_RECOVERY_POLL_MS = 100;
|
|
495
|
-
/** Polling interval for LocalAudioPipeline.level$ (ms). ~30fps is smooth for meters. */
|
|
496
|
-
const AUDIO_LEVEL_POLL_INTERVAL_MS = 33;
|
|
497
|
-
/** RMS level threshold (0..1) above which the local participant is considered speaking. */
|
|
498
|
-
const VAD_THRESHOLD = .03;
|
|
499
|
-
/** Hold window in ms below the threshold before speaking$ flips back to false. */
|
|
500
|
-
const VAD_HOLD_MS = 250;
|
|
501
|
-
/** Whether to persist device selections to storage by default. */
|
|
502
|
-
const DEFAULT_PERSIST_DEVICE_SELECTION = true;
|
|
503
|
-
/** Whether to auto-apply device changes to active calls by default. */
|
|
504
|
-
const DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS = true;
|
|
505
|
-
/** Storage keys for persisted device selections. */
|
|
506
|
-
const DEVICE_STORAGE_KEY_AUDIO_INPUT = "sw:device:audioinput";
|
|
507
|
-
const DEVICE_STORAGE_KEY_AUDIO_OUTPUT = "sw:device:audiooutput";
|
|
508
|
-
const DEVICE_STORAGE_KEY_VIDEO_INPUT = "sw:device:videoinput";
|
|
509
|
-
/** Whether to auto-mute video when the tab becomes hidden. */
|
|
510
|
-
const DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN = false;
|
|
511
|
-
/** Whether to re-enumerate devices when the page becomes visible. */
|
|
512
|
-
const DEFAULT_REFRESH_DEVICES_ON_VISIBLE = true;
|
|
513
|
-
/** Whether to check peer connection health when the page becomes visible. */
|
|
514
|
-
const DEFAULT_CHECK_CONNECTION_ON_VISIBLE = true;
|
|
515
|
-
/** Whether automatic video degradation on low bandwidth is enabled. */
|
|
516
|
-
const DEFAULT_ENABLE_AUTO_DEGRADATION = true;
|
|
517
|
-
/** Bitrate in kbps below which video is automatically disabled. */
|
|
518
|
-
const DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS = 150;
|
|
519
|
-
/** Bitrate in kbps above which video is automatically re-enabled (hysteresis). */
|
|
520
|
-
const DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS = 300;
|
|
521
|
-
/** Whether relay-only escalation is enabled as a last-resort recovery tier. */
|
|
522
|
-
const DEFAULT_ENABLE_RELAY_FALLBACK = true;
|
|
523
|
-
/** Whether to listen for browser online/offline/connection events. */
|
|
524
|
-
const DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION = true;
|
|
525
|
-
/** Whether to intercept server-sent media-timeout hangups and attempt recovery. */
|
|
526
|
-
const DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION = true;
|
|
527
|
-
/** Default video track constraints applied when video is enabled without explicit constraints. */
|
|
528
|
-
const DEFAULT_VIDEO_CONSTRAINTS = {
|
|
529
|
-
width: { ideal: 1280 },
|
|
530
|
-
height: { ideal: 720 },
|
|
531
|
-
aspectRatio: 16 / 9
|
|
532
|
-
};
|
|
533
|
-
/** Whether stereo Opus is enabled by default. */
|
|
534
|
-
const DEFAULT_STEREO_AUDIO = false;
|
|
535
|
-
/** Max average bitrate for stereo Opus in bits per second. */
|
|
536
|
-
const DEFAULT_STEREO_MAX_AVERAGE_BITRATE = 51e4;
|
|
537
|
-
|
|
538
407
|
//#endregion
|
|
539
408
|
//#region src/utils/time.ts
|
|
540
409
|
function fromSecToMs(seconds) {
|
|
@@ -546,23 +415,23 @@ function fromMsToSec(milliseconds) {
|
|
|
546
415
|
|
|
547
416
|
//#endregion
|
|
548
417
|
//#region src/containers/PreferencesContainer.ts
|
|
549
|
-
const logger$
|
|
418
|
+
const logger$31 = require_operators.getLogger();
|
|
550
419
|
var PreferencesContainer = class PreferencesContainer {
|
|
551
420
|
static get instance() {
|
|
552
421
|
this._instance ??= new PreferencesContainer();
|
|
553
422
|
return this._instance;
|
|
554
423
|
}
|
|
555
424
|
constructor() {
|
|
556
|
-
this.deviceDebounceTime = DEFAULT_DEVICE_DEBOUNCE_TIME_MS;
|
|
557
|
-
this.devicePollingInterval = DEFAULT_DEVICE_POLLING_INTERVAL_MS;
|
|
558
|
-
this.reconnectCallsTimeout = DEFAULT_RECONNECT_CALLS_TIMEOUT_MS;
|
|
559
|
-
this.connectionTimeout = DEFAULT_CONNECTION_TIMEOUT_MS;
|
|
560
|
-
this.reconnectDelayMin = DEFAULT_RECONNECT_DELAY_MIN_MS;
|
|
561
|
-
this.reconnectDelayMax = DEFAULT_RECONNECT_DELAY_MAX_MS;
|
|
425
|
+
this.deviceDebounceTime = require_operators.DEFAULT_DEVICE_DEBOUNCE_TIME_MS;
|
|
426
|
+
this.devicePollingInterval = require_operators.DEFAULT_DEVICE_POLLING_INTERVAL_MS;
|
|
427
|
+
this.reconnectCallsTimeout = require_operators.DEFAULT_RECONNECT_CALLS_TIMEOUT_MS;
|
|
428
|
+
this.connectionTimeout = require_operators.DEFAULT_CONNECTION_TIMEOUT_MS;
|
|
429
|
+
this.reconnectDelayMin = require_operators.DEFAULT_RECONNECT_DELAY_MIN_MS;
|
|
430
|
+
this.reconnectDelayMax = require_operators.DEFAULT_RECONNECT_DELAY_MAX_MS;
|
|
562
431
|
this.disableUdpIceServers = false;
|
|
563
432
|
this.relayOnly = false;
|
|
564
|
-
this.iceCandidateTimeout = DEFAULT_ICE_CANDIDATE_TIMEOUT_MS;
|
|
565
|
-
this.iceGatheringTimeout = DEFAULT_ICE_GATHERING_TIMEOUT_MS;
|
|
433
|
+
this.iceCandidateTimeout = require_operators.DEFAULT_ICE_CANDIDATE_TIMEOUT_MS;
|
|
434
|
+
this.iceGatheringTimeout = require_operators.DEFAULT_ICE_GATHERING_TIMEOUT_MS;
|
|
566
435
|
this.defaultSignalWireOptions = {
|
|
567
436
|
skipConnection: false,
|
|
568
437
|
skipRegister: false,
|
|
@@ -589,38 +458,38 @@ var PreferencesContainer = class PreferencesContainer {
|
|
|
589
458
|
"call.joined"
|
|
590
459
|
];
|
|
591
460
|
this.userVariables = {};
|
|
592
|
-
this.statsPollingInterval = DEFAULT_STATS_POLLING_INTERVAL_MS;
|
|
593
|
-
this.statsBaselineSamples = DEFAULT_STATS_BASELINE_SAMPLES;
|
|
594
|
-
this.statsNoPacketThreshold = DEFAULT_STATS_NO_PACKET_THRESHOLD_MS;
|
|
595
|
-
this.statsRttSpikeMultiplier = DEFAULT_STATS_RTT_SPIKE_MULTIPLIER;
|
|
596
|
-
this.statsPacketLossThreshold = DEFAULT_STATS_PACKET_LOSS_THRESHOLD;
|
|
597
|
-
this.statsJitterSpikeMultiplier = DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER;
|
|
598
|
-
this.statsHistorySize = DEFAULT_STATS_HISTORY_SIZE;
|
|
599
|
-
this.keyframeMaxBurst = DEFAULT_KEYFRAME_MAX_BURST
|
|
600
|
-
this.keyframeBurstWindow = DEFAULT_KEYFRAME_BURST_WINDOW_MS
|
|
601
|
-
this.keyframeCooldown = DEFAULT_KEYFRAME_COOLDOWN_MS
|
|
602
|
-
this.reinviteDebounceTime = DEFAULT_REINVITE_DEBOUNCE_TIME_MS;
|
|
603
|
-
this.reinviteMaxAttempts = DEFAULT_REINVITE_MAX_ATTEMPTS;
|
|
604
|
-
this.reinviteTimeout = DEFAULT_REINVITE_TIMEOUT_MS;
|
|
605
|
-
this.recoveryDebounceTime = DEFAULT_RECOVERY_DEBOUNCE_TIME_MS;
|
|
606
|
-
this.recoveryCooldown = DEFAULT_RECOVERY_COOLDOWN_MS;
|
|
607
|
-
this.iceDisconnectedGracePeriod = DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS;
|
|
608
|
-
this.iceRestartTimeout = DEFAULT_ICE_RESTART_TIMEOUT_MS
|
|
609
|
-
this.maxRecoveryAttempts = DEFAULT_MAX_RECOVERY_ATTEMPTS;
|
|
610
|
-
this.enableRelayFallback = DEFAULT_ENABLE_RELAY_FALLBACK;
|
|
611
|
-
this.enableNetworkChangeDetection = DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION;
|
|
612
|
-
this.enableServerHangupInterception = DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION;
|
|
613
|
-
this.persistDeviceSelection = DEFAULT_PERSIST_DEVICE_SELECTION;
|
|
614
|
-
this.syncDevicesToActiveCalls = DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS;
|
|
615
|
-
this.autoMuteVideoOnHidden = DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN;
|
|
616
|
-
this.refreshDevicesOnVisible = DEFAULT_REFRESH_DEVICES_ON_VISIBLE;
|
|
617
|
-
this.checkConnectionOnVisible = DEFAULT_CHECK_CONNECTION_ON_VISIBLE;
|
|
461
|
+
this.statsPollingInterval = require_operators.DEFAULT_STATS_POLLING_INTERVAL_MS;
|
|
462
|
+
this.statsBaselineSamples = require_operators.DEFAULT_STATS_BASELINE_SAMPLES;
|
|
463
|
+
this.statsNoPacketThreshold = require_operators.DEFAULT_STATS_NO_PACKET_THRESHOLD_MS;
|
|
464
|
+
this.statsRttSpikeMultiplier = require_operators.DEFAULT_STATS_RTT_SPIKE_MULTIPLIER;
|
|
465
|
+
this.statsPacketLossThreshold = require_operators.DEFAULT_STATS_PACKET_LOSS_THRESHOLD;
|
|
466
|
+
this.statsJitterSpikeMultiplier = require_operators.DEFAULT_STATS_JITTER_SPIKE_MULTIPLIER;
|
|
467
|
+
this.statsHistorySize = require_operators.DEFAULT_STATS_HISTORY_SIZE;
|
|
468
|
+
this.keyframeMaxBurst = require_operators.DEFAULT_KEYFRAME_MAX_BURST;
|
|
469
|
+
this.keyframeBurstWindow = require_operators.DEFAULT_KEYFRAME_BURST_WINDOW_MS;
|
|
470
|
+
this.keyframeCooldown = require_operators.DEFAULT_KEYFRAME_COOLDOWN_MS;
|
|
471
|
+
this.reinviteDebounceTime = require_operators.DEFAULT_REINVITE_DEBOUNCE_TIME_MS;
|
|
472
|
+
this.reinviteMaxAttempts = require_operators.DEFAULT_REINVITE_MAX_ATTEMPTS;
|
|
473
|
+
this.reinviteTimeout = require_operators.DEFAULT_REINVITE_TIMEOUT_MS;
|
|
474
|
+
this.recoveryDebounceTime = require_operators.DEFAULT_RECOVERY_DEBOUNCE_TIME_MS;
|
|
475
|
+
this.recoveryCooldown = require_operators.DEFAULT_RECOVERY_COOLDOWN_MS;
|
|
476
|
+
this.iceDisconnectedGracePeriod = require_operators.DEFAULT_ICE_DISCONNECTED_GRACE_PERIOD_MS;
|
|
477
|
+
this.iceRestartTimeout = require_operators.DEFAULT_ICE_RESTART_TIMEOUT_MS;
|
|
478
|
+
this.maxRecoveryAttempts = require_operators.DEFAULT_MAX_RECOVERY_ATTEMPTS;
|
|
479
|
+
this.enableRelayFallback = require_operators.DEFAULT_ENABLE_RELAY_FALLBACK;
|
|
480
|
+
this.enableNetworkChangeDetection = require_operators.DEFAULT_ENABLE_NETWORK_CHANGE_DETECTION;
|
|
481
|
+
this.enableServerHangupInterception = require_operators.DEFAULT_ENABLE_SERVER_HANGUP_INTERCEPTION;
|
|
482
|
+
this.persistDeviceSelection = require_operators.DEFAULT_PERSIST_DEVICE_SELECTION;
|
|
483
|
+
this.syncDevicesToActiveCalls = require_operators.DEFAULT_SYNC_DEVICES_TO_ACTIVE_CALLS;
|
|
484
|
+
this.autoMuteVideoOnHidden = require_operators.DEFAULT_AUTO_MUTE_VIDEO_ON_HIDDEN;
|
|
485
|
+
this.refreshDevicesOnVisible = require_operators.DEFAULT_REFRESH_DEVICES_ON_VISIBLE;
|
|
486
|
+
this.checkConnectionOnVisible = require_operators.DEFAULT_CHECK_CONNECTION_ON_VISIBLE;
|
|
618
487
|
this.defaultAudioConstraints = void 0;
|
|
619
488
|
this.defaultVideoConstraints = void 0;
|
|
620
|
-
this.stereoAudio = DEFAULT_STEREO_AUDIO;
|
|
621
|
-
this.enableAutoDegradation = DEFAULT_ENABLE_AUTO_DEGRADATION;
|
|
622
|
-
this.degradationBitrateThreshold = DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS;
|
|
623
|
-
this.degradationRecoveryThreshold = DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS;
|
|
489
|
+
this.stereoAudio = require_operators.DEFAULT_STEREO_AUDIO;
|
|
490
|
+
this.enableAutoDegradation = require_operators.DEFAULT_ENABLE_AUTO_DEGRADATION;
|
|
491
|
+
this.degradationBitrateThreshold = require_operators.DEFAULT_DEGRADATION_BITRATE_THRESHOLD_KBPS;
|
|
492
|
+
this.degradationRecoveryThreshold = require_operators.DEFAULT_DEGRADATION_RECOVERY_THRESHOLD_KBPS;
|
|
624
493
|
this.preferredVideoCodecs = [];
|
|
625
494
|
this.preferredAudioCodecs = [];
|
|
626
495
|
}
|
|
@@ -1207,17 +1076,17 @@ var ClientPreferences = class {
|
|
|
1207
1076
|
_saveToStorage() {
|
|
1208
1077
|
if (!this._storage) return;
|
|
1209
1078
|
const data = collectStoredPreferences();
|
|
1210
|
-
this._storage.setItem(PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
|
|
1211
|
-
logger$
|
|
1079
|
+
this._storage.setItem(require_operators.PREFERENCES_STORAGE_KEY, data, "local").catch((error) => {
|
|
1080
|
+
logger$31.error(`[ClientPreferences] Failed to save preferences: ${String(error)}`);
|
|
1212
1081
|
});
|
|
1213
1082
|
}
|
|
1214
1083
|
/** Loads preferences from storage and applies them to the container. */
|
|
1215
1084
|
_loadFromStorage() {
|
|
1216
1085
|
if (!this._storage) return;
|
|
1217
|
-
this._storage.getItem(PREFERENCES_STORAGE_KEY, "local").then((stored) => {
|
|
1086
|
+
this._storage.getItem(require_operators.PREFERENCES_STORAGE_KEY, "local").then((stored) => {
|
|
1218
1087
|
if (stored) applyStoredPreferences(stored);
|
|
1219
1088
|
}).catch((error) => {
|
|
1220
|
-
logger$
|
|
1089
|
+
logger$31.error(`[ClientPreferences] Failed to load preferences: ${String(error)}`);
|
|
1221
1090
|
});
|
|
1222
1091
|
}
|
|
1223
1092
|
};
|
|
@@ -1238,12 +1107,12 @@ function toError(value) {
|
|
|
1238
1107
|
|
|
1239
1108
|
//#endregion
|
|
1240
1109
|
//#region src/controllers/NavigatorDeviceController.ts
|
|
1241
|
-
const logger$
|
|
1110
|
+
const logger$30 = require_operators.getLogger();
|
|
1242
1111
|
/** Maps a device kind to its storage key. */
|
|
1243
1112
|
const DEVICE_STORAGE_KEYS = {
|
|
1244
|
-
audioinput: DEVICE_STORAGE_KEY_AUDIO_INPUT,
|
|
1245
|
-
audiooutput: DEVICE_STORAGE_KEY_AUDIO_OUTPUT,
|
|
1246
|
-
videoinput: DEVICE_STORAGE_KEY_VIDEO_INPUT
|
|
1113
|
+
audioinput: require_operators.DEVICE_STORAGE_KEY_AUDIO_INPUT,
|
|
1114
|
+
audiooutput: require_operators.DEVICE_STORAGE_KEY_AUDIO_OUTPUT,
|
|
1115
|
+
videoinput: require_operators.DEVICE_STORAGE_KEY_VIDEO_INPUT
|
|
1247
1116
|
};
|
|
1248
1117
|
const initialDevicesState = {
|
|
1249
1118
|
audioinput: [],
|
|
@@ -1260,7 +1129,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1260
1129
|
super();
|
|
1261
1130
|
this.webRTCApiProvider = webRTCApiProvider;
|
|
1262
1131
|
this.deviceChangeHandler = () => {
|
|
1263
|
-
logger$
|
|
1132
|
+
logger$30.debug("[DeviceController] Device change detected");
|
|
1264
1133
|
this.enumerateDevices();
|
|
1265
1134
|
};
|
|
1266
1135
|
this._devicesState$ = this.createBehaviorSubject(initialDevicesState);
|
|
@@ -1325,13 +1194,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1325
1194
|
return this.cachedObservable("videoInputDevices$", () => this._devicesState$.pipe((0, rxjs.map)((state) => state.videoinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$)));
|
|
1326
1195
|
}
|
|
1327
1196
|
get selectedAudioInputDevice$() {
|
|
1328
|
-
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.audioinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$
|
|
1197
|
+
return this.cachedObservable("selectedAudioInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.audioinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$30.debug("[DeviceController] Selected audio input device changed:", info))));
|
|
1329
1198
|
}
|
|
1330
1199
|
get selectedAudioOutputDevice$() {
|
|
1331
|
-
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.audiooutput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$
|
|
1200
|
+
return this.cachedObservable("selectedAudioOutputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.audiooutput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$30.debug("[DeviceController] Selected audio output device changed:", info))));
|
|
1332
1201
|
}
|
|
1333
1202
|
get selectedVideoInputDevice$() {
|
|
1334
|
-
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.videoinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$
|
|
1203
|
+
return this.cachedObservable("selectedVideoInputDevice$", () => this._selectedDevicesState$.asObservable().pipe((0, rxjs.map)((state) => state.videoinput), (0, rxjs.distinctUntilChanged)(), (0, rxjs.takeUntil)(this.destroyed$), (0, rxjs.tap)((info) => logger$30.debug("[DeviceController] Selected video input device changed:", info))));
|
|
1335
1204
|
}
|
|
1336
1205
|
get selectedAudioInputDevice() {
|
|
1337
1206
|
if (this._audioInputDisabled$.value) return null;
|
|
@@ -1406,7 +1275,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1406
1275
|
if (device) this.persistDeviceSelection("audioinput", device);
|
|
1407
1276
|
}
|
|
1408
1277
|
selectVideoInputDevice(device) {
|
|
1409
|
-
logger$
|
|
1278
|
+
logger$30.debug("[DeviceController] Setting selected video input device:", device);
|
|
1410
1279
|
if (this._videoInputDisabled$.value && device) this._videoInputDisabled$.next(false);
|
|
1411
1280
|
const previous = this._selectedDevicesState$.value.videoinput;
|
|
1412
1281
|
if (previous && previous.deviceId !== device?.deviceId) this._deviceHistory.push("videoinput", previous);
|
|
@@ -1463,7 +1332,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1463
1332
|
}
|
|
1464
1333
|
const fromHistory = this._deviceHistory.findInHistory(kind, devices);
|
|
1465
1334
|
if (fromHistory) {
|
|
1466
|
-
logger$
|
|
1335
|
+
logger$30.debug(`[DeviceController] Device disappeared, falling back to history: ${fromHistory.label}`);
|
|
1467
1336
|
this.emitDeviceRecovered(kind, selected, fromHistory, "device_disconnected");
|
|
1468
1337
|
return fromHistory;
|
|
1469
1338
|
}
|
|
@@ -1516,7 +1385,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1516
1385
|
try {
|
|
1517
1386
|
await this._storageManager.setItem(DEVICE_STORAGE_KEYS[kind], stored, "local");
|
|
1518
1387
|
} catch (error) {
|
|
1519
|
-
logger$
|
|
1388
|
+
logger$30.error(`[DeviceController] Failed to persist device selection for ${kind}:`, error);
|
|
1520
1389
|
}
|
|
1521
1390
|
}
|
|
1522
1391
|
async loadPersistedDevices() {
|
|
@@ -1532,7 +1401,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1532
1401
|
[kind]: stored
|
|
1533
1402
|
};
|
|
1534
1403
|
} catch (error) {
|
|
1535
|
-
logger$
|
|
1404
|
+
logger$30.error(`[DeviceController] Failed to load persisted device for ${kind}:`, error);
|
|
1536
1405
|
}
|
|
1537
1406
|
}
|
|
1538
1407
|
/** Clears device history, persisted selections, and re-enumerates devices. */
|
|
@@ -1550,7 +1419,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1550
1419
|
this.disableDeviceMonitoring();
|
|
1551
1420
|
this.webRTCApiProvider.mediaDevices.addEventListener("devicechange", this.deviceChangeHandler);
|
|
1552
1421
|
if (PreferencesContainer.instance.devicePollingInterval > 0) this._devicesPoolingSubscription = (0, rxjs.interval)(PreferencesContainer.instance.devicePollingInterval).subscribe(() => {
|
|
1553
|
-
logger$
|
|
1422
|
+
logger$30.debug("[DeviceController] Polling devices due to interval");
|
|
1554
1423
|
this.enumerateDevices();
|
|
1555
1424
|
});
|
|
1556
1425
|
this.enumerateDevices();
|
|
@@ -1576,13 +1445,13 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1576
1445
|
videoinput: []
|
|
1577
1446
|
});
|
|
1578
1447
|
this._devicesState$.next(devicesByKind);
|
|
1579
|
-
logger$
|
|
1448
|
+
logger$30.debug("[DeviceController] Devices enumerated:", {
|
|
1580
1449
|
audioInputs: devicesByKind.audioinput.length,
|
|
1581
1450
|
audioOutputs: devicesByKind.audiooutput.length,
|
|
1582
1451
|
videoInputs: devicesByKind.videoinput.length
|
|
1583
1452
|
});
|
|
1584
1453
|
} catch (error) {
|
|
1585
|
-
logger$
|
|
1454
|
+
logger$30.error("[DeviceController] Failed to enumerate devices:", error);
|
|
1586
1455
|
this._errors$.next(toError(error));
|
|
1587
1456
|
}
|
|
1588
1457
|
}
|
|
@@ -1598,7 +1467,7 @@ var NavigatorDeviceController = class extends Destroyable {
|
|
|
1598
1467
|
stream.getTracks().forEach((t) => t.stop());
|
|
1599
1468
|
return capabilities;
|
|
1600
1469
|
} catch (error) {
|
|
1601
|
-
logger$
|
|
1470
|
+
logger$30.error("[DeviceController] Failed to get device capabilities:", error);
|
|
1602
1471
|
this._errors$.next(toError(error));
|
|
1603
1472
|
throw error;
|
|
1604
1473
|
}
|
|
@@ -1849,7 +1718,7 @@ var DependencyContainer = class {
|
|
|
1849
1718
|
|
|
1850
1719
|
//#endregion
|
|
1851
1720
|
//#region src/controllers/CryptoController.ts
|
|
1852
|
-
const logger$
|
|
1721
|
+
const logger$29 = require_operators.getLogger();
|
|
1853
1722
|
const DPOP_DB_NAME = "sw-dpop";
|
|
1854
1723
|
const DPOP_DB_VERSION = 1;
|
|
1855
1724
|
const DPOP_STORE_NAME = "keys";
|
|
@@ -1908,7 +1777,7 @@ async function loadKeyPairFromDB() {
|
|
|
1908
1777
|
tx.oncomplete = () => db.close();
|
|
1909
1778
|
});
|
|
1910
1779
|
} catch (error) {
|
|
1911
|
-
logger$
|
|
1780
|
+
logger$29.warn("[DPoP] Failed to load key pair from IndexedDB:", error);
|
|
1912
1781
|
return null;
|
|
1913
1782
|
}
|
|
1914
1783
|
}
|
|
@@ -1928,7 +1797,7 @@ async function saveKeyPairToDB(keyPair) {
|
|
|
1928
1797
|
};
|
|
1929
1798
|
});
|
|
1930
1799
|
} catch (error) {
|
|
1931
|
-
logger$
|
|
1800
|
+
logger$29.warn("[DPoP] Failed to save key pair to IndexedDB:", error);
|
|
1932
1801
|
}
|
|
1933
1802
|
}
|
|
1934
1803
|
async function deleteKeyPairFromDB() {
|
|
@@ -1947,7 +1816,7 @@ async function deleteKeyPairFromDB() {
|
|
|
1947
1816
|
};
|
|
1948
1817
|
});
|
|
1949
1818
|
} catch (error) {
|
|
1950
|
-
logger$
|
|
1819
|
+
logger$29.warn("[DPoP] Failed to delete key pair from IndexedDB:", error);
|
|
1951
1820
|
}
|
|
1952
1821
|
}
|
|
1953
1822
|
/**
|
|
@@ -2007,13 +1876,13 @@ var CryptoController = class {
|
|
|
2007
1876
|
this._publicJwk = await crypto.subtle.exportKey("jwk", stored.publicKey);
|
|
2008
1877
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
2009
1878
|
this._initialized = true;
|
|
2010
|
-
logger$
|
|
1879
|
+
logger$29.debug("[DPoP] Key pair restored from IndexedDB, fingerprint:", this._fingerprint);
|
|
2011
1880
|
return this._fingerprint;
|
|
2012
1881
|
} catch (error) {
|
|
2013
|
-
logger$
|
|
1882
|
+
logger$29.warn("[DPoP] Stored key pair unusable, generating new one:", error);
|
|
2014
1883
|
await deleteKeyPairFromDB();
|
|
2015
1884
|
}
|
|
2016
|
-
logger$
|
|
1885
|
+
logger$29.debug("[DPoP] Generating RSA key pair");
|
|
2017
1886
|
this._keyPair = await crypto.subtle.generateKey({
|
|
2018
1887
|
name: "RSASSA-PKCS1-v1_5",
|
|
2019
1888
|
modulusLength: 2048,
|
|
@@ -2028,7 +1897,7 @@ var CryptoController = class {
|
|
|
2028
1897
|
this._fingerprint = await computeJwkThumbprint(this._publicJwk);
|
|
2029
1898
|
this._initialized = true;
|
|
2030
1899
|
await saveKeyPairToDB(this._keyPair);
|
|
2031
|
-
logger$
|
|
1900
|
+
logger$29.debug("[DPoP] Key pair generated and persisted, fingerprint:", this._fingerprint);
|
|
2032
1901
|
return this._fingerprint;
|
|
2033
1902
|
}
|
|
2034
1903
|
/**
|
|
@@ -2094,7 +1963,7 @@ var CryptoController = class {
|
|
|
2094
1963
|
this._fingerprint = null;
|
|
2095
1964
|
this._initialized = false;
|
|
2096
1965
|
deleteKeyPairFromDB();
|
|
2097
|
-
logger$
|
|
1966
|
+
logger$29.debug("[DPoP] Controller destroyed");
|
|
2098
1967
|
}
|
|
2099
1968
|
get publicJwk() {
|
|
2100
1969
|
if (!this._publicJwk) throw new require_operators.DPoPInitError("CryptoController not initialized. Call init() first.");
|
|
@@ -2117,7 +1986,7 @@ var CryptoController = class {
|
|
|
2117
1986
|
|
|
2118
1987
|
//#endregion
|
|
2119
1988
|
//#region src/controllers/NetworkMonitor.ts
|
|
2120
|
-
const logger$
|
|
1989
|
+
const logger$28 = require_operators.getLogger();
|
|
2121
1990
|
/**
|
|
2122
1991
|
* Safely check whether we are running in a browser environment
|
|
2123
1992
|
* with `window` and the relevant event targets.
|
|
@@ -2174,7 +2043,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
2174
2043
|
}
|
|
2175
2044
|
attachListeners() {
|
|
2176
2045
|
if (!hasBrowserNetworkEvents()) {
|
|
2177
|
-
logger$
|
|
2046
|
+
logger$28.debug("NetworkMonitor: no browser environment detected, skipping event listeners");
|
|
2178
2047
|
return;
|
|
2179
2048
|
}
|
|
2180
2049
|
window.addEventListener("online", this._onOnline);
|
|
@@ -2182,7 +2051,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
2182
2051
|
const connection = getNetworkConnection();
|
|
2183
2052
|
if (connection) connection.addEventListener("change", this._onConnectionChange);
|
|
2184
2053
|
this._listenersAttached = true;
|
|
2185
|
-
logger$
|
|
2054
|
+
logger$28.debug("NetworkMonitor: event listeners attached");
|
|
2186
2055
|
}
|
|
2187
2056
|
removeListeners() {
|
|
2188
2057
|
if (!this._listenersAttached) return;
|
|
@@ -2193,10 +2062,10 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
2193
2062
|
if (connection) connection.removeEventListener("change", this._onConnectionChange);
|
|
2194
2063
|
}
|
|
2195
2064
|
this._listenersAttached = false;
|
|
2196
|
-
logger$
|
|
2065
|
+
logger$28.debug("NetworkMonitor: event listeners removed");
|
|
2197
2066
|
}
|
|
2198
2067
|
handleOnline() {
|
|
2199
|
-
logger$
|
|
2068
|
+
logger$28.info("NetworkMonitor: browser went online");
|
|
2200
2069
|
this._isOnline$.next(true);
|
|
2201
2070
|
this._networkChange$.next({
|
|
2202
2071
|
type: "online",
|
|
@@ -2205,7 +2074,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
2205
2074
|
});
|
|
2206
2075
|
}
|
|
2207
2076
|
handleOffline() {
|
|
2208
|
-
logger$
|
|
2077
|
+
logger$28.info("NetworkMonitor: browser went offline");
|
|
2209
2078
|
this._isOnline$.next(false);
|
|
2210
2079
|
this._networkChange$.next({
|
|
2211
2080
|
type: "offline",
|
|
@@ -2214,7 +2083,7 @@ var NetworkMonitor = class extends Destroyable {
|
|
|
2214
2083
|
}
|
|
2215
2084
|
handleConnectionChange() {
|
|
2216
2085
|
const networkType = getNetworkType();
|
|
2217
|
-
logger$
|
|
2086
|
+
logger$28.info(`NetworkMonitor: connection changed — effectiveType=${networkType ?? "unknown"}`);
|
|
2218
2087
|
this._networkChange$.next({
|
|
2219
2088
|
type: "connection_change",
|
|
2220
2089
|
timestamp: Date.now(),
|
|
@@ -2329,7 +2198,7 @@ function getNavigatorMediaDevices() {
|
|
|
2329
2198
|
|
|
2330
2199
|
//#endregion
|
|
2331
2200
|
//#region src/controllers/PreflightRunner.ts
|
|
2332
|
-
const logger$
|
|
2201
|
+
const logger$27 = require_operators.getLogger();
|
|
2333
2202
|
const DEFAULT_MEDIA_TEST_DURATION_S = 10;
|
|
2334
2203
|
const ICE_GATHERING_TIMEOUT_MS = 1e4;
|
|
2335
2204
|
const SIGNALING_RTT_TIMEOUT_MS = 5e3;
|
|
@@ -2378,7 +2247,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
2378
2247
|
if (!this._options.skipMediaTest) try {
|
|
2379
2248
|
bandwidth = await this.testMediaBandwidth(destination);
|
|
2380
2249
|
} catch (error) {
|
|
2381
|
-
logger$
|
|
2250
|
+
logger$27.warn("[PreflightRunner] Media bandwidth test failed:", error);
|
|
2382
2251
|
warnings.push("Media bandwidth test failed");
|
|
2383
2252
|
}
|
|
2384
2253
|
return {
|
|
@@ -2390,7 +2259,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
2390
2259
|
warnings
|
|
2391
2260
|
};
|
|
2392
2261
|
} catch (error) {
|
|
2393
|
-
logger$
|
|
2262
|
+
logger$27.error("[PreflightRunner] Preflight test failed:", error);
|
|
2394
2263
|
throw new require_operators.PreflightError("preflight", error instanceof Error ? error : new Error(String(error)));
|
|
2395
2264
|
} finally {
|
|
2396
2265
|
this.destroy();
|
|
@@ -2421,7 +2290,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
2421
2290
|
if (track.kind === "video" && track.readyState === "live") videoWorking = true;
|
|
2422
2291
|
}
|
|
2423
2292
|
} catch (error) {
|
|
2424
|
-
logger$
|
|
2293
|
+
logger$27.warn("[PreflightRunner] Device test failed:", error);
|
|
2425
2294
|
} finally {
|
|
2426
2295
|
if (audioStream) audioStream.getTracks().forEach((t) => t.stop());
|
|
2427
2296
|
}
|
|
@@ -2479,7 +2348,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
2479
2348
|
rttMs
|
|
2480
2349
|
};
|
|
2481
2350
|
} catch (error) {
|
|
2482
|
-
logger$
|
|
2351
|
+
logger$27.warn("[PreflightRunner] ICE connectivity test failed:", error);
|
|
2483
2352
|
return {
|
|
2484
2353
|
type: "failed",
|
|
2485
2354
|
turnReachable: false,
|
|
@@ -2526,7 +2395,7 @@ var PreflightRunner = class extends Destroyable {
|
|
|
2526
2395
|
|
|
2527
2396
|
//#endregion
|
|
2528
2397
|
//#region src/controllers/VisibilityController.ts
|
|
2529
|
-
const logger$
|
|
2398
|
+
const logger$26 = require_operators.getLogger();
|
|
2530
2399
|
/**
|
|
2531
2400
|
* Checks whether the document visibility API is available.
|
|
2532
2401
|
*/
|
|
@@ -2563,8 +2432,8 @@ var VisibilityController = class extends Destroyable {
|
|
|
2563
2432
|
this._boundHandler = this._handleVisibilityChange.bind(this);
|
|
2564
2433
|
if (this._hasVisibilityApi) {
|
|
2565
2434
|
document.addEventListener("visibilitychange", this._boundHandler);
|
|
2566
|
-
logger$
|
|
2567
|
-
} else logger$
|
|
2435
|
+
logger$26.debug("VisibilityController: listening for visibilitychange events");
|
|
2436
|
+
} else logger$26.debug("VisibilityController: document visibility API not available, defaulting to visible");
|
|
2568
2437
|
}
|
|
2569
2438
|
/**
|
|
2570
2439
|
* Observable of the current visibility state.
|
|
@@ -2589,7 +2458,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
2589
2458
|
destroy() {
|
|
2590
2459
|
if (this._hasVisibilityApi) {
|
|
2591
2460
|
document.removeEventListener("visibilitychange", this._boundHandler);
|
|
2592
|
-
logger$
|
|
2461
|
+
logger$26.debug("VisibilityController: removed visibilitychange listener");
|
|
2593
2462
|
}
|
|
2594
2463
|
super.destroy();
|
|
2595
2464
|
}
|
|
@@ -2607,7 +2476,7 @@ var VisibilityController = class extends Destroyable {
|
|
|
2607
2476
|
timestamp: Date.now()
|
|
2608
2477
|
};
|
|
2609
2478
|
this._visibilityChange$.next(changeEvent);
|
|
2610
|
-
logger$
|
|
2479
|
+
logger$26.debug("VisibilityController: visibility changed", {
|
|
2611
2480
|
from: previousState,
|
|
2612
2481
|
to: newState
|
|
2613
2482
|
});
|
|
@@ -2808,15 +2677,57 @@ const RPCEventAckResponse = (id) => makeRPCResponse({
|
|
|
2808
2677
|
result: {}
|
|
2809
2678
|
});
|
|
2810
2679
|
|
|
2680
|
+
//#endregion
|
|
2681
|
+
//#region src/utils/authRecovery.ts
|
|
2682
|
+
/**
|
|
2683
|
+
* Walk an error's `error`/`cause` chain looking for a {@link JSONRPCError}.
|
|
2684
|
+
* Errors thrown by call creation are wrapped (e.g. `CallCreateError`), so the
|
|
2685
|
+
* underlying signaling error is nested. Bounded by a visited set to guard
|
|
2686
|
+
* against cyclic causes.
|
|
2687
|
+
*/
|
|
2688
|
+
function findJSONRPCError(error) {
|
|
2689
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2690
|
+
let current = error;
|
|
2691
|
+
while (current instanceof Error && !seen.has(current)) {
|
|
2692
|
+
seen.add(current);
|
|
2693
|
+
if (current instanceof require_operators.JSONRPCError) return current;
|
|
2694
|
+
current = current.error ?? current.cause;
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
/**
|
|
2698
|
+
* Whether an error is a session-recoverable authentication failure
|
|
2699
|
+
* (`-32002` authentication failed or `-32003` requester validation failed)
|
|
2700
|
+
* that a credential re-mint + retry can heal.
|
|
2701
|
+
*/
|
|
2702
|
+
function isRecoverableAuthError(error) {
|
|
2703
|
+
const rpcError = findJSONRPCError(error);
|
|
2704
|
+
return rpcError !== void 0 && (rpcError.code === require_operators.RPC_ERROR_REQUESTER_VALIDATION_FAILED || rpcError.code === require_operators.RPC_ERROR_AUTHENTICATION_FAILED);
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* Whether an error is specifically a requester-validation rejection
|
|
2708
|
+
* (`-32003`) — the server refusing the session's credential.
|
|
2709
|
+
*
|
|
2710
|
+
* Narrower than {@link isRecoverableAuthError} on purpose. `-32002` is
|
|
2711
|
+
* overloaded server-side: a rejected reattach arrives as `-32002` with
|
|
2712
|
+
* `cause: INVALID_MSG_UNSPECIFIED` and message `CALL ERROR`, which is a
|
|
2713
|
+
* call-level rejection and says nothing about the credential. Use this where
|
|
2714
|
+
* the decision must not be fooled by that, such as deciding whether retrying
|
|
2715
|
+
* an operation could possibly succeed.
|
|
2716
|
+
*/
|
|
2717
|
+
function isRequesterValidationError(error) {
|
|
2718
|
+
return findJSONRPCError(error)?.code === require_operators.RPC_ERROR_REQUESTER_VALIDATION_FAILED;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2811
2721
|
//#endregion
|
|
2812
2722
|
//#region src/managers/AttachManager.ts
|
|
2813
|
-
const logger$
|
|
2723
|
+
const logger$25 = require_operators.getLogger();
|
|
2814
2724
|
var AttachManager = class {
|
|
2815
|
-
constructor(storage, deviceController, reconnectCallsTimeout, attachKey) {
|
|
2725
|
+
constructor(storage, deviceController, reconnectCallsTimeout, attachKey, credentialRecovered) {
|
|
2816
2726
|
this.storage = storage;
|
|
2817
2727
|
this.deviceController = deviceController;
|
|
2818
2728
|
this.reconnectCallsTimeout = reconnectCallsTimeout;
|
|
2819
2729
|
this.attachKey = attachKey;
|
|
2730
|
+
this.credentialRecovered = credentialRecovered;
|
|
2820
2731
|
this.writeQueue = Promise.resolve();
|
|
2821
2732
|
}
|
|
2822
2733
|
async detachAll() {
|
|
@@ -2831,7 +2742,7 @@ var AttachManager = class {
|
|
|
2831
2742
|
try {
|
|
2832
2743
|
return await this.storage.getItem(this.attachKey) ?? {};
|
|
2833
2744
|
} catch (error) {
|
|
2834
|
-
logger$
|
|
2745
|
+
logger$25.warn("[AttachManager] Failed to retrieve attached calls from storage", error);
|
|
2835
2746
|
return {};
|
|
2836
2747
|
}
|
|
2837
2748
|
}
|
|
@@ -2839,7 +2750,7 @@ var AttachManager = class {
|
|
|
2839
2750
|
try {
|
|
2840
2751
|
await this.storage.setItem(this.attachKey, attached);
|
|
2841
2752
|
} catch (error) {
|
|
2842
|
-
logger$
|
|
2753
|
+
logger$25.warn("[AttachManager] Failed to write attached calls to storage", error);
|
|
2843
2754
|
}
|
|
2844
2755
|
}
|
|
2845
2756
|
/**
|
|
@@ -2858,11 +2769,39 @@ var AttachManager = class {
|
|
|
2858
2769
|
}
|
|
2859
2770
|
async attach(call) {
|
|
2860
2771
|
if (!call.to) {
|
|
2861
|
-
logger$
|
|
2772
|
+
logger$25.warn("[AttachManager] Skip attach for calls with no destination");
|
|
2862
2773
|
return;
|
|
2863
2774
|
}
|
|
2775
|
+
const attachment = this.buildAttachment(call, call.to);
|
|
2776
|
+
await this.mutate((attached) => ({
|
|
2777
|
+
...attached,
|
|
2778
|
+
[call.id]: attachment
|
|
2779
|
+
}));
|
|
2780
|
+
}
|
|
2781
|
+
/**
|
|
2782
|
+
* Keep an already-stored call's reference alive and current — the periodic
|
|
2783
|
+
* refresh the `verto.ping` keepalive drives.
|
|
2784
|
+
*
|
|
2785
|
+
* Only ever updates: a call with no record is one nothing wants reattached,
|
|
2786
|
+
* and re-creating it here would undo a `detach`. That matters because a ping
|
|
2787
|
+
* can land in the window between `bye()` detaching and the call being torn
|
|
2788
|
+
* down, and a record revived there survives the hangup — so the next page
|
|
2789
|
+
* load dials a call nobody is on. The existence check and the write share
|
|
2790
|
+
* one {@link mutate} turn, so a concurrent detach cannot slip between them.
|
|
2791
|
+
*/
|
|
2792
|
+
async refresh(call) {
|
|
2793
|
+
if (!call.to) return;
|
|
2864
2794
|
const destination = call.to;
|
|
2865
|
-
|
|
2795
|
+
await this.mutate((attached) => {
|
|
2796
|
+
if (!Object.hasOwn(attached, call.id)) return attached;
|
|
2797
|
+
return {
|
|
2798
|
+
...attached,
|
|
2799
|
+
[call.id]: this.buildAttachment(call, destination)
|
|
2800
|
+
};
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
buildAttachment(call, destination) {
|
|
2804
|
+
return {
|
|
2866
2805
|
nodeId: call.nodeId,
|
|
2867
2806
|
destination,
|
|
2868
2807
|
mediaDirections: call.mediaDirections,
|
|
@@ -2870,10 +2809,6 @@ var AttachManager = class {
|
|
|
2870
2809
|
videoInputDevice: call.mediaDirections.video !== "inactive" ? this.deviceController.selectedVideoInputDevice : null,
|
|
2871
2810
|
attachedAt: Date.now()
|
|
2872
2811
|
};
|
|
2873
|
-
await this.mutate((attached) => ({
|
|
2874
|
-
...attached,
|
|
2875
|
-
[call.id]: attachment
|
|
2876
|
-
}));
|
|
2877
2812
|
}
|
|
2878
2813
|
async detach(call) {
|
|
2879
2814
|
await this.mutate((attached) => {
|
|
@@ -2896,8 +2831,14 @@ var AttachManager = class {
|
|
|
2896
2831
|
* rejecting. Once that fix is deployed, this will work for both
|
|
2897
2832
|
* page reloads and WebSocket reconnects.
|
|
2898
2833
|
*
|
|
2899
|
-
*
|
|
2900
|
-
*
|
|
2834
|
+
* A failed reattach does NOT generally cost the stored reference. It is
|
|
2835
|
+
* discarded only when the server denied the reattach on a session whose
|
|
2836
|
+
* credential it had already accepted — a verified reauthentication followed
|
|
2837
|
+
* by a refusal is the server saying the call is gone, and that is the one
|
|
2838
|
+
* refusal worth acting on. Until then the credential may be what is being
|
|
2839
|
+
* refused, and the record is the only way a later reload can try again;
|
|
2840
|
+
* keeping it costs nothing, since `detachExpired` reaps it once it is older
|
|
2841
|
+
* than `reconnectCallsTimeout`.
|
|
2901
2842
|
*/
|
|
2902
2843
|
async reattachCalls() {
|
|
2903
2844
|
const attached = await this.readAttached();
|
|
@@ -2906,25 +2847,31 @@ var AttachManager = class {
|
|
|
2906
2847
|
const { destination } = attachment;
|
|
2907
2848
|
const options = this.buildCallOptions(attachment);
|
|
2908
2849
|
let succeeded = false;
|
|
2850
|
+
let refusedOnCredentials = false;
|
|
2909
2851
|
for (let attempt = 1; attempt <= 3; attempt++) try {
|
|
2910
2852
|
await this.session.createOutboundCall(destination, {
|
|
2911
2853
|
callId,
|
|
2912
2854
|
...options
|
|
2913
2855
|
});
|
|
2914
|
-
logger$
|
|
2856
|
+
logger$25.info(`[AttachManager] Reattached call ${callId} (attempt ${attempt})`);
|
|
2915
2857
|
succeeded = true;
|
|
2916
2858
|
break;
|
|
2917
2859
|
} catch (error) {
|
|
2918
|
-
logger$
|
|
2860
|
+
logger$25.warn(`[AttachManager] Reattach attempt ${attempt}/3 failed for call ${callId}:`, error);
|
|
2861
|
+
if (isRequesterValidationError(error)) {
|
|
2862
|
+
refusedOnCredentials = true;
|
|
2863
|
+
logger$25.warn(`[AttachManager] Reattach of ${callId} was refused on credentials; not retrying.`);
|
|
2864
|
+
break;
|
|
2865
|
+
}
|
|
2919
2866
|
if (attempt < 3) await new Promise((r) => setTimeout(r, (attempt + 1) * 1e3));
|
|
2920
2867
|
}
|
|
2921
|
-
if (!succeeded) {
|
|
2922
|
-
logger$
|
|
2868
|
+
if (!succeeded) if (this.credentialRecovered() && !refusedOnCredentials) {
|
|
2869
|
+
logger$25.warn(`[AttachManager] Reattach of ${callId} was denied after a verified reauthentication, removing reference`);
|
|
2923
2870
|
await this.detach({
|
|
2924
2871
|
id: callId,
|
|
2925
2872
|
mediaDirections: attachment.mediaDirections
|
|
2926
2873
|
});
|
|
2927
|
-
}
|
|
2874
|
+
} else logger$25.warn(`[AttachManager] Reattach failed for call ${callId}; keeping the reference (credential refused or never proven good)`);
|
|
2928
2875
|
}
|
|
2929
2876
|
}
|
|
2930
2877
|
/**
|
|
@@ -3237,7 +3184,7 @@ function toggleHandraiseMethod(is) {
|
|
|
3237
3184
|
|
|
3238
3185
|
//#endregion
|
|
3239
3186
|
//#region src/core/entities/Participant.ts
|
|
3240
|
-
const logger$
|
|
3187
|
+
const logger$24 = require_operators.getLogger();
|
|
3241
3188
|
const initialState = {};
|
|
3242
3189
|
/**
|
|
3243
3190
|
* Represents a participant in a call.
|
|
@@ -3247,9 +3194,9 @@ const initialState = {};
|
|
|
3247
3194
|
* the local participant with additional device control.
|
|
3248
3195
|
*/
|
|
3249
3196
|
var Participant = class extends Destroyable {
|
|
3250
|
-
constructor(id,
|
|
3197
|
+
constructor(id, callExecuteMethod, deviceController) {
|
|
3251
3198
|
super();
|
|
3252
|
-
this.
|
|
3199
|
+
this.callExecuteMethod = callExecuteMethod;
|
|
3253
3200
|
this.deviceController = deviceController;
|
|
3254
3201
|
this._state$ = this.createBehaviorSubject(initialState);
|
|
3255
3202
|
this.id = id;
|
|
@@ -3471,22 +3418,55 @@ var Participant = class extends Destroyable {
|
|
|
3471
3418
|
get value() {
|
|
3472
3419
|
return this._state$.value;
|
|
3473
3420
|
}
|
|
3421
|
+
/**
|
|
3422
|
+
* Target triple for member RPCs, built from the participant's own state.
|
|
3423
|
+
* The backend locates the member's session by the target `call_id`/`node_id`,
|
|
3424
|
+
* so this must always be the participant's own call context — never the
|
|
3425
|
+
* local call's id (issue #19400).
|
|
3426
|
+
*
|
|
3427
|
+
* Reading it doubles as a readiness probe: it throws until the first full
|
|
3428
|
+
* member event (`member.joined`/`member.updated` or the `call.joined`
|
|
3429
|
+
* roster) arrives, and never regresses afterwards.
|
|
3430
|
+
*
|
|
3431
|
+
* @throws {ParticipantNotReadyError} If the member state has not been
|
|
3432
|
+
* received yet (e.g. a participant first seen via `member.talking`) — an
|
|
3433
|
+
* empty call context can never address the member, so fail fast instead of
|
|
3434
|
+
* sending a doomed RPC.
|
|
3435
|
+
*/
|
|
3436
|
+
get target() {
|
|
3437
|
+
const { call_id, node_id } = this._state$.value;
|
|
3438
|
+
if (!call_id || !node_id) throw new require_operators.ParticipantNotReadyError(this.id);
|
|
3439
|
+
return {
|
|
3440
|
+
member_id: this.id,
|
|
3441
|
+
call_id,
|
|
3442
|
+
node_id
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
/**
|
|
3446
|
+
* Executes a member RPC against this participant, injecting its own
|
|
3447
|
+
* {@link target} as the target.
|
|
3448
|
+
*
|
|
3449
|
+
* @throws {ParticipantNotReadyError} Via {@link target}, when the
|
|
3450
|
+
* member state has not been received yet.
|
|
3451
|
+
*/
|
|
3452
|
+
async executeMethod(method, args) {
|
|
3453
|
+
return this.callExecuteMethod(this.target, method, args);
|
|
3454
|
+
}
|
|
3474
3455
|
/** Toggles the deafened state (mutes/unmutes incoming audio). */
|
|
3475
3456
|
async toggleDeaf() {
|
|
3476
|
-
|
|
3477
|
-
await this.executeMethod(this.id, method, {});
|
|
3457
|
+
await this.executeMethod(toggleDeafMethod(this.deaf), {});
|
|
3478
3458
|
}
|
|
3479
3459
|
/** Toggles the hand-raised state. */
|
|
3480
3460
|
async toggleHandraise() {
|
|
3481
|
-
await this.executeMethod(
|
|
3461
|
+
await this.executeMethod(toggleHandraiseMethod(this.handraised), {});
|
|
3482
3462
|
}
|
|
3483
3463
|
/** Mutes the participant's audio. */
|
|
3484
3464
|
async mute() {
|
|
3485
|
-
await this.executeMethod(
|
|
3465
|
+
await this.executeMethod("call.mute", { channels: ["audio"] });
|
|
3486
3466
|
}
|
|
3487
3467
|
/** Unmutes the participant's audio. */
|
|
3488
3468
|
async unmute() {
|
|
3489
|
-
await this.executeMethod(
|
|
3469
|
+
await this.executeMethod("call.unmute", { channels: ["audio"] });
|
|
3490
3470
|
}
|
|
3491
3471
|
/** Toggles the participant's audio mute state. */
|
|
3492
3472
|
async toggleMute() {
|
|
@@ -3494,11 +3474,11 @@ var Participant = class extends Destroyable {
|
|
|
3494
3474
|
}
|
|
3495
3475
|
/** Mutes the participant's video. */
|
|
3496
3476
|
async muteVideo() {
|
|
3497
|
-
await this.executeMethod(
|
|
3477
|
+
await this.executeMethod("call.mute", { channels: ["video"] });
|
|
3498
3478
|
}
|
|
3499
3479
|
/** Unmutes the participant's video. */
|
|
3500
3480
|
async unmuteVideo() {
|
|
3501
|
-
await this.executeMethod(
|
|
3481
|
+
await this.executeMethod("call.unmute", { channels: ["video"] });
|
|
3502
3482
|
}
|
|
3503
3483
|
/** Toggles the participant's video mute state. */
|
|
3504
3484
|
async toggleMuteVideo() {
|
|
@@ -3506,7 +3486,7 @@ var Participant = class extends Destroyable {
|
|
|
3506
3486
|
}
|
|
3507
3487
|
/** Toggles echo cancellation on the audio input. */
|
|
3508
3488
|
async toggleEchoCancellation() {
|
|
3509
|
-
await this.executeMethod(
|
|
3489
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3510
3490
|
echo_cancellation: !this.echoCancellation,
|
|
3511
3491
|
auto_gain: this.autoGain,
|
|
3512
3492
|
noise_suppression: this.noiseSuppression
|
|
@@ -3514,7 +3494,7 @@ var Participant = class extends Destroyable {
|
|
|
3514
3494
|
}
|
|
3515
3495
|
/** Toggles automatic gain control on the audio input. */
|
|
3516
3496
|
async toggleAudioInputAutoGain() {
|
|
3517
|
-
await this.executeMethod(
|
|
3497
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3518
3498
|
echo_cancellation: this.echoCancellation,
|
|
3519
3499
|
auto_gain: !this.autoGain,
|
|
3520
3500
|
noise_suppression: this.noiseSuppression
|
|
@@ -3522,7 +3502,7 @@ var Participant = class extends Destroyable {
|
|
|
3522
3502
|
}
|
|
3523
3503
|
/** Toggles noise suppression on the audio input. */
|
|
3524
3504
|
async toggleNoiseSuppression() {
|
|
3525
|
-
await this.executeMethod(
|
|
3505
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3526
3506
|
echo_cancellation: this.echoCancellation,
|
|
3527
3507
|
auto_gain: this.autoGain,
|
|
3528
3508
|
noise_suppression: !this.noiseSuppression
|
|
@@ -3530,7 +3510,7 @@ var Participant = class extends Destroyable {
|
|
|
3530
3510
|
}
|
|
3531
3511
|
/** Toggles low-bitrate mode for this participant's media. */
|
|
3532
3512
|
async toggleLowbitrate() {
|
|
3533
|
-
await this.executeMethod(
|
|
3513
|
+
await this.executeMethod("call.lowbitrate.set", { lowbitrate: !this.lowbitrate });
|
|
3534
3514
|
}
|
|
3535
3515
|
/**
|
|
3536
3516
|
* Adjusts the **conference-only** microphone energy gate / sensitivity level
|
|
@@ -3546,7 +3526,7 @@ var Participant = class extends Destroyable {
|
|
|
3546
3526
|
* (integer, larger values are more sensitive).
|
|
3547
3527
|
*/
|
|
3548
3528
|
async setAudioInputSensitivity(value) {
|
|
3549
|
-
await this.executeMethod(
|
|
3529
|
+
await this.executeMethod("call.microphone.sensitivity.set", { sensitivity: value });
|
|
3550
3530
|
}
|
|
3551
3531
|
/**
|
|
3552
3532
|
* Sets the **server-side** microphone volume on this participant's bridged
|
|
@@ -3559,7 +3539,7 @@ var Participant = class extends Destroyable {
|
|
|
3559
3539
|
* @param value - Volume level (0-100).
|
|
3560
3540
|
*/
|
|
3561
3541
|
async setAudioInputVolume(value) {
|
|
3562
|
-
await this.executeMethod(
|
|
3542
|
+
await this.executeMethod("call.microphone.volume.set", { volume: value });
|
|
3563
3543
|
}
|
|
3564
3544
|
/**
|
|
3565
3545
|
* Sets the **server-side** speaker volume on this participant's bridged call
|
|
@@ -3573,45 +3553,31 @@ var Participant = class extends Destroyable {
|
|
|
3573
3553
|
* @param value - Volume level (0-100).
|
|
3574
3554
|
*/
|
|
3575
3555
|
async setAudioOutputVolume(value) {
|
|
3576
|
-
await this.executeMethod(
|
|
3556
|
+
await this.executeMethod("call.speaker.volume.set", { volume: value });
|
|
3577
3557
|
}
|
|
3578
3558
|
/**
|
|
3579
3559
|
* Sets the participant's position in the video layout.
|
|
3580
3560
|
*
|
|
3581
|
-
* Requires the `member.position` capability. The gateway
|
|
3582
|
-
*
|
|
3583
|
-
*
|
|
3584
|
-
*
|
|
3585
|
-
* guarantee a visible change: the backend silently returns `200` (no-op) for
|
|
3586
|
-
* non-conference targets.
|
|
3561
|
+
* Requires the `member.position` capability. The gateway requires a
|
|
3562
|
+
* `targets` array of `{ target, position }` entries (issue #19400). A
|
|
3563
|
+
* resolved promise does not guarantee a visible change: the backend silently
|
|
3564
|
+
* returns `200` (no-op) for non-conference targets.
|
|
3587
3565
|
*
|
|
3588
3566
|
* @param value - The {@link VideoPosition} to assign (e.g. `'auto'`, `'reserved-0'`).
|
|
3589
3567
|
*/
|
|
3590
3568
|
async setPosition(value) {
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
member_id: this.id,
|
|
3594
|
-
call_id: state.call_id ?? "",
|
|
3595
|
-
node_id: state.node_id ?? ""
|
|
3596
|
-
};
|
|
3597
|
-
await this.executeMethod(target, "call.member.position.set", { targets: [{
|
|
3598
|
-
target,
|
|
3569
|
+
await this.executeMethod("call.member.position.set", { targets: [{
|
|
3570
|
+
target: this.target,
|
|
3599
3571
|
position: value
|
|
3600
3572
|
}] });
|
|
3601
3573
|
}
|
|
3602
3574
|
/** Removes this participant from the call. */
|
|
3603
3575
|
async remove() {
|
|
3604
|
-
|
|
3605
|
-
const target = {
|
|
3606
|
-
member_id: this.id,
|
|
3607
|
-
call_id: state.call_id ?? "",
|
|
3608
|
-
node_id: state.node_id ?? ""
|
|
3609
|
-
};
|
|
3610
|
-
await this.executeMethod(target, "call.member.remove", {});
|
|
3576
|
+
await this.executeMethod("call.member.remove", { targets: [this.target] });
|
|
3611
3577
|
}
|
|
3612
3578
|
/** Ends the call for this participant. */
|
|
3613
3579
|
async end() {
|
|
3614
|
-
await this.executeMethod(
|
|
3580
|
+
await this.executeMethod("call.end", {});
|
|
3615
3581
|
}
|
|
3616
3582
|
/**
|
|
3617
3583
|
* Replaces custom metadata for this participant.
|
|
@@ -3631,7 +3597,7 @@ var Participant = class extends Destroyable {
|
|
|
3631
3597
|
}
|
|
3632
3598
|
/** Destroys the participant, releasing all subscriptions and references. */
|
|
3633
3599
|
destroy() {
|
|
3634
|
-
this.
|
|
3600
|
+
this.callExecuteMethod = void 0;
|
|
3635
3601
|
super.destroy();
|
|
3636
3602
|
}
|
|
3637
3603
|
};
|
|
@@ -3643,8 +3609,8 @@ var Participant = class extends Destroyable {
|
|
|
3643
3609
|
*/
|
|
3644
3610
|
var SelfParticipant = class extends Participant {
|
|
3645
3611
|
/** @internal */
|
|
3646
|
-
constructor(id,
|
|
3647
|
-
super(id,
|
|
3612
|
+
constructor(id, callExecuteMethod, vertoManager, deviceController) {
|
|
3613
|
+
super(id, callExecuteMethod, deviceController);
|
|
3648
3614
|
this.vertoManager = vertoManager;
|
|
3649
3615
|
this._studioAudio$ = this.createBehaviorSubject(false);
|
|
3650
3616
|
this.capabilities = new SelfCapabilities();
|
|
@@ -3668,7 +3634,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3668
3634
|
async enableStudioAudio() {
|
|
3669
3635
|
if (this._studioAudio$.value) return;
|
|
3670
3636
|
this._studioAudio$.next(true);
|
|
3671
|
-
await this.executeMethod(
|
|
3637
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3672
3638
|
echo_cancellation: false,
|
|
3673
3639
|
auto_gain: false,
|
|
3674
3640
|
noise_suppression: false
|
|
@@ -3681,18 +3647,37 @@ var SelfParticipant = class extends Participant {
|
|
|
3681
3647
|
async disableStudioAudio() {
|
|
3682
3648
|
if (!this._studioAudio$.value) return;
|
|
3683
3649
|
this._studioAudio$.next(false);
|
|
3684
|
-
await this.executeMethod(
|
|
3650
|
+
await this.executeMethod("call.audioflags.set", {
|
|
3685
3651
|
echo_cancellation: true,
|
|
3686
3652
|
auto_gain: true,
|
|
3687
3653
|
noise_suppression: true
|
|
3688
3654
|
});
|
|
3689
3655
|
}
|
|
3690
|
-
/**
|
|
3691
|
-
|
|
3656
|
+
/**
|
|
3657
|
+
* Starts sharing the local screen.
|
|
3658
|
+
*
|
|
3659
|
+
* A call carries at most one screen share. Read `screenShareStatus` before
|
|
3660
|
+
* calling and treat `'starting'`/`'stopping'` as busy.
|
|
3661
|
+
*
|
|
3662
|
+
* The call is unaffected when acquisition fails.
|
|
3663
|
+
*
|
|
3664
|
+
* @param options - Pass `{ audio: true }` to also request the shared
|
|
3665
|
+
* surface's audio. Defaults to video only.
|
|
3666
|
+
* @throws {ScreenShareAlreadyActiveError} When this call is already
|
|
3667
|
+
* sharing a screen. Call {@link stopScreenShare} before starting another.
|
|
3668
|
+
* @throws {AuxiliaryLegCancelledError} When {@link stopScreenShare} removes
|
|
3669
|
+
* the share before its leg finishes connecting.
|
|
3670
|
+
* @throws The raw `getDisplayMedia` error. A dismissed picker or a
|
|
3671
|
+
* permission denial rejects with a `NotAllowedError` `DOMException` —
|
|
3672
|
+
* inspect `error.name` to tell benign cancels apart from real failures.
|
|
3673
|
+
*/
|
|
3674
|
+
async startScreenShare(options) {
|
|
3692
3675
|
try {
|
|
3693
|
-
await this.vertoManager.addScreenMedia();
|
|
3676
|
+
await this.vertoManager.addScreenMedia(options);
|
|
3694
3677
|
} catch (error) {
|
|
3695
|
-
logger$
|
|
3678
|
+
if (error instanceof require_operators.AuxiliaryLegCancelledError) logger$24.debug("[Participant.startScreenShare] Screen share cancelled before connecting.");
|
|
3679
|
+
else logger$24.error("[Participant.startScreenShare] Screen share error:", error);
|
|
3680
|
+
throw error;
|
|
3696
3681
|
}
|
|
3697
3682
|
}
|
|
3698
3683
|
/** Observable of the current screen share status. */
|
|
@@ -3707,12 +3692,24 @@ var SelfParticipant = class extends Participant {
|
|
|
3707
3692
|
async stopScreenShare() {
|
|
3708
3693
|
return this.vertoManager.removeScreenMedia();
|
|
3709
3694
|
}
|
|
3710
|
-
/**
|
|
3695
|
+
/**
|
|
3696
|
+
* Adds an additional media input device to the call.
|
|
3697
|
+
*
|
|
3698
|
+
* The call is unaffected when acquisition fails.
|
|
3699
|
+
*
|
|
3700
|
+
* @throws {AuxiliaryLegCancelledError} When {@link removeAdditionalDevice}
|
|
3701
|
+
* removes the device before its leg finishes connecting.
|
|
3702
|
+
* @throws The raw `getUserMedia` error (e.g. `NotAllowedError` on
|
|
3703
|
+
* permission denial) — inspect `error.name` to decide how to react — or
|
|
3704
|
+
* `AuxiliaryLegTimeoutError` if the leg does not connect in time.
|
|
3705
|
+
*/
|
|
3711
3706
|
async addAdditionalDevice(options) {
|
|
3712
3707
|
try {
|
|
3713
3708
|
await this.vertoManager.addInputDevice(options);
|
|
3714
3709
|
} catch (error) {
|
|
3715
|
-
logger$
|
|
3710
|
+
if (error instanceof require_operators.AuxiliaryLegCancelledError) logger$24.debug("[Participant.addAdditionalDevice] Device removed before connecting.");
|
|
3711
|
+
else logger$24.error("[Participant.addAdditionalDevice] Additional device error:", error);
|
|
3712
|
+
throw error;
|
|
3716
3713
|
}
|
|
3717
3714
|
}
|
|
3718
3715
|
/** Removes an additional media input device by ID. */
|
|
@@ -3746,22 +3743,31 @@ var SelfParticipant = class extends Participant {
|
|
|
3746
3743
|
this.deviceController.selectAudioInputDevice(device);
|
|
3747
3744
|
if (options.savePreference) PreferencesContainer.instance.preferredAudioInput = device;
|
|
3748
3745
|
}
|
|
3749
|
-
/**
|
|
3746
|
+
/**
|
|
3747
|
+
* Updates the audio input track constraints for the active call.
|
|
3748
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
3749
|
+
*/
|
|
3750
3750
|
async setAudioInputDeviceConstraints(constraints) {
|
|
3751
|
-
|
|
3751
|
+
return this.vertoManager.updateMediaConstraints({ audio: constraints });
|
|
3752
3752
|
}
|
|
3753
|
-
/**
|
|
3753
|
+
/**
|
|
3754
|
+
* Updates both audio and video input track constraints for the active call.
|
|
3755
|
+
* @returns whether both kinds took the constraints.
|
|
3756
|
+
*/
|
|
3754
3757
|
async setInputDevicesConstraints(constraints) {
|
|
3755
|
-
|
|
3758
|
+
return this.vertoManager.updateMediaConstraints(constraints);
|
|
3756
3759
|
}
|
|
3757
3760
|
/** Selects the video input device for future calls. Optionally saves as a preference. */
|
|
3758
3761
|
selectVideoInputDevice(device, options = {}) {
|
|
3759
3762
|
this.deviceController.selectVideoInputDevice(device);
|
|
3760
3763
|
if (options.savePreference) PreferencesContainer.instance.preferredVideoInput = device;
|
|
3761
3764
|
}
|
|
3762
|
-
/**
|
|
3765
|
+
/**
|
|
3766
|
+
* Updates the video input track constraints for the active call.
|
|
3767
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
3768
|
+
*/
|
|
3763
3769
|
async setVideoInputDeviceConstraints(constraints) {
|
|
3764
|
-
|
|
3770
|
+
return this.vertoManager.updateMediaConstraints({ video: constraints });
|
|
3765
3771
|
}
|
|
3766
3772
|
/** Selects the audio output device. Optionally saves as a preference. */
|
|
3767
3773
|
selectAudioOutputDevice(device, options = {}) {
|
|
@@ -3774,7 +3780,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3774
3780
|
*/
|
|
3775
3781
|
exitStudioModeIfActive() {
|
|
3776
3782
|
if (this._studioAudio$.value) {
|
|
3777
|
-
logger$
|
|
3783
|
+
logger$24.debug("[SelfParticipant] Exiting studio audio mode due to individual flag toggle");
|
|
3778
3784
|
this._studioAudio$.next(false);
|
|
3779
3785
|
}
|
|
3780
3786
|
}
|
|
@@ -3798,7 +3804,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3798
3804
|
try {
|
|
3799
3805
|
await super.mute();
|
|
3800
3806
|
} catch (error) {
|
|
3801
|
-
logger$
|
|
3807
|
+
logger$24.warn("[Participant.toggleAudioInput] Server Error while muting audio input, proceeding with local toggle anyway", error);
|
|
3802
3808
|
} finally {
|
|
3803
3809
|
this.vertoManager.muteMainAudioInputDevice();
|
|
3804
3810
|
}
|
|
@@ -3808,7 +3814,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3808
3814
|
try {
|
|
3809
3815
|
await super.unmute();
|
|
3810
3816
|
} catch (error) {
|
|
3811
|
-
logger$
|
|
3817
|
+
logger$24.warn("[Participant.toggleAudioInput] Server Error while unmuting audio input, proceeding with local toggle anyway", error);
|
|
3812
3818
|
} finally {
|
|
3813
3819
|
await this.vertoManager.unmuteMainAudioInputDevice();
|
|
3814
3820
|
}
|
|
@@ -3818,7 +3824,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3818
3824
|
try {
|
|
3819
3825
|
await super.muteVideo();
|
|
3820
3826
|
} catch (error) {
|
|
3821
|
-
logger$
|
|
3827
|
+
logger$24.warn("[Participant.toggleVideoInput] Server Error while muting video input, proceeding with local toggle anyway", error);
|
|
3822
3828
|
} finally {
|
|
3823
3829
|
this.vertoManager.muteMainVideoInputDevice();
|
|
3824
3830
|
}
|
|
@@ -3828,7 +3834,7 @@ var SelfParticipant = class extends Participant {
|
|
|
3828
3834
|
try {
|
|
3829
3835
|
await super.unmuteVideo();
|
|
3830
3836
|
} catch (error) {
|
|
3831
|
-
logger$
|
|
3837
|
+
logger$24.warn("[Participant.toggleVideoInput] Server Error while unmuting video input, proceeding with local toggle anyway", error);
|
|
3832
3838
|
} finally {
|
|
3833
3839
|
await this.vertoManager.unmuteMainVideoInputDevice();
|
|
3834
3840
|
}
|
|
@@ -3922,7 +3928,7 @@ function isLayoutChangedPayload(value) {
|
|
|
3922
3928
|
|
|
3923
3929
|
//#endregion
|
|
3924
3930
|
//#region src/managers/CallEventsManager.ts
|
|
3925
|
-
const logger$
|
|
3931
|
+
const logger$23 = require_operators.getLogger();
|
|
3926
3932
|
const initialSessionState = {};
|
|
3927
3933
|
/** @internal */
|
|
3928
3934
|
var CallEventsManager = class extends Destroyable {
|
|
@@ -4026,7 +4032,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4026
4032
|
}
|
|
4027
4033
|
initSubscriptions() {
|
|
4028
4034
|
this.subscribeTo(this.callJoinedEvent$, (callJoinedEvent) => {
|
|
4029
|
-
logger$
|
|
4035
|
+
logger$23.debug("[CallEventsManager] Handling call.joined event for call/session IDs:", {
|
|
4030
4036
|
callId: callJoinedEvent.call_id,
|
|
4031
4037
|
roomSessionId: callJoinedEvent.room_session_id
|
|
4032
4038
|
});
|
|
@@ -4053,19 +4059,19 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4053
4059
|
if (this._self$.value?.capabilities.setLayout) this.updateLayouts();
|
|
4054
4060
|
});
|
|
4055
4061
|
this.subscribeTo(this.memberUpdates$, (member) => {
|
|
4056
|
-
logger$
|
|
4062
|
+
logger$23.debug("[CallEventsManager] Handling member update event for member ID:", member);
|
|
4057
4063
|
this.upsertParticipant(member);
|
|
4058
4064
|
});
|
|
4059
4065
|
this.subscribeTo(this.webRtcCallSession.memberLeft$, (memberLeftEvent) => {
|
|
4060
|
-
logger$
|
|
4066
|
+
logger$23.debug("[CallEventsManager] Handling member.left event for member ID:", memberLeftEvent.member.member_id);
|
|
4061
4067
|
const participants = { ...this._participants$.value };
|
|
4062
4068
|
if (memberLeftEvent.member.member_id in participants) {
|
|
4063
4069
|
delete participants[memberLeftEvent.member.member_id];
|
|
4064
4070
|
this._participants$.next(participants);
|
|
4065
|
-
} else logger$
|
|
4071
|
+
} else logger$23.warn(`[CallEventsManager] Received member.left event for unknown member ID: ${memberLeftEvent.member.member_id}`);
|
|
4066
4072
|
});
|
|
4067
4073
|
this.subscribeTo(this.webRtcCallSession.callUpdated$, (callUpdatedEvent) => {
|
|
4068
|
-
logger$
|
|
4074
|
+
logger$23.debug("[CallEventsManager] Handling call.updated event:", callUpdatedEvent);
|
|
4069
4075
|
const roomSession = callUpdatedEvent.room_session;
|
|
4070
4076
|
this._sessionState$.next({
|
|
4071
4077
|
...this._sessionState$.value,
|
|
@@ -4080,7 +4086,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4080
4086
|
});
|
|
4081
4087
|
});
|
|
4082
4088
|
this.subscribeTo(this.layoutChangedEvent$, (layoutChangedEvent) => {
|
|
4083
|
-
logger$
|
|
4089
|
+
logger$23.debug("[CallEventsManager] Handling layout.changed event:", layoutChangedEvent);
|
|
4084
4090
|
this._sessionState$.next({
|
|
4085
4091
|
...this._sessionState$.value,
|
|
4086
4092
|
layout_name: layoutChangedEvent.id,
|
|
@@ -4090,10 +4096,10 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4090
4096
|
});
|
|
4091
4097
|
}
|
|
4092
4098
|
updateParticipantPositions(layoutChangedEvent) {
|
|
4093
|
-
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$
|
|
4099
|
+
if (Object.keys(this._participants$.value).length > 0 && !layoutChangedEvent.layers.some((layer) => !!layer.member_id)) logger$23.warn("[CallEventsManager] No layers with member_id found in layout.changed event. Nothing to update.");
|
|
4094
4100
|
layoutChangedEvent.layers.filter((layer) => !!layer.member_id).filter((layer) => {
|
|
4095
4101
|
if (!(layer.member_id in this._participants$.value)) {
|
|
4096
|
-
logger$
|
|
4102
|
+
logger$23.warn(`[CallEventsManager] Skipping layout layer for unknown member_id: ${layer.member_id}`);
|
|
4097
4103
|
return false;
|
|
4098
4104
|
}
|
|
4099
4105
|
return true;
|
|
@@ -4111,12 +4117,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4111
4117
|
updateLayouts() {
|
|
4112
4118
|
if (!this.selfId) return;
|
|
4113
4119
|
this.webRtcCallSession.executeMethod(this.selfId, "call.layout.list", {}).then((response) => {
|
|
4120
|
+
const layouts = response.result?.layouts;
|
|
4121
|
+
if (!layouts) {
|
|
4122
|
+
logger$23.warn("[CallEventsManager] Layout list response carried no layouts; keeping current layouts");
|
|
4123
|
+
return;
|
|
4124
|
+
}
|
|
4114
4125
|
this._sessionState$.next({
|
|
4115
4126
|
...this._sessionState$.value,
|
|
4116
|
-
layouts
|
|
4127
|
+
layouts
|
|
4117
4128
|
});
|
|
4118
4129
|
}).catch((error) => {
|
|
4119
|
-
logger$
|
|
4130
|
+
logger$23.error("[CallEventsManager] Error fetching layouts:", error);
|
|
4120
4131
|
});
|
|
4121
4132
|
}
|
|
4122
4133
|
updateParticipants(members) {
|
|
@@ -4132,7 +4143,7 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4132
4143
|
}
|
|
4133
4144
|
const participant = this._participants$.value[member.member_id];
|
|
4134
4145
|
const oldValue = participant.value;
|
|
4135
|
-
logger$
|
|
4146
|
+
logger$23.debug("[CallEventsManager] Updating participant:", member.member_id, {
|
|
4136
4147
|
oldValue,
|
|
4137
4148
|
newValue: member
|
|
4138
4149
|
});
|
|
@@ -4145,17 +4156,17 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4145
4156
|
}
|
|
4146
4157
|
get callJoinedEvent$() {
|
|
4147
4158
|
return this.cachedObservable("callJoinedEvent$", () => this.webRtcCallSession.callEvent$.pipe((0, rxjs.filter)(isCallJoinedPayload), (0, rxjs.tap)((event) => {
|
|
4148
|
-
logger$
|
|
4159
|
+
logger$23.debug("[CallEventsManager] Call joined event:", event);
|
|
4149
4160
|
})));
|
|
4150
4161
|
}
|
|
4151
4162
|
get layoutChangedEvent$() {
|
|
4152
4163
|
return this.cachedObservable("layoutChangedEvent$", () => this.webRtcCallSession.callEvent$.pipe(require_operators.filterAs(isLayoutChangedPayload, "layout"), (0, rxjs.tap)((event) => {
|
|
4153
|
-
logger$
|
|
4164
|
+
logger$23.debug("[CallEventsManager] Layout changed event:", event);
|
|
4154
4165
|
})));
|
|
4155
4166
|
}
|
|
4156
4167
|
get memberUpdates$() {
|
|
4157
4168
|
return this.cachedObservable("memberUpdates$", () => (0, rxjs.merge)(this.webRtcCallSession.memberJoined$, this.webRtcCallSession.memberUpdated$, this.webRtcCallSession.memberTalking$).pipe((0, rxjs.map)((event) => event.member), (0, rxjs.tap)((event) => {
|
|
4158
|
-
logger$
|
|
4169
|
+
logger$23.debug("[CallEventsManager] Member update event:", event);
|
|
4159
4170
|
})));
|
|
4160
4171
|
}
|
|
4161
4172
|
destroy() {
|
|
@@ -4174,6 +4185,111 @@ var CallEventsManager = class extends Destroyable {
|
|
|
4174
4185
|
}
|
|
4175
4186
|
};
|
|
4176
4187
|
|
|
4188
|
+
//#endregion
|
|
4189
|
+
//#region src/controllers/ConstraintFallbackHelper.ts
|
|
4190
|
+
/**
|
|
4191
|
+
* ConstraintFallbackHelper - Provides getUserMedia with automatic constraint
|
|
4192
|
+
* fallback on OverconstrainedError.
|
|
4193
|
+
*
|
|
4194
|
+
* When a specific device ID is requested, the helper tries progressively
|
|
4195
|
+
* looser constraints:
|
|
4196
|
+
* 1. `{ deviceId: { exact: deviceId } }` -- exact match
|
|
4197
|
+
* 2. `{ deviceId: deviceId }` -- preferred (browser may pick another)
|
|
4198
|
+
* 3. `{}` -- no constraint, browser default
|
|
4199
|
+
*
|
|
4200
|
+
* This prevents stale device IDs from blocking call setup.
|
|
4201
|
+
*
|
|
4202
|
+
* @see Section 5.8 and Section 11 of the Implementation Guide
|
|
4203
|
+
*/
|
|
4204
|
+
const logger$22 = require_operators.getLogger();
|
|
4205
|
+
/**
|
|
4206
|
+
* Attempts getUserMedia with progressively looser constraints.
|
|
4207
|
+
*
|
|
4208
|
+
* The function tries three levels of constraint specificity for the given
|
|
4209
|
+
* device kind. Each level is only attempted if the previous one fails with
|
|
4210
|
+
* an OverconstrainedError. Non-OverconstrainedError failures (e.g.,
|
|
4211
|
+
* NotAllowedError) are thrown immediately without fallback.
|
|
4212
|
+
*
|
|
4213
|
+
* @param mediaDevices - Anything exposing `getUserMedia` (a full
|
|
4214
|
+
* `WebRTCMediaDevices`, or a shim wrapping one)
|
|
4215
|
+
* @param constraints - The full MediaStreamConstraints to use as a base
|
|
4216
|
+
* @param kind - Which track kind to apply fallback to ('audio' | 'video')
|
|
4217
|
+
* @param deviceId - The device ID to try (if undefined, calls getUserMedia as-is)
|
|
4218
|
+
* @returns The stream and the fallback level that succeeded
|
|
4219
|
+
* @throws When all fallback levels fail, or when a non-OverconstrainedError occurs
|
|
4220
|
+
*/
|
|
4221
|
+
async function getUserMediaWithFallback(mediaDevices, constraints, kind, deviceId) {
|
|
4222
|
+
if (!deviceId) return {
|
|
4223
|
+
stream: await mediaDevices.getUserMedia(constraints),
|
|
4224
|
+
fallbackLevel: "default"
|
|
4225
|
+
};
|
|
4226
|
+
const baseConstraints = typeof constraints[kind] === "object" ? constraints[kind] : {};
|
|
4227
|
+
try {
|
|
4228
|
+
const exactConstraints = {
|
|
4229
|
+
...constraints,
|
|
4230
|
+
[kind]: {
|
|
4231
|
+
...baseConstraints,
|
|
4232
|
+
deviceId: { exact: deviceId }
|
|
4233
|
+
}
|
|
4234
|
+
};
|
|
4235
|
+
return {
|
|
4236
|
+
stream: await mediaDevices.getUserMedia(exactConstraints),
|
|
4237
|
+
fallbackLevel: "exact"
|
|
4238
|
+
};
|
|
4239
|
+
} catch (error) {
|
|
4240
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
4241
|
+
logger$22.debug(`[ConstraintFallbackHelper] Exact constraint failed for ${kind}, trying preferred`, { deviceId });
|
|
4242
|
+
}
|
|
4243
|
+
try {
|
|
4244
|
+
const preferredConstraints = {
|
|
4245
|
+
...constraints,
|
|
4246
|
+
[kind]: {
|
|
4247
|
+
...baseConstraints,
|
|
4248
|
+
deviceId
|
|
4249
|
+
}
|
|
4250
|
+
};
|
|
4251
|
+
return {
|
|
4252
|
+
stream: await mediaDevices.getUserMedia(preferredConstraints),
|
|
4253
|
+
fallbackLevel: "preferred"
|
|
4254
|
+
};
|
|
4255
|
+
} catch (error) {
|
|
4256
|
+
if (!isOverconstrainedError(error)) throw error;
|
|
4257
|
+
logger$22.debug(`[ConstraintFallbackHelper] Preferred constraint failed for ${kind}, trying default`, { deviceId });
|
|
4258
|
+
}
|
|
4259
|
+
try {
|
|
4260
|
+
const defaultConstraints = {
|
|
4261
|
+
...constraints,
|
|
4262
|
+
[kind]: { ...baseConstraints }
|
|
4263
|
+
};
|
|
4264
|
+
if (typeof defaultConstraints[kind] === "object") {
|
|
4265
|
+
const { deviceId: _removed, ...rest } = defaultConstraints[kind];
|
|
4266
|
+
defaultConstraints[kind] = rest;
|
|
4267
|
+
}
|
|
4268
|
+
const stream = await mediaDevices.getUserMedia(defaultConstraints);
|
|
4269
|
+
logger$22.warn(`[ConstraintFallbackHelper] Fell back to browser default for ${kind}`, { requestedDeviceId: deviceId });
|
|
4270
|
+
return {
|
|
4271
|
+
stream,
|
|
4272
|
+
fallbackLevel: "default"
|
|
4273
|
+
};
|
|
4274
|
+
} catch (error) {
|
|
4275
|
+
logger$22.error(`[ConstraintFallbackHelper] All fallback levels exhausted for ${kind}`, {
|
|
4276
|
+
deviceId,
|
|
4277
|
+
error
|
|
4278
|
+
});
|
|
4279
|
+
throw error;
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
/**
|
|
4283
|
+
* Checks whether an error is an OverconstrainedError.
|
|
4284
|
+
*
|
|
4285
|
+
* Browsers may throw either a native OverconstrainedError or a DOMException
|
|
4286
|
+
* with a specific name.
|
|
4287
|
+
*/
|
|
4288
|
+
function isOverconstrainedError(error) {
|
|
4289
|
+
if (error instanceof Error) return error.name === "OverconstrainedError" || error.name === "ConstraintNotSatisfiedError";
|
|
4290
|
+
return false;
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4177
4293
|
//#endregion
|
|
4178
4294
|
//#region src/helpers/SDPHelper.ts
|
|
4179
4295
|
/**
|
|
@@ -4315,7 +4431,7 @@ function reorderCodecs(sdp, preferredVideo = [], preferredAudio = []) {
|
|
|
4315
4431
|
* // a=fmtp:111 minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1;maxaveragebitrate=510000
|
|
4316
4432
|
* ```
|
|
4317
4433
|
*/
|
|
4318
|
-
function enableStereoOpus(sdp, maxBitrate = DEFAULT_STEREO_MAX_AVERAGE_BITRATE) {
|
|
4434
|
+
function enableStereoOpus(sdp, maxBitrate = require_operators.DEFAULT_STEREO_MAX_AVERAGE_BITRATE) {
|
|
4319
4435
|
if (!sdp) return sdp;
|
|
4320
4436
|
const opusPayloadType = findOpusPayloadType(sdp);
|
|
4321
4437
|
if (opusPayloadType === null) return sdp;
|
|
@@ -4444,8 +4560,8 @@ var ICEGatheringController = class extends Destroyable {
|
|
|
4444
4560
|
state: "new",
|
|
4445
4561
|
validSDP: false
|
|
4446
4562
|
});
|
|
4447
|
-
this.iceCandidateTimeout = options.iceCandidateTimeout ?? DEFAULT_ICE_CANDIDATE_TIMEOUT_MS;
|
|
4448
|
-
this.iceGatheringTimeout = options.iceGatheringTimeout ?? DEFAULT_ICE_GATHERING_TIMEOUT_MS;
|
|
4563
|
+
this.iceCandidateTimeout = options.iceCandidateTimeout ?? require_operators.DEFAULT_ICE_CANDIDATE_TIMEOUT_MS;
|
|
4564
|
+
this.iceGatheringTimeout = options.iceGatheringTimeout ?? require_operators.DEFAULT_ICE_GATHERING_TIMEOUT_MS;
|
|
4449
4565
|
this.relayOnly = options.relayOnly ?? false;
|
|
4450
4566
|
this.setupEventListeners();
|
|
4451
4567
|
this.subscribeTo(this.peerConnectionControllerNegotiating$.pipe((0, rxjs.filter)((isNegotiating) => isNegotiating)), (isNegotiating) => {
|
|
@@ -4590,9 +4706,9 @@ var LocalAudioPipeline = class extends Destroyable {
|
|
|
4590
4706
|
this._destination = this._audioContext.createMediaStreamDestination();
|
|
4591
4707
|
this._gainNode.connect(this._analyser);
|
|
4592
4708
|
this._analyser.connect(this._destination);
|
|
4593
|
-
this._speakingThreshold = options.speakingThreshold ?? VAD_THRESHOLD;
|
|
4594
|
-
this._speakingHoldMs = options.speakingHoldMs ?? VAD_HOLD_MS;
|
|
4595
|
-
this._pollIntervalMs = options.pollIntervalMs ?? AUDIO_LEVEL_POLL_INTERVAL_MS;
|
|
4709
|
+
this._speakingThreshold = options.speakingThreshold ?? require_operators.VAD_THRESHOLD;
|
|
4710
|
+
this._speakingHoldMs = options.speakingHoldMs ?? require_operators.VAD_HOLD_MS;
|
|
4711
|
+
this._pollIntervalMs = options.pollIntervalMs ?? require_operators.AUDIO_LEVEL_POLL_INTERVAL_MS;
|
|
4596
4712
|
const initial = options.initialGain ?? 1;
|
|
4597
4713
|
this._gain$.next(initial);
|
|
4598
4714
|
this.applyEffectiveGain();
|
|
@@ -4729,6 +4845,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4729
4845
|
this._localAudioTracks$ = this.createBehaviorSubject([]);
|
|
4730
4846
|
this._localVideoTracks$ = this.createBehaviorSubject([]);
|
|
4731
4847
|
this._mediaTrackEnded$ = this.createSubject();
|
|
4848
|
+
this._trackOrigins = /* @__PURE__ */ new WeakMap();
|
|
4732
4849
|
}
|
|
4733
4850
|
get localStream$() {
|
|
4734
4851
|
return this._localStream$.asObservable().pipe((0, rxjs.takeUntil)(this.destroyed$));
|
|
@@ -4751,6 +4868,22 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4751
4868
|
get localVideoTracks() {
|
|
4752
4869
|
return this._localVideoTracks$.value;
|
|
4753
4870
|
}
|
|
4871
|
+
tagTracks(tracks, origin) {
|
|
4872
|
+
for (const track of tracks) this._trackOrigins.set(track, origin);
|
|
4873
|
+
}
|
|
4874
|
+
setTrackOrigin(track, origin) {
|
|
4875
|
+
this._trackOrigins.set(track, origin);
|
|
4876
|
+
}
|
|
4877
|
+
getTrackOrigin(track) {
|
|
4878
|
+
return this._trackOrigins.get(track);
|
|
4879
|
+
}
|
|
4880
|
+
/**
|
|
4881
|
+
* Fail-safe: an unrecorded track reads as not-a-device-capture, so a missed
|
|
4882
|
+
* tagging site leaves media alone rather than destroying it.
|
|
4883
|
+
*/
|
|
4884
|
+
isDeviceCapture(track) {
|
|
4885
|
+
return this._trackOrigins.get(track) === "device";
|
|
4886
|
+
}
|
|
4754
4887
|
/**
|
|
4755
4888
|
* Build the local media stream based on the provided options.
|
|
4756
4889
|
*/
|
|
@@ -4760,13 +4893,16 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4760
4893
|
if (this.options.inputAudioStream ?? this.options.inputVideoStream) {
|
|
4761
4894
|
const tracks = [...this.options.inputAudioStream?.getTracks() ?? [], ...this.options.inputVideoStream?.getTracks() ?? []];
|
|
4762
4895
|
stream = new MediaStream(tracks);
|
|
4896
|
+
this.tagTracks(tracks, "application");
|
|
4763
4897
|
} else if (this.options.propose === "screenshare") {
|
|
4764
|
-
|
|
4898
|
+
const audio = this.options.screenShareAudio ?? false;
|
|
4899
|
+
logger$19.debug("[LocalStreamController] Requesting display media for screen sharing with audio:", audio);
|
|
4765
4900
|
stream = await this.options.getDisplayMedia({
|
|
4766
4901
|
video: true,
|
|
4767
|
-
audio
|
|
4902
|
+
audio
|
|
4768
4903
|
});
|
|
4769
4904
|
logger$19.debug("[LocalStreamController] Screen share media obtained:", stream);
|
|
4905
|
+
this.tagTracks(stream.getTracks(), "display");
|
|
4770
4906
|
} else {
|
|
4771
4907
|
const constraints = {
|
|
4772
4908
|
audio: this.options.inputAudioDeviceConstraints,
|
|
@@ -4775,6 +4911,7 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4775
4911
|
logger$19.debug("[LocalStreamController] Requesting user media with constraints:", constraints);
|
|
4776
4912
|
stream = await this.options.getUserMedia(constraints);
|
|
4777
4913
|
logger$19.debug("[LocalStreamController] User media obtained:", stream);
|
|
4914
|
+
this.tagTracks(stream.getTracks(), "device");
|
|
4778
4915
|
}
|
|
4779
4916
|
this._localStream$.next(stream);
|
|
4780
4917
|
this._localAudioTracks$.next(stream.getAudioTracks());
|
|
@@ -4784,10 +4921,13 @@ var LocalStreamController = class extends Destroyable {
|
|
|
4784
4921
|
/**
|
|
4785
4922
|
* Add a local media track to the local stream.
|
|
4786
4923
|
* @param track - The MediaStreamTrack to add
|
|
4924
|
+
* @param origin - Defaults to `'device'`; every internal caller passes a
|
|
4925
|
+
* fresh `getUserMedia` capture.
|
|
4787
4926
|
* @returns The MediaStream (either existing or newly created)
|
|
4788
4927
|
*/
|
|
4789
|
-
addTrack(track) {
|
|
4928
|
+
addTrack(track, origin = "device") {
|
|
4790
4929
|
const localStream = this._localStream$.value ?? new MediaStream();
|
|
4930
|
+
this._trackOrigins.set(track, origin);
|
|
4791
4931
|
track.addEventListener("ended", this.mediaTrackEndedHandler);
|
|
4792
4932
|
localStream.addTrack(track);
|
|
4793
4933
|
this._localStream$.next(localStream);
|
|
@@ -5057,15 +5197,27 @@ var TransceiverController = class extends Destroyable {
|
|
|
5057
5197
|
for (let i = 0; i < Number(msStreamsNumber); i++) this.peerConnection.addTransceiver("video", { direction: "recvonly" });
|
|
5058
5198
|
}
|
|
5059
5199
|
}
|
|
5200
|
+
/**
|
|
5201
|
+
* @returns whether every live sender of the kind took the constraints. A
|
|
5202
|
+
* skipped non-device sender, an exhausted fallback, and having no live sender
|
|
5203
|
+
* at all all report `false` — `mediaParamsUpdated.applied` is built from this,
|
|
5204
|
+
* and an application told `true` cannot tell a working push from a no-op.
|
|
5205
|
+
*/
|
|
5060
5206
|
async updateSendersConstraints(kind, constraints) {
|
|
5061
5207
|
if (!constraints) {
|
|
5062
5208
|
this.stopTrackSender(kind);
|
|
5063
|
-
return
|
|
5209
|
+
return false;
|
|
5064
5210
|
}
|
|
5065
5211
|
const senders = this.peerConnection.getSenders().filter((sender) => sender.track?.kind === kind && sender.track.readyState === "live");
|
|
5212
|
+
let applied = senders.length > 0;
|
|
5066
5213
|
for (const sender of senders) {
|
|
5067
5214
|
const { track } = sender;
|
|
5068
5215
|
if (track) {
|
|
5216
|
+
if (!this.options.localStreamController.isDeviceCapture(track)) {
|
|
5217
|
+
logger$18.debug(`[TransceiverController] Skipping ${kind} constraints for a non-device track (origin: ${this.options.localStreamController.getTrackOrigin(track) ?? "unrecorded"}), track ${track.id}`);
|
|
5218
|
+
applied = false;
|
|
5219
|
+
continue;
|
|
5220
|
+
}
|
|
5069
5221
|
const constraintsToApply = {
|
|
5070
5222
|
...track.getConstraints(),
|
|
5071
5223
|
...constraints
|
|
@@ -5081,33 +5233,39 @@ var TransceiverController = class extends Destroyable {
|
|
|
5081
5233
|
} catch (fallbackError) {
|
|
5082
5234
|
logger$18.warn(`[TransceiverController] Track replacement fallback also failed for ${kind} track:`, fallbackError);
|
|
5083
5235
|
this.options.onError?.(new require_operators.MediaTrackError("updateSendersConstraints", kind, fallbackError));
|
|
5236
|
+
applied = false;
|
|
5084
5237
|
}
|
|
5085
5238
|
}
|
|
5086
5239
|
}
|
|
5087
5240
|
}
|
|
5241
|
+
return applied;
|
|
5088
5242
|
}
|
|
5089
5243
|
/**
|
|
5090
|
-
* Fallback when applyConstraints fails
|
|
5091
|
-
* one via getUserMedia with the merged constraints (preserving the current
|
|
5092
|
-
* deviceId), replace the sender track, and update the localStream.
|
|
5244
|
+
* Fallback when applyConstraints fails, which on iOS Safari it silently does.
|
|
5093
5245
|
*
|
|
5094
|
-
*
|
|
5095
|
-
*
|
|
5246
|
+
* Order matters: acquiring before stopping means a failed acquisition leaves
|
|
5247
|
+
* the existing media playing. The deviceId goes through the fallback ladder
|
|
5248
|
+
* rather than pinned `{ exact }`, so a stale id degrades instead of failing.
|
|
5096
5249
|
*/
|
|
5097
5250
|
async replaceTrackFallback(sender, oldTrack, kind, mergedConstraints) {
|
|
5098
5251
|
const { deviceId } = oldTrack.getSettings();
|
|
5099
|
-
const
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5252
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: this.options.getUserMedia }, { [kind]: mergedConstraints }, kind, deviceId);
|
|
5253
|
+
const newTrack = stream.getTracks().find((t) => t.kind === kind);
|
|
5254
|
+
if (!newTrack) {
|
|
5255
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
5256
|
+
throw new require_operators.MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
5257
|
+
}
|
|
5258
|
+
try {
|
|
5259
|
+
await sender.replaceTrack(newTrack);
|
|
5260
|
+
} catch (error) {
|
|
5261
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
5262
|
+
throw error;
|
|
5263
|
+
}
|
|
5264
|
+
const oldTrackId = oldTrack.id;
|
|
5265
|
+
this.options.localStreamController.removeTrack(oldTrackId);
|
|
5104
5266
|
oldTrack.stop();
|
|
5105
|
-
this.options.localStreamController.removeTrack(trackId);
|
|
5106
|
-
const newTrack = (await this.options.getUserMedia({ [kind]: constraintsWithDevice })).getTracks().find((t) => t.kind === kind);
|
|
5107
|
-
if (!newTrack) throw new require_operators.MediaTrackError("replaceTrackFallback", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
5108
|
-
await sender.replaceTrack(newTrack);
|
|
5109
5267
|
this.options.localStreamController.addTrack(newTrack);
|
|
5110
|
-
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind}. New track: ${newTrack.id}`);
|
|
5268
|
+
logger$18.debug(`[TransceiverController] Track replacement fallback succeeded for ${kind} (deviceId fallback level: ${fallbackLevel}). New track: ${newTrack.id}`);
|
|
5111
5269
|
}
|
|
5112
5270
|
getMediaDirections() {
|
|
5113
5271
|
if (this.peerConnection.connectionState === "connected") return this.peerConnection.getTransceivers().reduce((acc, transceiver) => {
|
|
@@ -5176,48 +5334,44 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5176
5334
|
this.negotiationNeeded$.next();
|
|
5177
5335
|
};
|
|
5178
5336
|
this.updateSelectedInputDevice = async (kind, deviceInfo) => {
|
|
5337
|
+
const { localStream } = this;
|
|
5338
|
+
if (!localStream) {
|
|
5339
|
+
logger$17.warn("[RTCPeerConnectionController] No local stream available to update input device.");
|
|
5340
|
+
return;
|
|
5341
|
+
}
|
|
5342
|
+
const currentTrack = localStream.getTracks().find((track) => track.kind === kind);
|
|
5343
|
+
if (!currentTrack) {
|
|
5344
|
+
logger$17.debug(`[RTCPeerConnectionController] No ${kind} track to switch.`);
|
|
5345
|
+
return;
|
|
5346
|
+
}
|
|
5347
|
+
if (!deviceInfo) {
|
|
5348
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
5349
|
+
this.stopTrackSender(kind);
|
|
5350
|
+
return;
|
|
5351
|
+
}
|
|
5352
|
+
const constraints = {
|
|
5353
|
+
...currentTrack.getConstraints(),
|
|
5354
|
+
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
5355
|
+
};
|
|
5179
5356
|
try {
|
|
5180
|
-
const
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
return;
|
|
5184
|
-
}
|
|
5185
|
-
logger$17.debug(`[RTCPeerConnectionController] Updating selected ${kind} input device:`, localStream.getTracks());
|
|
5186
|
-
const track = localStream.getTracks().find((track$1) => track$1.kind === kind);
|
|
5187
|
-
if (track) {
|
|
5188
|
-
this.transceiverController?.stopTrackSender(kind);
|
|
5189
|
-
this.localStreamController.removeTrack(track.id);
|
|
5190
|
-
logger$17.debug(`[RTCPeerConnectionController] Stopped existing ${kind} track: ${track.id}`, localStream.getTracks());
|
|
5191
|
-
if (!deviceInfo) {
|
|
5192
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected: none`);
|
|
5193
|
-
return;
|
|
5194
|
-
}
|
|
5195
|
-
const streamTrack = (await this.getUserMedia({ [kind]: {
|
|
5196
|
-
...track.getConstraints(),
|
|
5197
|
-
...this.deviceController.deviceInfoToConstraints(deviceInfo)
|
|
5198
|
-
} })).getTracks().find((t) => t.kind === kind);
|
|
5199
|
-
if (streamTrack) {
|
|
5200
|
-
logger$17.debug(`[RTCPeerConnectionController] Adding new ${kind} track: ${streamTrack.id}`);
|
|
5201
|
-
this.localStreamController.addTrack(streamTrack);
|
|
5202
|
-
await this.transceiverController?.replaceSenderTrack(kind, streamTrack);
|
|
5203
|
-
logger$17.debug(`[RTCPeerConnectionController] Added new ${kind} track: ${streamTrack.id}`, this.localStream?.getTracks());
|
|
5204
|
-
}
|
|
5205
|
-
}
|
|
5206
|
-
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo?.label);
|
|
5357
|
+
const newTrack = await this.acquireInputTrack(kind, constraints, deviceInfo, currentTrack);
|
|
5358
|
+
await this.attachInputTrack(kind, newTrack, currentTrack);
|
|
5359
|
+
logger$17.debug(`[RTCPeerConnectionController] ${kind} input device selected:`, deviceInfo.label, newTrack.id);
|
|
5207
5360
|
} catch (error) {
|
|
5208
5361
|
logger$17.error(`[RTCPeerConnectionController] Failed to select ${kind} input device:`, error);
|
|
5209
|
-
this._errors$.next(
|
|
5210
|
-
throw error;
|
|
5362
|
+
this._errors$.next(new require_operators.MediaTrackError("updateSelectedInputDevice", kind, error));
|
|
5211
5363
|
}
|
|
5212
5364
|
};
|
|
5213
5365
|
this._isNegotiating$ = this.createBehaviorSubject(false);
|
|
5214
5366
|
this._memberId = null;
|
|
5367
|
+
this._nodeId = null;
|
|
5215
5368
|
this._iceConnectionState$ = this.createReplaySubject(1);
|
|
5216
5369
|
this._connectionState$ = this.createReplaySubject(1);
|
|
5217
5370
|
this._signalingState$ = this.createReplaySubject(1);
|
|
5218
5371
|
this._iceGatheringState$ = this.createReplaySubject(1);
|
|
5219
5372
|
this._errors$ = this.createReplaySubject(1);
|
|
5220
5373
|
this._iceCandidates$ = this.createReplaySubject(1);
|
|
5374
|
+
this._localMediaSettled$ = this.createReplaySubject(1);
|
|
5221
5375
|
this._initialized$ = this.createReplaySubject(1);
|
|
5222
5376
|
this._remoteDescription$ = this.createReplaySubject(1);
|
|
5223
5377
|
this._remoteStream$ = this.createBehaviorSubject(null);
|
|
@@ -5250,6 +5404,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5250
5404
|
inputVideoStream: this.options.inputVideoStream,
|
|
5251
5405
|
inputAudioDeviceConstraints: this.inputAudioDeviceConstraints,
|
|
5252
5406
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints,
|
|
5407
|
+
screenShareAudio: this.options.screenShareAudio,
|
|
5253
5408
|
getUserMedia: async (constraints) => this.getUserMedia(constraints),
|
|
5254
5409
|
getDisplayMedia: async (options$1) => this.getDisplayMedia(options$1)
|
|
5255
5410
|
});
|
|
@@ -5276,6 +5431,13 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5276
5431
|
get memberId() {
|
|
5277
5432
|
return this._memberId;
|
|
5278
5433
|
}
|
|
5434
|
+
/** The node this leg's invite landed on — auxiliary legs are placed independently. */
|
|
5435
|
+
setNodeId(nodeId) {
|
|
5436
|
+
this._nodeId = nodeId;
|
|
5437
|
+
}
|
|
5438
|
+
get nodeId() {
|
|
5439
|
+
return this._nodeId;
|
|
5440
|
+
}
|
|
5279
5441
|
stopTrackSender(kind, options = { updateTransceiverDirection: false }) {
|
|
5280
5442
|
const audioCovered = kind === "audio" || kind === "both";
|
|
5281
5443
|
if (audioCovered && this._localAudioPipeline) this.stopRawAudioInputForPipeline();
|
|
@@ -5285,10 +5447,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5285
5447
|
}
|
|
5286
5448
|
stopRawAudioInputForPipeline() {
|
|
5287
5449
|
const rawTracks = this.localStreamController.localAudioTracks;
|
|
5288
|
-
for (const track of rawTracks) if (track.readyState === "live")
|
|
5289
|
-
track.stop();
|
|
5290
|
-
this.localStreamController.removeTrack(track.id);
|
|
5291
|
-
}
|
|
5450
|
+
for (const track of rawTracks) if (track.readyState === "live") this.localStreamController.removeTrack(track.id);
|
|
5292
5451
|
this._localAudioPipeline?.setInputTrack(null);
|
|
5293
5452
|
}
|
|
5294
5453
|
get isNegotiating$() {
|
|
@@ -5321,6 +5480,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5321
5480
|
get remoteDescription$() {
|
|
5322
5481
|
return this.cachedObservable("remoteDescription$", () => this._remoteDescription$.asObservable().pipe((0, rxjs.takeUntil)(this.destroyed$)));
|
|
5323
5482
|
}
|
|
5483
|
+
/** Emits once local media is settled — acquired, or knowingly receive-only. */
|
|
5484
|
+
get localMediaSettled$() {
|
|
5485
|
+
return this.cachedObservable("localMediaSettled$", () => this._localMediaSettled$.asObservable().pipe((0, rxjs.takeUntil)(this.destroyed$)));
|
|
5486
|
+
}
|
|
5324
5487
|
get localStream$() {
|
|
5325
5488
|
return this.cachedObservable("localStream$", () => this.localStreamController.localStream$.pipe((0, rxjs.takeUntil)(this.destroyed$)));
|
|
5326
5489
|
}
|
|
@@ -5348,6 +5511,9 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5348
5511
|
get propose() {
|
|
5349
5512
|
return this.options.propose ?? "main";
|
|
5350
5513
|
}
|
|
5514
|
+
get connectionState() {
|
|
5515
|
+
return this.peerConnection?.connectionState;
|
|
5516
|
+
}
|
|
5351
5517
|
get isAdditionalDevice() {
|
|
5352
5518
|
return this.propose === "additional-device";
|
|
5353
5519
|
}
|
|
@@ -5428,7 +5594,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5428
5594
|
case "main":
|
|
5429
5595
|
default: return {
|
|
5430
5596
|
...options,
|
|
5431
|
-
offerToReceiveAudio: true,
|
|
5597
|
+
offerToReceiveAudio: this.options.receiveAudio ?? true,
|
|
5432
5598
|
offerToReceiveVideo: this.options.receiveVideo ?? Boolean(this.inputVideoDeviceConstraints)
|
|
5433
5599
|
};
|
|
5434
5600
|
}
|
|
@@ -5476,7 +5642,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5476
5642
|
this._isNegotiating$.next(true);
|
|
5477
5643
|
await this._setRemoteDescription(this.sdpInit);
|
|
5478
5644
|
} else {
|
|
5479
|
-
await this.setupTrackHandling();
|
|
5645
|
+
if (!await this.setupTrackHandling()) return;
|
|
5480
5646
|
this._initialized$.next(true);
|
|
5481
5647
|
}
|
|
5482
5648
|
} catch (error) {
|
|
@@ -5582,13 +5748,14 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5582
5748
|
*/
|
|
5583
5749
|
async acceptInbound(mediaOverrides) {
|
|
5584
5750
|
if (mediaOverrides) {
|
|
5585
|
-
const { audio, video, receiveAudio, receiveVideo } = mediaOverrides;
|
|
5751
|
+
const { audio, video, receiveAudio, receiveVideo, fallbackToReceiveOnly } = mediaOverrides;
|
|
5586
5752
|
this.options = {
|
|
5587
5753
|
...this.options,
|
|
5588
5754
|
...audio !== void 0 ? { audio } : {},
|
|
5589
5755
|
...video !== void 0 ? { video } : {},
|
|
5590
5756
|
...receiveAudio !== void 0 ? { receiveAudio } : {},
|
|
5591
|
-
...receiveVideo !== void 0 ? { receiveVideo } : {}
|
|
5757
|
+
...receiveVideo !== void 0 ? { receiveVideo } : {},
|
|
5758
|
+
...fallbackToReceiveOnly !== void 0 ? { fallbackToReceiveOnly } : {}
|
|
5592
5759
|
};
|
|
5593
5760
|
this.transceiverController?.updateOptions({
|
|
5594
5761
|
receiveAudio: this.receiveAudio,
|
|
@@ -5599,7 +5766,10 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5599
5766
|
inputVideoDeviceConstraints: this.inputVideoDeviceConstraints
|
|
5600
5767
|
});
|
|
5601
5768
|
}
|
|
5602
|
-
await this.setupLocalTracks()
|
|
5769
|
+
if (!await this.setupLocalTracks()) {
|
|
5770
|
+
logger$17.debug("[RTCPeerConnectionController] Inbound answer abandoned; the connection went away.");
|
|
5771
|
+
return;
|
|
5772
|
+
}
|
|
5603
5773
|
const { answerOptions } = this;
|
|
5604
5774
|
logger$17.debug("[RTCPeerConnectionController] Creating inbound answer with options:", answerOptions);
|
|
5605
5775
|
await this.createAnswer(answerOptions);
|
|
@@ -5719,7 +5889,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5719
5889
|
if (policyChanged) this.restoreIceTransportPolicy();
|
|
5720
5890
|
throw error;
|
|
5721
5891
|
}
|
|
5722
|
-
if (policyChanged) (0, rxjs.firstValueFrom)((0, rxjs.race)(this._iceGatheringState$.pipe((0, rxjs.filter)((state) => state === "complete"), (0, rxjs.take)(1)), (0, rxjs.timer)(ICE_GATHERING_COMPLETE_TIMEOUT_MS).pipe((0, rxjs.map)(() => "timeout")))).then(() => this.restoreIceTransportPolicy()).catch((error) => {
|
|
5892
|
+
if (policyChanged) (0, rxjs.firstValueFrom)((0, rxjs.race)(this._iceGatheringState$.pipe((0, rxjs.filter)((state) => state === "complete"), (0, rxjs.take)(1)), (0, rxjs.timer)(require_operators.ICE_GATHERING_COMPLETE_TIMEOUT_MS).pipe((0, rxjs.map)(() => "timeout")))).then(() => this.restoreIceTransportPolicy()).catch((error) => {
|
|
5723
5893
|
logger$17.warn("[RTCPeerConnectionController] Error waiting for ICE gathering to complete:", error);
|
|
5724
5894
|
this.restoreIceTransportPolicy();
|
|
5725
5895
|
});
|
|
@@ -5737,23 +5907,52 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5737
5907
|
}
|
|
5738
5908
|
/**
|
|
5739
5909
|
* Setup track handling for remote tracks.
|
|
5910
|
+
*
|
|
5911
|
+
* @returns `false` when the connection went away while local media was being
|
|
5912
|
+
* acquired — see {@link setupLocalTracks}.
|
|
5740
5913
|
*/
|
|
5741
5914
|
async setupTrackHandling() {
|
|
5742
5915
|
if (!this.peerConnection) throw new require_operators.DependencyError("RTCPeerConnection is not initialized");
|
|
5743
|
-
await this.setupLocalTracks();
|
|
5916
|
+
if (!await this.setupLocalTracks()) return false;
|
|
5744
5917
|
await this.setupRemoteTracks();
|
|
5918
|
+
return true;
|
|
5745
5919
|
}
|
|
5920
|
+
/**
|
|
5921
|
+
* @returns `false` when the connection was torn down while getUserMedia was
|
|
5922
|
+
* in flight. The acquisition is not cancellable, so the caller must stop
|
|
5923
|
+
* rather than go on to touch a peer connection that is closed or gone.
|
|
5924
|
+
*/
|
|
5746
5925
|
async setupLocalTracks() {
|
|
5747
5926
|
logger$17.debug("[RTCPeerConnectionController] Setting up local tracks/transceivers.");
|
|
5748
|
-
|
|
5927
|
+
if (this.hasNoLocalMediaToSend()) {
|
|
5928
|
+
if (!this.receiveAudio && !this.receiveVideo) throw new require_operators.InvalidParams("Call requests no media: enable audio/video or receiveAudio/receiveVideo");
|
|
5929
|
+
logger$17.debug("[RTCPeerConnectionController] No local media requested; negotiating receive-only.");
|
|
5930
|
+
this.setupReceiveOnlyTransceivers();
|
|
5931
|
+
this._localMediaSettled$.next();
|
|
5932
|
+
return true;
|
|
5933
|
+
}
|
|
5934
|
+
let localStream;
|
|
5935
|
+
try {
|
|
5936
|
+
localStream = this.localStream ?? await this.localStreamController.buildLocalStream();
|
|
5937
|
+
} catch (error) {
|
|
5938
|
+
this.handleLocalMediaFailure(error);
|
|
5939
|
+
this._localMediaSettled$.next();
|
|
5940
|
+
return true;
|
|
5941
|
+
}
|
|
5942
|
+
if (!this.peerConnection || this.peerConnection.signalingState === "closed") {
|
|
5943
|
+
logger$17.debug("[RTCPeerConnectionController] Local media arrived after teardown; releasing it.");
|
|
5944
|
+
localStream.getTracks().forEach((track) => track.stop());
|
|
5945
|
+
return false;
|
|
5946
|
+
}
|
|
5947
|
+
this._localMediaSettled$.next();
|
|
5749
5948
|
if (this.transceiverController?.useAddStream ?? false) {
|
|
5750
5949
|
logger$17.warn("[RTCPeerConnectionController] Using deprecated addStream API to add local stream.");
|
|
5751
|
-
this.peerConnection
|
|
5950
|
+
this.peerConnection.addStream(localStream);
|
|
5752
5951
|
if (!this.isNegotiating) {
|
|
5753
5952
|
logger$17.debug("[RTCPeerConnectionController] Forcing negotiationneeded after local tracks setup.");
|
|
5754
5953
|
this.negotiationNeeded$.next();
|
|
5755
5954
|
}
|
|
5756
|
-
return;
|
|
5955
|
+
return true;
|
|
5757
5956
|
}
|
|
5758
5957
|
for (const kind of ["audio", "video"]) {
|
|
5759
5958
|
const tracks = (kind === "audio" ? localStream.getAudioTracks() : localStream.getVideoTracks()).map((track, index) => ({
|
|
@@ -5767,10 +5966,53 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5767
5966
|
await this.transceiverController?.setupTransceiverSender(track, localStream, transceivers[index]);
|
|
5768
5967
|
} else {
|
|
5769
5968
|
logger$17.debug(`[RTCPeerConnectionController] Using addTrack for local ${kind} track:`, track.id);
|
|
5770
|
-
this.peerConnection
|
|
5969
|
+
this.peerConnection.addTrack(track, localStream);
|
|
5771
5970
|
}
|
|
5772
5971
|
}
|
|
5773
5972
|
}
|
|
5973
|
+
return true;
|
|
5974
|
+
}
|
|
5975
|
+
/** True for a main connection with no local media to send. */
|
|
5976
|
+
hasNoLocalMediaToSend() {
|
|
5977
|
+
const hasInputStreams = Boolean(this.options.inputAudioStream ?? this.options.inputVideoStream);
|
|
5978
|
+
return this.propose === "main" && !this.localStream && !hasInputStreams && !this.inputAudioDeviceConstraints && !this.inputVideoDeviceConstraints;
|
|
5979
|
+
}
|
|
5980
|
+
/** The media kinds this connection wants to send: 'audiovideo' | 'video' | 'audio'. */
|
|
5981
|
+
get requestedMediaKinds() {
|
|
5982
|
+
const wantsAudio = Boolean(this.inputAudioDeviceConstraints);
|
|
5983
|
+
const wantsVideo = Boolean(this.inputVideoDeviceConstraints);
|
|
5984
|
+
if (wantsAudio && wantsVideo) return "audiovideo";
|
|
5985
|
+
return wantsVideo ? "video" : "audio";
|
|
5986
|
+
}
|
|
5987
|
+
/**
|
|
5988
|
+
* Handle a local media acquisition failure with a typed, semantically
|
|
5989
|
+
* accurate MediaAccessError created at the acquisition site:
|
|
5990
|
+
* - Auxiliary connections (screenshare / additional-device) throw a
|
|
5991
|
+
* non-fatal error — VertoManager surfaces it and the call is unaffected.
|
|
5992
|
+
* - The main connection degrades to receive-only when allowed (default),
|
|
5993
|
+
* otherwise fails with a fatal error.
|
|
5994
|
+
*/
|
|
5995
|
+
handleLocalMediaFailure(error) {
|
|
5996
|
+
if (this.propose === "screenshare") throw new require_operators.MediaAccessError("startScreenShare", "screen", error, false);
|
|
5997
|
+
if (this.propose === "additional-device") throw new require_operators.MediaAccessError("addInputDevice", this.requestedMediaKinds, error, false);
|
|
5998
|
+
const canReceive = this.receiveAudio || this.receiveVideo;
|
|
5999
|
+
if (!((this.options.fallbackToReceiveOnly ?? true) && canReceive)) throw new require_operators.MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, true);
|
|
6000
|
+
logger$17.warn("[RTCPeerConnectionController] Local media unavailable; continuing receive-only:", error);
|
|
6001
|
+
this._errors$.next(new require_operators.MediaAccessError("acquireLocalMedia", this.requestedMediaKinds, error, false));
|
|
6002
|
+
this.setupReceiveOnlyTransceivers();
|
|
6003
|
+
}
|
|
6004
|
+
/**
|
|
6005
|
+
* Negotiate receive-only m-lines when there are no local tracks to send.
|
|
6006
|
+
* Only offer-type connections add transceivers — answer-type connections
|
|
6007
|
+
* reuse the transceivers created from the remote offer.
|
|
6008
|
+
*/
|
|
6009
|
+
setupReceiveOnlyTransceivers() {
|
|
6010
|
+
if (this.type !== "offer") return;
|
|
6011
|
+
if (this.transceiverController?.useAddTransceivers ?? false) {
|
|
6012
|
+
this.peerConnection?.addTransceiver("audio", { direction: this.receiveAudio ? "recvonly" : "inactive" });
|
|
6013
|
+
this.peerConnection?.addTransceiver("video", { direction: this.receiveVideo ? "recvonly" : "inactive" });
|
|
6014
|
+
}
|
|
6015
|
+
if (!this.isNegotiating) this.negotiationNeeded$.next();
|
|
5774
6016
|
}
|
|
5775
6017
|
async getUserMedia(constraints) {
|
|
5776
6018
|
return (this.options.webRTCApiProvider?.mediaDevices ?? navigator.mediaDevices).getUserMedia(constraints);
|
|
@@ -5808,7 +6050,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5808
6050
|
stream = await this.getUserMedia({ audio: constraints });
|
|
5809
6051
|
} catch (error) {
|
|
5810
6052
|
logger$17.error("[RTCPeerConnectionController] Failed to re-acquire mic for pipeline restore:", error);
|
|
5811
|
-
this._errors$.next(
|
|
6053
|
+
this._errors$.next(new require_operators.MediaTrackError("restoreAudioPipelineInput", "audio", error));
|
|
5812
6054
|
return;
|
|
5813
6055
|
}
|
|
5814
6056
|
const newTrack = stream.getAudioTracks().at(0);
|
|
@@ -5817,6 +6059,61 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5817
6059
|
this._localAudioPipeline.setInputTrack(newTrack);
|
|
5818
6060
|
}
|
|
5819
6061
|
/**
|
|
6062
|
+
* Capture the newly selected device, leaving the current capture running.
|
|
6063
|
+
*
|
|
6064
|
+
* A rejection must leave the current track sending, so nothing is released
|
|
6065
|
+
* until the replacement is in hand. The one exception is hardware that admits
|
|
6066
|
+
* a single opener — a phone's front and back cameras, typically — which
|
|
6067
|
+
* rejects the second capture until the first is closed.
|
|
6068
|
+
*/
|
|
6069
|
+
async acquireInputTrack(kind, constraints, deviceInfo, currentTrack) {
|
|
6070
|
+
try {
|
|
6071
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
6072
|
+
} catch (error) {
|
|
6073
|
+
if (!require_operators.isMediaDeviceInUse(error)) throw error;
|
|
6074
|
+
logger$17.warn(`[RTCPeerConnectionController] ${kind} device is held exclusively; releasing the current capture to retry:`, error);
|
|
6075
|
+
const previousDeviceId = currentTrack.getSettings().deviceId;
|
|
6076
|
+
this.stopTrackSender(kind);
|
|
6077
|
+
try {
|
|
6078
|
+
return await this.captureTrack(kind, constraints, deviceInfo.deviceId);
|
|
6079
|
+
} catch (retryError) {
|
|
6080
|
+
await this.restorePreviousInputTrack(kind, constraints, previousDeviceId, currentTrack);
|
|
6081
|
+
throw retryError;
|
|
6082
|
+
}
|
|
6083
|
+
}
|
|
6084
|
+
}
|
|
6085
|
+
async captureTrack(kind, constraints, deviceId) {
|
|
6086
|
+
const { stream, fallbackLevel } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { [kind]: constraints }, kind, deviceId);
|
|
6087
|
+
const track = stream.getTracks().find((t) => t.kind === kind);
|
|
6088
|
+
if (!track) {
|
|
6089
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
6090
|
+
throw new require_operators.MediaTrackError("updateSelectedInputDevice", kind, /* @__PURE__ */ new Error("getUserMedia returned no track of the requested kind"));
|
|
6091
|
+
}
|
|
6092
|
+
if (fallbackLevel !== "exact") logger$17.warn(`[RTCPeerConnectionController] ${kind} device acquired at fallback level '${fallbackLevel}'; the capture may not be the requested device.`);
|
|
6093
|
+
return track;
|
|
6094
|
+
}
|
|
6095
|
+
/** Best-effort return to the device that was released for an exclusive retry. */
|
|
6096
|
+
async restorePreviousInputTrack(kind, constraints, previousDeviceId, releasedTrack) {
|
|
6097
|
+
try {
|
|
6098
|
+
const restored = await this.captureTrack(kind, constraints, previousDeviceId);
|
|
6099
|
+
await this.attachInputTrack(kind, restored, releasedTrack);
|
|
6100
|
+
} catch (error) {
|
|
6101
|
+
logger$17.error(`[RTCPeerConnectionController] Failed to restore the previous ${kind} device:`, error);
|
|
6102
|
+
}
|
|
6103
|
+
}
|
|
6104
|
+
async attachInputTrack(kind, newTrack, oldTrack) {
|
|
6105
|
+
const pipelineOwnsAudio = kind === "audio" && this._localAudioPipeline;
|
|
6106
|
+
if (!pipelineOwnsAudio) try {
|
|
6107
|
+
await this.transceiverController?.replaceSenderTrack(kind, newTrack);
|
|
6108
|
+
} catch (error) {
|
|
6109
|
+
newTrack.stop();
|
|
6110
|
+
throw error;
|
|
6111
|
+
}
|
|
6112
|
+
this.localStreamController.removeTrack(oldTrack.id);
|
|
6113
|
+
this.localStreamController.addTrack(newTrack);
|
|
6114
|
+
if (pipelineOwnsAudio) this._localAudioPipeline?.setInputTrack(newTrack);
|
|
6115
|
+
}
|
|
6116
|
+
/**
|
|
5820
6117
|
* Return the lazily-created {@link LocalAudioPipeline}, constructing it on
|
|
5821
6118
|
* first access. On creation the current audio sender's track is routed
|
|
5822
6119
|
* through the pipeline (input → gain → analyser → destination) and the
|
|
@@ -5849,6 +6146,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5849
6146
|
const sender = (this.transceiverController?.audioTransceivers.at(0))?.sender ?? this.peerConnection.getSenders().find((s) => s.track?.kind === "audio");
|
|
5850
6147
|
if (!sender || !raw) return;
|
|
5851
6148
|
try {
|
|
6149
|
+
this.localStreamController.setTrackOrigin(this._localAudioPipeline.outputTrack, "processed");
|
|
5852
6150
|
await sender.replaceTrack(this._localAudioPipeline.outputTrack);
|
|
5853
6151
|
} catch (error) {
|
|
5854
6152
|
logger$17.warn("[RTCPeerConnectionController] Failed to route audio sender through pipeline:", error);
|
|
@@ -5870,7 +6168,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5870
6168
|
logger$17.debug(`[RTCPeerConnectionController] ${track.kind} track added:`, track.id);
|
|
5871
6169
|
} catch (error) {
|
|
5872
6170
|
logger$17.error(`[RTCPeerConnectionController] Failed to add ${track.kind} track:`, error);
|
|
5873
|
-
this._errors$.next(
|
|
6171
|
+
this._errors$.next(new require_operators.MediaTrackError("addLocalTrack", track.kind, error));
|
|
5874
6172
|
throw error;
|
|
5875
6173
|
}
|
|
5876
6174
|
}
|
|
@@ -5895,7 +6193,7 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5895
6193
|
logger$17.debug(`[RTCPeerConnectionController] ${sender.track?.kind} track removed:`, trackId);
|
|
5896
6194
|
} catch (error) {
|
|
5897
6195
|
logger$17.error(`[RTCPeerConnectionController] Failed to remove ${sender.track?.kind} track:`, error);
|
|
5898
|
-
this._errors$.next(
|
|
6196
|
+
this._errors$.next(new require_operators.MediaTrackError("removeLocalTrack", sender.track?.kind ?? "unknown", error));
|
|
5899
6197
|
throw error;
|
|
5900
6198
|
}
|
|
5901
6199
|
}
|
|
@@ -5909,37 +6207,68 @@ var RTCPeerConnectionController = class extends Destroyable {
|
|
|
5909
6207
|
for (const existingTrack of existingTracks) this.removeLocalTrack(existingTrack.id);
|
|
5910
6208
|
this.addLocalTrack(track);
|
|
5911
6209
|
}
|
|
6210
|
+
/**
|
|
6211
|
+
* @returns whether the constraints reached the media the leg is sending.
|
|
6212
|
+
*
|
|
6213
|
+
* With the pipeline engaged the audio sender carries the processed
|
|
6214
|
+
* destination track, so the sender scan would find nothing it may touch and
|
|
6215
|
+
* every audio constraint API would silently no-op. The constraints belong to
|
|
6216
|
+
* the pipeline's device source, which is the capture that sender ultimately
|
|
6217
|
+
* carries.
|
|
6218
|
+
*/
|
|
5912
6219
|
async updateSendersConstraints(kind, constraints) {
|
|
5913
|
-
|
|
6220
|
+
if (kind === "audio" && this._localAudioPipeline) {
|
|
6221
|
+
if (!constraints) {
|
|
6222
|
+
this.stopTrackSender("audio");
|
|
6223
|
+
return false;
|
|
6224
|
+
}
|
|
6225
|
+
return this.applyPipelineSourceConstraints(constraints);
|
|
6226
|
+
}
|
|
6227
|
+
return await this.transceiverController?.updateSendersConstraints(kind, constraints) ?? false;
|
|
5914
6228
|
}
|
|
5915
6229
|
/**
|
|
5916
|
-
*
|
|
5917
|
-
*
|
|
5918
|
-
*
|
|
5919
|
-
*
|
|
6230
|
+
* Mirror of the sender path for a piped audio leg: same merge, same fallback
|
|
6231
|
+
* ladder, same device-capture invariant — but the swap target is the pipeline
|
|
6232
|
+
* input, so the sender keeps emitting the pipeline's output track and its
|
|
6233
|
+
* identity survives the change.
|
|
5920
6234
|
*/
|
|
5921
|
-
async
|
|
5922
|
-
const
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
6235
|
+
async applyPipelineSourceConstraints(constraints) {
|
|
6236
|
+
const pipeline = this._localAudioPipeline;
|
|
6237
|
+
const source = this.localStreamController.localAudioTracks.at(0);
|
|
6238
|
+
if (!pipeline || !source) {
|
|
6239
|
+
logger$17.debug("[RTCPeerConnectionController] No pipeline input to constrain.");
|
|
6240
|
+
return false;
|
|
5926
6241
|
}
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
6242
|
+
if (!this.localStreamController.isDeviceCapture(source)) {
|
|
6243
|
+
logger$17.debug(`[RTCPeerConnectionController] Skipping audio constraints for a non-device pipeline input (origin: ${this.localStreamController.getTrackOrigin(source) ?? "unrecorded"}).`);
|
|
6244
|
+
return false;
|
|
6245
|
+
}
|
|
6246
|
+
const merged = {
|
|
6247
|
+
...source.getConstraints(),
|
|
6248
|
+
...constraints
|
|
6249
|
+
};
|
|
6250
|
+
try {
|
|
6251
|
+
await source.applyConstraints(merged);
|
|
6252
|
+
logger$17.debug("[RTCPeerConnectionController] Pipeline input constraints updated:", merged);
|
|
6253
|
+
return true;
|
|
6254
|
+
} catch (error) {
|
|
6255
|
+
logger$17.warn("[RTCPeerConnectionController] applyConstraints failed on the pipeline input, re-acquiring:", error);
|
|
6256
|
+
}
|
|
6257
|
+
try {
|
|
6258
|
+
const { stream } = await getUserMediaWithFallback({ getUserMedia: async (c) => this.getUserMedia(c) }, { audio: merged }, "audio", source.getSettings().deviceId);
|
|
6259
|
+
const newTrack = stream.getAudioTracks().at(0);
|
|
6260
|
+
if (!newTrack) {
|
|
6261
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
6262
|
+
throw new Error("getUserMedia returned no audio track");
|
|
6263
|
+
}
|
|
6264
|
+
this.localStreamController.removeTrack(source.id);
|
|
5941
6265
|
this.localStreamController.addTrack(newTrack);
|
|
5942
|
-
|
|
6266
|
+
pipeline.setInputTrack(newTrack);
|
|
6267
|
+
return true;
|
|
6268
|
+
} catch (error) {
|
|
6269
|
+
logger$17.warn("[RTCPeerConnectionController] Failed to re-acquire the pipeline input for constraints:", error);
|
|
6270
|
+
this._errors$.next(new require_operators.MediaTrackError("updateSendersConstraints", "audio", error));
|
|
6271
|
+
return false;
|
|
5943
6272
|
}
|
|
5944
6273
|
}
|
|
5945
6274
|
/**
|
|
@@ -6058,10 +6387,52 @@ const logger$16 = require_operators.getLogger();
|
|
|
6058
6387
|
function resolveInviteNodeId(args) {
|
|
6059
6388
|
return args.isInvite && !args.reattach && !args.explicitNodeId ? "" : args.currentNodeId ?? "";
|
|
6060
6389
|
}
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6390
|
+
/**
|
|
6391
|
+
* Surface the real outcome of a `webrtc.verto` reply.
|
|
6392
|
+
*
|
|
6393
|
+
* A webrtc.verto response nests several envelopes, each keyed by a verto-style
|
|
6394
|
+
* string `code` ("200" ok, "400"/etc. fail) rather than a JSON-RPC `error`. An outer
|
|
6395
|
+
* layer reports only whether the frame was delivered; an inner layer carries the op's
|
|
6396
|
+
* own outcome:
|
|
6397
|
+
*
|
|
6398
|
+
* response.result = { code:"200", result:{…} } ← delivery acknowledgement
|
|
6399
|
+
* .result = { jsonrpc, id, result:{…} } ← the reply payload
|
|
6400
|
+
* .result = { code:"400", message:"Bad request" } ← the actual op outcome
|
|
6401
|
+
*
|
|
6402
|
+
* A failure can appear at any layer (delivery refused, or the op itself rejected
|
|
6403
|
+
* deeper down), so walk every nested `.result` object and return the FIRST non-2xx
|
|
6404
|
+
* `code` with its message. Returns null when every `code` seen is 2xx or absent —
|
|
6405
|
+
* i.e. the op succeeded. This is the only way to detect that e.g. a mute/kick was
|
|
6406
|
+
* rejected, since the outer delivery `code` is "200" (delivered) even then.
|
|
6407
|
+
*
|
|
6408
|
+
* Pure function — exported for unit testing.
|
|
6409
|
+
*/
|
|
6410
|
+
function findNestedVertoFailure(response) {
|
|
6411
|
+
let node = response;
|
|
6412
|
+
while (node !== null && typeof node === "object") {
|
|
6413
|
+
const obj = node;
|
|
6414
|
+
const err = obj.error;
|
|
6415
|
+
if (err !== null && typeof err === "object") {
|
|
6416
|
+
const e = err;
|
|
6417
|
+
const errCode = typeof e.code === "string" || typeof e.code === "number" ? String(e.code) : void 0;
|
|
6418
|
+
if (errCode !== void 0 && !/^2\d\d$/.test(errCode)) return {
|
|
6419
|
+
code: errCode,
|
|
6420
|
+
message: typeof e.message === "string" ? e.message : void 0
|
|
6421
|
+
};
|
|
6422
|
+
}
|
|
6423
|
+
const code = typeof obj.code === "string" || typeof obj.code === "number" ? String(obj.code) : void 0;
|
|
6424
|
+
if (code !== void 0 && !/^2\d\d$/.test(code)) return {
|
|
6425
|
+
code,
|
|
6426
|
+
message: typeof obj.message === "string" ? obj.message : void 0
|
|
6427
|
+
};
|
|
6428
|
+
node = obj.result !== null && typeof obj.result === "object" ? obj.result : null;
|
|
6429
|
+
}
|
|
6430
|
+
return null;
|
|
6431
|
+
}
|
|
6432
|
+
var VertoManager = class extends Destroyable {
|
|
6433
|
+
constructor(callSession) {
|
|
6434
|
+
super();
|
|
6435
|
+
this.callSession = callSession;
|
|
6065
6436
|
}
|
|
6066
6437
|
destroy() {
|
|
6067
6438
|
this.callSession = void 0;
|
|
@@ -6080,7 +6451,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6080
6451
|
this._signalingStatus$ = this.createReplaySubject(1);
|
|
6081
6452
|
this._screenShareStatus$ = this.createBehaviorSubject("none");
|
|
6082
6453
|
this._rtcPeerConnectionsMap = /* @__PURE__ */ new Map();
|
|
6083
|
-
this.
|
|
6454
|
+
this._legErrors$ = this.createSubject();
|
|
6084
6455
|
this._nodeId$ = this.createBehaviorSubject(options.nodeId ?? null);
|
|
6085
6456
|
this.onError = options.onError;
|
|
6086
6457
|
this.onModifyFailed = options.onModifyFailed;
|
|
@@ -6128,6 +6499,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6128
6499
|
get selfId$() {
|
|
6129
6500
|
return this._selfId$.asObservable();
|
|
6130
6501
|
}
|
|
6502
|
+
/** Separates the media phase of call creation from the signalling phase. */
|
|
6503
|
+
get localMediaSettled$() {
|
|
6504
|
+
return this.mainPeerConnection.localMediaSettled$;
|
|
6505
|
+
}
|
|
6131
6506
|
get localStream() {
|
|
6132
6507
|
return this._rtcPeerConnectionsMap.get(this.webRtcCallSession.id)?.localStream ?? null;
|
|
6133
6508
|
}
|
|
@@ -6184,35 +6559,95 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6184
6559
|
const { mediaParams, callID } = event;
|
|
6185
6560
|
const rtcPeerConnController = this._rtcPeerConnectionsMap.get(callID);
|
|
6186
6561
|
const { audio, video } = mediaParams;
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
audio,
|
|
6193
|
-
video,
|
|
6194
|
-
timestamp: Date.now()
|
|
6195
|
-
});
|
|
6196
|
-
} catch (error) {
|
|
6197
|
-
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", error);
|
|
6198
|
-
this.onError?.(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
6199
|
-
}
|
|
6200
|
-
})();
|
|
6562
|
+
if (!rtcPeerConnController) {
|
|
6563
|
+
logger$16.warn(`[WebRTCManager] Ignoring server-pushed media params for unknown leg ${callID}`);
|
|
6564
|
+
return;
|
|
6565
|
+
}
|
|
6566
|
+
this.applyServerMediaParams(rtcPeerConnController, audio, video);
|
|
6201
6567
|
});
|
|
6202
6568
|
this.subscribeTo(this.vertoPing$, (vertoPing) => {
|
|
6203
|
-
this.attachManager.
|
|
6569
|
+
this.attachManager.refresh(this.buildAttachableCall());
|
|
6204
6570
|
this.sendVertoPong(vertoPing);
|
|
6205
6571
|
});
|
|
6206
6572
|
}
|
|
6207
6573
|
/**
|
|
6574
|
+
* An auxiliary-leg failure must never destroy the call; main-leg and
|
|
6575
|
+
* call-level errors keep `CallFactory.isFatalError`'s classification.
|
|
6576
|
+
*
|
|
6577
|
+
* Every site holding a peer connection reports through here, so the invariant
|
|
6578
|
+
* is structural rather than per-call-site — which is how cloud-product#20523
|
|
6579
|
+
* happened, with only one of fourteen sites passing `{ fatal: false }`.
|
|
6580
|
+
*
|
|
6581
|
+
* `override` composes rather than replaces: a caller may force non-fatal for a
|
|
6582
|
+
* reason of its own (a `verto.info` frame is best-effort whichever leg carries
|
|
6583
|
+
* it), and an auxiliary leg stays non-fatal regardless.
|
|
6584
|
+
*/
|
|
6585
|
+
reportLegError(error, rtcPeerConnController, override) {
|
|
6586
|
+
const leg = rtcPeerConnController?.propose;
|
|
6587
|
+
const legId = rtcPeerConnController?.id;
|
|
6588
|
+
const auxiliary = Boolean(rtcPeerConnController) && !rtcPeerConnController?.isMainDevice;
|
|
6589
|
+
this.onError?.(error, {
|
|
6590
|
+
...override?.fatal === false || auxiliary ? { fatal: false } : {},
|
|
6591
|
+
...leg ? { leg } : {},
|
|
6592
|
+
...legId ? { legId } : {}
|
|
6593
|
+
});
|
|
6594
|
+
if (legId) this._legErrors$.next({
|
|
6595
|
+
legId,
|
|
6596
|
+
error
|
|
6597
|
+
});
|
|
6598
|
+
}
|
|
6599
|
+
/**
|
|
6600
|
+
* Errors reported for one leg, as a stream that fails with them.
|
|
6601
|
+
*
|
|
6602
|
+
* Signaling failures are reported, never thrown — so nothing that waits on a
|
|
6603
|
+
* leg's progress would otherwise learn of a rejected invite. Merging this in
|
|
6604
|
+
* lets the wait end with the reason the server gave.
|
|
6605
|
+
*/
|
|
6606
|
+
legError$(legId) {
|
|
6607
|
+
return this._legErrors$.pipe((0, rxjs.filter)((report) => report.legId === legId), (0, rxjs.map)((report) => {
|
|
6608
|
+
throw report.error;
|
|
6609
|
+
}));
|
|
6610
|
+
}
|
|
6611
|
+
/**
|
|
6612
|
+
* Audio and video are applied independently so a failure in one cannot
|
|
6613
|
+
* suppress the other, and `mediaParamsUpdated` is emitted whatever happens —
|
|
6614
|
+
* an application should not be starved of the params by a constraint failure.
|
|
6615
|
+
*/
|
|
6616
|
+
async applyServerMediaParams(rtcPeerConnController, audio, video) {
|
|
6617
|
+
const failures = [];
|
|
6618
|
+
let applied = true;
|
|
6619
|
+
if (audio) try {
|
|
6620
|
+
applied = await rtcPeerConnController.updateSendersConstraints("audio", audio) && applied;
|
|
6621
|
+
} catch (error) {
|
|
6622
|
+
applied = false;
|
|
6623
|
+
failures.push(toError(error));
|
|
6624
|
+
}
|
|
6625
|
+
if (video) try {
|
|
6626
|
+
applied = await rtcPeerConnController.updateSendersConstraints("video", video) && applied;
|
|
6627
|
+
} catch (error) {
|
|
6628
|
+
applied = false;
|
|
6629
|
+
failures.push(toError(error));
|
|
6630
|
+
}
|
|
6631
|
+
this.webRtcCallSession.emitMediaParamsUpdated({
|
|
6632
|
+
audio,
|
|
6633
|
+
video,
|
|
6634
|
+
timestamp: Date.now(),
|
|
6635
|
+
applied
|
|
6636
|
+
});
|
|
6637
|
+
for (const failure of failures) {
|
|
6638
|
+
logger$16.warn("[WebRTCManager] Error applying server-pushed media params:", failure);
|
|
6639
|
+
this.reportLegError(failure, rtcPeerConnController, { fatal: false });
|
|
6640
|
+
}
|
|
6641
|
+
}
|
|
6642
|
+
/**
|
|
6208
6643
|
* Set node_id/selfId only when the current value is null.
|
|
6209
6644
|
*
|
|
6210
6645
|
* During reattach, `call.joined` and `verto.answer` events can deliver
|
|
6211
6646
|
* these identifiers before the `verto.invite` RPC response (`CALL CREATED`)
|
|
6212
6647
|
* arrives. These methods let early events populate them eagerly so that
|
|
6213
6648
|
* downstream RPC calls (e.g. `call.layout.list`) don't fail with empty
|
|
6214
|
-
* identifiers. `processInviteResponse()` remains the authoritative source
|
|
6215
|
-
*
|
|
6649
|
+
* identifiers. `processInviteResponse()` remains the authoritative source and
|
|
6650
|
+
* overwrites unconditionally — for selfId, on the main leg only.
|
|
6216
6651
|
*/
|
|
6217
6652
|
setNodeIdIfNull(nodeId) {
|
|
6218
6653
|
if (!this._nodeId$.value && nodeId) {
|
|
@@ -6235,24 +6670,33 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6235
6670
|
this.onError?.(new require_operators.VertoPongError(error));
|
|
6236
6671
|
}
|
|
6237
6672
|
}
|
|
6673
|
+
/**
|
|
6674
|
+
* @returns whether the constraints reached the media the call is sending.
|
|
6675
|
+
* `false` is an outcome, not a failure: the leg may have no live sender of
|
|
6676
|
+
* the kind, or carry media the SDK did not capture and may not replace.
|
|
6677
|
+
* A failure behind it still reaches the call's `errors$`, so a caller that
|
|
6678
|
+
* ignores this value learns of it there.
|
|
6679
|
+
*/
|
|
6238
6680
|
async updateMediaConstraints(options = {}) {
|
|
6239
6681
|
const { audio, video } = options;
|
|
6682
|
+
let applied = true;
|
|
6240
6683
|
try {
|
|
6241
|
-
if (audio) await this.mainPeerConnection.updateSendersConstraints("audio", audio);
|
|
6242
|
-
if (video) await this.mainPeerConnection.updateSendersConstraints("video", video);
|
|
6684
|
+
if (audio) applied = await this.mainPeerConnection.updateSendersConstraints("audio", audio) && applied;
|
|
6685
|
+
if (video) applied = await this.mainPeerConnection.updateSendersConstraints("video", video) && applied;
|
|
6243
6686
|
} catch (error) {
|
|
6244
6687
|
logger$16.warn("[WebRTCManager] Error updating media constraints:", error);
|
|
6245
|
-
this.
|
|
6688
|
+
this.reportLegError(toError(error), this.mainPeerConnection);
|
|
6246
6689
|
throw error;
|
|
6247
6690
|
}
|
|
6691
|
+
return applied;
|
|
6248
6692
|
}
|
|
6249
6693
|
get selfId() {
|
|
6250
6694
|
return this._selfId$.value;
|
|
6251
6695
|
}
|
|
6252
6696
|
/** Build an AttachableCall from the current call state. */
|
|
6253
|
-
buildAttachableCall(idOverride) {
|
|
6697
|
+
buildAttachableCall(idOverride, nodeIdOverride) {
|
|
6254
6698
|
return {
|
|
6255
|
-
nodeId: this.nodeId ?? void 0,
|
|
6699
|
+
nodeId: nodeIdOverride ?? this.nodeId ?? void 0,
|
|
6256
6700
|
id: idOverride ?? this.webRtcCallSession.id,
|
|
6257
6701
|
to: this.webRtcCallSession.to,
|
|
6258
6702
|
mediaDirections: this.webRtcCallSession.mediaDirections
|
|
@@ -6382,24 +6826,49 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6382
6826
|
get vertoPing$() {
|
|
6383
6827
|
return this.cachedObservable("vertoPing$", () => this.webRtcCallSession.webrtcMessages$.pipe(require_operators.filterAs(isVertoPingInnerParams, "params"), (0, rxjs.takeUntil)(this.destroyed$)));
|
|
6384
6828
|
}
|
|
6829
|
+
/**
|
|
6830
|
+
* Send a member-control op in-dialog via verto.info.
|
|
6831
|
+
*
|
|
6832
|
+
* The control payload rides in `params.command` — a sibling of `dialogParams`,
|
|
6833
|
+
* at the same level as `dtmf` in {@link sendDigits} — and the inner verto.info
|
|
6834
|
+
* is matched to this call's channel by `dialogParams.callID`, the in-dialog
|
|
6835
|
+
* convention for member-scoped frames. Because it is delivered on the dialog
|
|
6836
|
+
* itself, control lands on the call's own channel with no {node_id,call_id,member_id}
|
|
6837
|
+
* "self" tuple to get wrong. The outer webrtc.verto envelope (added by executeVerto)
|
|
6838
|
+
* still carries the own-leg callID + node_id for session routing.
|
|
6839
|
+
*
|
|
6840
|
+
* Keep `command` OUT of `dialogParams`: it is read at the params level, and
|
|
6841
|
+
* filterVertoParams rewrites/filters dialogParams keys but passes params-level
|
|
6842
|
+
* keys through verbatim.
|
|
6843
|
+
*/
|
|
6844
|
+
async sendCallControl(method, params) {
|
|
6845
|
+
const response = await this.executeVerto(VertoInfo({
|
|
6846
|
+
dialogParams: { callID: this.webRtcCallSession.id },
|
|
6847
|
+
command: {
|
|
6848
|
+
method,
|
|
6849
|
+
params
|
|
6850
|
+
}
|
|
6851
|
+
}));
|
|
6852
|
+
const failure = findNestedVertoFailure(response);
|
|
6853
|
+
if (failure) throw new require_operators.JSONRPCError(Number.parseInt(failure.code, 10) || 0, `Call control "${method}" failed (code ${failure.code})${failure.message ? `: ${failure.message}` : ""}`, void 0);
|
|
6854
|
+
return response;
|
|
6855
|
+
}
|
|
6385
6856
|
async executeVerto(message, optionals = {}) {
|
|
6386
|
-
const
|
|
6857
|
+
const params = {
|
|
6387
6858
|
callID: optionals.callID ?? this.webRtcCallSession.id,
|
|
6388
6859
|
node_id: optionals.node_id ?? this._nodeId$.value ?? "",
|
|
6389
6860
|
message,
|
|
6390
6861
|
subscribe: optionals.subscribe
|
|
6391
|
-
}
|
|
6862
|
+
};
|
|
6863
|
+
const webrtcVertoMessage = WebrtcVerto(params);
|
|
6392
6864
|
const response = await this.webRtcCallSession.execute(webrtcVertoMessage);
|
|
6393
|
-
|
|
6394
|
-
const error = new require_operators.JSONRPCError(response.error.code, response.error.message, response.error.data);
|
|
6395
|
-
this.onError?.(error);
|
|
6396
|
-
return response;
|
|
6397
|
-
}
|
|
6865
|
+
const nonFatal = message.method === "verto.info" ? { fatal: false } : void 0;
|
|
6398
6866
|
const innerResult = require_operators.getValueFrom(response, "result.result");
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
|
|
6402
|
-
|
|
6867
|
+
const failure = response.error ?? innerResult?.error;
|
|
6868
|
+
if (failure) {
|
|
6869
|
+
const error = new require_operators.JSONRPCError(failure.code, failure.message, failure.data);
|
|
6870
|
+
if (message.method === "verto.invite" || message.method === "verto.answer") throw error;
|
|
6871
|
+
this.reportLegError(error, this._rtcPeerConnectionsMap.get(params.callID), nonFatal);
|
|
6403
6872
|
}
|
|
6404
6873
|
return response;
|
|
6405
6874
|
}
|
|
@@ -6418,8 +6887,9 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6418
6887
|
default:
|
|
6419
6888
|
}
|
|
6420
6889
|
} catch (error) {
|
|
6890
|
+
if (vertoMethod === "verto.answer") throw error;
|
|
6421
6891
|
logger$16.error(`[WebRTCManager] Error sending Verto ${vertoMethod}:`, error);
|
|
6422
|
-
this.
|
|
6892
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
6423
6893
|
if (vertoMethod === "verto.modify") this.onModifyFailed?.();
|
|
6424
6894
|
}
|
|
6425
6895
|
}
|
|
@@ -6435,7 +6905,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6435
6905
|
} catch (error) {
|
|
6436
6906
|
logger$16.warn("[WebRTCManager] Error processing modify response:", error);
|
|
6437
6907
|
const modifyError = error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
6438
|
-
this.
|
|
6908
|
+
this.reportLegError(modifyError, rtcPeerConnController);
|
|
6439
6909
|
}
|
|
6440
6910
|
}
|
|
6441
6911
|
}
|
|
@@ -6448,37 +6918,35 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6448
6918
|
status,
|
|
6449
6919
|
signalingError
|
|
6450
6920
|
});
|
|
6451
|
-
this.
|
|
6921
|
+
this.reportLegError(signalingError, null, { fatal: false });
|
|
6452
6922
|
return;
|
|
6453
6923
|
}
|
|
6454
6924
|
if (rtcPeerConnController.isMainDevice) this._signalingStatus$.next(status);
|
|
6455
6925
|
}
|
|
6456
6926
|
processInviteResponse(response, rtcPeerConnController) {
|
|
6457
|
-
if (
|
|
6927
|
+
if (require_operators.getValueFrom(response, "result.result.result.message") === "CALL CREATED") {
|
|
6458
6928
|
this.emitMainSignalingStatus(rtcPeerConnController.id, "trying");
|
|
6459
|
-
|
|
6929
|
+
const nodeId = require_operators.getValueFrom(response, "result.node_id") ?? null;
|
|
6460
6930
|
const memberId = require_operators.getValueFrom(response, "result.result.result.memberID") ?? null;
|
|
6461
|
-
const callId = require_operators.getValueFrom(response, "result.result.result.callID")
|
|
6931
|
+
const callId = require_operators.getValueFrom(response, "result.result.result.callID");
|
|
6462
6932
|
logger$16.debug("[WebRTCManager] Verto invite response:", {
|
|
6463
6933
|
callId,
|
|
6464
6934
|
memberId,
|
|
6465
6935
|
response
|
|
6466
6936
|
});
|
|
6467
|
-
this._selfId$.next(memberId);
|
|
6468
6937
|
rtcPeerConnController.setMemberId(memberId);
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
this.
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6938
|
+
rtcPeerConnController.setNodeId(nodeId);
|
|
6939
|
+
if (rtcPeerConnController.isMainDevice) {
|
|
6940
|
+
this._selfId$.next(memberId);
|
|
6941
|
+
this._nodeId$.next(nodeId);
|
|
6942
|
+
this.attachManager.attach(this.buildAttachableCall(callId, nodeId ?? void 0));
|
|
6943
|
+
}
|
|
6944
|
+
if (callId) this.webRtcCallSession.addCallId(callId);
|
|
6476
6945
|
logger$16.info("[WebRTCManager] Verto invite successful");
|
|
6477
6946
|
logger$16.debug(`[WebRTCManager] nodeid: ${this._nodeId$.value}, selfId: ${this._selfId$.value}`);
|
|
6478
6947
|
} else {
|
|
6479
6948
|
logger$16.error("[WebRTCManager] Verto invite failed:", response);
|
|
6480
|
-
|
|
6481
|
-
this.onError?.(inviteError);
|
|
6949
|
+
this.reportLegError(/* @__PURE__ */ new Error("Verto invite failed: unexpected response"), rtcPeerConnController);
|
|
6482
6950
|
}
|
|
6483
6951
|
}
|
|
6484
6952
|
get RTCPeerConnectionConfig() {
|
|
@@ -6503,6 +6971,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6503
6971
|
inputVideoStream: options.inputVideoStream,
|
|
6504
6972
|
receiveAudio: options.receiveAudio,
|
|
6505
6973
|
receiveVideo: options.receiveVideo,
|
|
6974
|
+
fallbackToReceiveOnly: options.fallbackToReceiveOnly,
|
|
6506
6975
|
webRTCApiProvider: this.webRTCApiProvider,
|
|
6507
6976
|
preferredVideoCodecs: options.preferredVideoCodecs,
|
|
6508
6977
|
preferredAudioCodecs: options.preferredAudioCodecs,
|
|
@@ -6516,7 +6985,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6516
6985
|
this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
|
|
6517
6986
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
6518
6987
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
6519
|
-
this.
|
|
6988
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
6520
6989
|
});
|
|
6521
6990
|
if (options.initOffer) this.handleInboundAnswer(rtcPeerConnController);
|
|
6522
6991
|
}
|
|
@@ -6545,7 +7014,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6545
7014
|
await rtcPeerConnController.acceptInbound(answerOptions);
|
|
6546
7015
|
} catch (error) {
|
|
6547
7016
|
logger$16.error("[WebRTCManager] Error creating inbound answer:", error);
|
|
6548
|
-
this.
|
|
7017
|
+
this.reportLegError(toError(error), rtcPeerConnController);
|
|
6549
7018
|
}
|
|
6550
7019
|
}
|
|
6551
7020
|
}
|
|
@@ -6617,7 +7086,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6617
7086
|
isInvite: isVertoInviteMessage(vertoMessage),
|
|
6618
7087
|
reattach: this.webRtcCallSession.options.reattach === true,
|
|
6619
7088
|
explicitNodeId: this.webRtcCallSession.options.nodeId,
|
|
6620
|
-
currentNodeId: this._nodeId$.value
|
|
7089
|
+
currentNodeId: rtcPeerConnController.nodeId ?? this._nodeId$.value
|
|
6621
7090
|
}),
|
|
6622
7091
|
subscribe
|
|
6623
7092
|
};
|
|
@@ -6649,7 +7118,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6649
7118
|
await this.attachManager.attach(this.buildAttachableCall());
|
|
6650
7119
|
} catch (error) {
|
|
6651
7120
|
logger$16.error("[WebRTCManager] Error sending Verto answer:", error);
|
|
6652
|
-
this.
|
|
7121
|
+
this.reportLegError(toError(error), rtcPeerConnectionController);
|
|
6653
7122
|
await rtcPeerConnectionController.updateAnswerStatus({ status: "failed" });
|
|
6654
7123
|
}
|
|
6655
7124
|
}
|
|
@@ -6674,7 +7143,7 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6674
7143
|
screenShare: rtcPeerConnectionController.isScreenShare,
|
|
6675
7144
|
additionalDevice: rtcPeerConnectionController.isAdditionalDevice,
|
|
6676
7145
|
pingSupported: true,
|
|
6677
|
-
version: INVITE_VERSION
|
|
7146
|
+
version: require_operators.INVITE_VERSION
|
|
6678
7147
|
};
|
|
6679
7148
|
}
|
|
6680
7149
|
muteMainAudioInputDevice() {
|
|
@@ -6721,17 +7190,23 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6721
7190
|
await this.mainPeerConnection.restoreTrackSender(deviceKind);
|
|
6722
7191
|
} else {
|
|
6723
7192
|
const error = new require_operators.InvalidParams("No valid device to be added");
|
|
6724
|
-
this.
|
|
7193
|
+
this.reportLegError(error, this.mainPeerConnection);
|
|
6725
7194
|
throw error;
|
|
6726
7195
|
}
|
|
6727
7196
|
}
|
|
6728
|
-
async addScreenMedia(options = {
|
|
6729
|
-
await this.initAdditionalPeerConnection("screenshare",
|
|
7197
|
+
async addScreenMedia(options = {}) {
|
|
7198
|
+
await this.initAdditionalPeerConnection("screenshare", {
|
|
7199
|
+
audio: false,
|
|
7200
|
+
screenShareAudio: options.audio ?? false
|
|
7201
|
+
});
|
|
6730
7202
|
}
|
|
6731
7203
|
async initAdditionalPeerConnection(propose, options) {
|
|
7204
|
+
const isScreenShare = propose === "screenshare";
|
|
7205
|
+
if (isScreenShare && this._screenShareId && this._rtcPeerConnectionsMap.has(this._screenShareId)) throw new require_operators.ScreenShareAlreadyActiveError(this._screenShareId);
|
|
7206
|
+
let firstPeerConnectionError;
|
|
6732
7207
|
let rtcPeerConnController = null;
|
|
6733
7208
|
try {
|
|
6734
|
-
this._screenShareStatus$.next("starting");
|
|
7209
|
+
if (isScreenShare) this._screenShareStatus$.next("starting");
|
|
6735
7210
|
rtcPeerConnController = new RTCPeerConnectionController({
|
|
6736
7211
|
...options,
|
|
6737
7212
|
...this.RTCPeerConnectionConfig,
|
|
@@ -6739,21 +7214,42 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6739
7214
|
webRTCApiProvider: this.webRTCApiProvider
|
|
6740
7215
|
}, void 0, this.deviceController);
|
|
6741
7216
|
this.setupLocalDescriptionHandler(rtcPeerConnController);
|
|
6742
|
-
if (
|
|
7217
|
+
if (isScreenShare) this._screenShareId = rtcPeerConnController.id;
|
|
6743
7218
|
this._rtcPeerConnectionsMap.set(rtcPeerConnController.id, rtcPeerConnController);
|
|
6744
7219
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
6745
7220
|
this.subscribeTo(rtcPeerConnController.errors$, (error) => {
|
|
6746
|
-
|
|
7221
|
+
firstPeerConnectionError ??= error;
|
|
7222
|
+
this.reportLegError(error, rtcPeerConnController);
|
|
6747
7223
|
});
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
7224
|
+
const pc = rtcPeerConnController;
|
|
7225
|
+
await (0, rxjs.firstValueFrom)((0, rxjs.merge)(pc.localMediaSettled$.pipe((0, rxjs.take)(1), (0, rxjs.switchMap)(() => pc.connectionState$.pipe((0, rxjs.filter)((state) => state === "connected"), (0, rxjs.take)(1), (0, rxjs.timeout)(require_operators.DEFAULT_AUX_LEG_CONNECT_TIMEOUT_MS)))), this.legError$(pc.id)).pipe((0, rxjs.takeUntil)((0, rxjs.merge)(this.destroyed$, pc.destroyed$))));
|
|
7226
|
+
if (isScreenShare) this._screenShareStatus$.next("started");
|
|
7227
|
+
logger$16.info(`[WebRTCManager] Additional peer connection connected (${propose}).`);
|
|
6751
7228
|
return rtcPeerConnController.id;
|
|
6752
7229
|
} catch (error) {
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
if (
|
|
6756
|
-
this.
|
|
7230
|
+
const cancelled = error instanceof require_operators.AuxiliaryLegCancelledError;
|
|
7231
|
+
const aborted = error instanceof rxjs.EmptyError && !firstPeerConnectionError;
|
|
7232
|
+
if (!cancelled && !aborted) logger$16.warn("[WebRTCManager] Error initializing additional peer connection:", error);
|
|
7233
|
+
if (rtcPeerConnController && this._rtcPeerConnectionsMap.has(rtcPeerConnController.id)) {
|
|
7234
|
+
rtcPeerConnController.destroy();
|
|
7235
|
+
this._rtcPeerConnectionsMap.delete(rtcPeerConnController.id);
|
|
7236
|
+
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
7237
|
+
}
|
|
7238
|
+
if (isScreenShare) {
|
|
7239
|
+
this._screenShareId = void 0;
|
|
7240
|
+
this._screenShareStatus$.next("none");
|
|
7241
|
+
}
|
|
7242
|
+
if (cancelled) {
|
|
7243
|
+
logger$16.debug("[WebRTCManager] Additional peer connection removed before connecting.");
|
|
7244
|
+
throw error;
|
|
7245
|
+
}
|
|
7246
|
+
if (firstPeerConnectionError) throw firstPeerConnectionError instanceof require_operators.MediaAccessError && firstPeerConnectionError.originalError instanceof Error ? firstPeerConnectionError.originalError : firstPeerConnectionError;
|
|
7247
|
+
if (error instanceof rxjs.EmptyError) {
|
|
7248
|
+
logger$16.debug("[WebRTCManager] Additional peer connection aborted before connecting.");
|
|
7249
|
+
return;
|
|
7250
|
+
}
|
|
7251
|
+
if (error instanceof rxjs.TimeoutError) throw new require_operators.AuxiliaryLegTimeoutError(propose, error);
|
|
7252
|
+
throw error instanceof Error ? error : new Error(String(error), { cause: error });
|
|
6757
7253
|
}
|
|
6758
7254
|
}
|
|
6759
7255
|
async removeInputDevices(id) {
|
|
@@ -6769,7 +7265,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6769
7265
|
if (removeTrack) return this.mainPeerConnection.stopTrackSender(removeTrack, { updateTransceiverDirection: true });
|
|
6770
7266
|
}
|
|
6771
7267
|
async removeScreenMedia() {
|
|
6772
|
-
if (!["starting", "started"].includes(this._screenShareStatus$.value))
|
|
7268
|
+
if (!["starting", "started"].includes(this._screenShareStatus$.value)) {
|
|
7269
|
+
logger$16.warn("[WebRTCManager] No active screen share to stop.");
|
|
7270
|
+
return;
|
|
7271
|
+
}
|
|
6773
7272
|
if (!this._screenShareId) {
|
|
6774
7273
|
logger$16.debug("[WebRTCManager] No screen share peer connection found.");
|
|
6775
7274
|
return;
|
|
@@ -6784,6 +7283,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6784
7283
|
try {
|
|
6785
7284
|
if (rtcPeerConnController) await this.executeVertoBye(rtcPeerConnController);
|
|
6786
7285
|
} finally {
|
|
7286
|
+
if (rtcPeerConnController && rtcPeerConnController.connectionState !== "connected") this._legErrors$.next({
|
|
7287
|
+
legId: id,
|
|
7288
|
+
error: new require_operators.AuxiliaryLegCancelledError(rtcPeerConnController.propose)
|
|
7289
|
+
});
|
|
6787
7290
|
rtcPeerConnController?.destroy();
|
|
6788
7291
|
this._rtcPeerConnectionsMap.delete(id);
|
|
6789
7292
|
this._rtcPeerConnections$.next(Array.from(this._rtcPeerConnectionsMap.values()));
|
|
@@ -6798,7 +7301,10 @@ var WebRTCVertoManager = class extends VertoManager {
|
|
|
6798
7301
|
await this.executeVerto(VertoBye({
|
|
6799
7302
|
...causeParams,
|
|
6800
7303
|
dialogParams: this.dialogParams(rtcPeerConnController)
|
|
6801
|
-
})
|
|
7304
|
+
}), {
|
|
7305
|
+
callID: rtcPeerConnController.id,
|
|
7306
|
+
node_id: rtcPeerConnController.nodeId ?? void 0
|
|
7307
|
+
});
|
|
6802
7308
|
} catch (error) {
|
|
6803
7309
|
logger$16.warn("[WebRTCManager] Call might already be disconnected, error sending Verto bye:", error);
|
|
6804
7310
|
throw error;
|
|
@@ -6868,7 +7374,7 @@ var RemoteAudioMeter = class extends Destroyable {
|
|
|
6868
7374
|
this._analyser.fftSize = 2048;
|
|
6869
7375
|
this._analyser.smoothingTimeConstant = .3;
|
|
6870
7376
|
this._analyserBuffer = new Uint8Array(new ArrayBuffer(this._analyser.fftSize));
|
|
6871
|
-
this._pollIntervalMs = options.pollIntervalMs ?? AUDIO_LEVEL_POLL_INTERVAL_MS;
|
|
7377
|
+
this._pollIntervalMs = options.pollIntervalMs ?? require_operators.AUDIO_LEVEL_POLL_INTERVAL_MS;
|
|
6872
7378
|
}
|
|
6873
7379
|
/** RMS level of the remote audio, 0..1. 0 when no stream is attached. */
|
|
6874
7380
|
get level$() {
|
|
@@ -7681,14 +8187,35 @@ function mosToQualityLevel(mos) {
|
|
|
7681
8187
|
}
|
|
7682
8188
|
|
|
7683
8189
|
//#endregion
|
|
7684
|
-
//#region src/
|
|
7685
|
-
const logger$12 = require_operators.getLogger();
|
|
8190
|
+
//#region src/utils/unwrapVertoReply.ts
|
|
7686
8191
|
/**
|
|
7687
|
-
*
|
|
7688
|
-
*
|
|
7689
|
-
*
|
|
8192
|
+
* Unwrap a method's own reply from a `webrtc.verto` envelope.
|
|
8193
|
+
*
|
|
8194
|
+
* A control verb sent in-dialog comes back nested two levels deep:
|
|
8195
|
+
*
|
|
8196
|
+
* ```
|
|
8197
|
+
* { result: { node_id, code, result: { jsonrpc, id, result: <method payload> } } }
|
|
8198
|
+
* ```
|
|
8199
|
+
*
|
|
8200
|
+
* whereas the routed transport resolves the method payload directly under
|
|
8201
|
+
* `.result`. Readers want the latter shape, and the difference is silent when it
|
|
8202
|
+
* is wrong — `response.result.layouts` simply evaluates to `undefined` against the
|
|
8203
|
+
* envelope, so the data arrives, nothing throws, and the caller sees an empty
|
|
8204
|
+
* value. That exact failure produced an empty layout dropdown with a successful
|
|
8205
|
+
* request behind it.
|
|
8206
|
+
*
|
|
8207
|
+
* Accepting both shapes here keeps every reader indifferent to which transport
|
|
8208
|
+
* produced the response. Anything that is not a nested envelope (a plain ack, or
|
|
8209
|
+
* an already-unwrapped reply) passes through untouched.
|
|
7690
8210
|
*/
|
|
7691
|
-
|
|
8211
|
+
function unwrapVertoReply(response) {
|
|
8212
|
+
const inner = require_operators.getValueFrom(response, "result.result");
|
|
8213
|
+
return inner && typeof inner === "object" && "result" in inner ? inner : response;
|
|
8214
|
+
}
|
|
8215
|
+
|
|
8216
|
+
//#endregion
|
|
8217
|
+
//#region src/core/entities/Call.ts
|
|
8218
|
+
const logger$12 = require_operators.getLogger();
|
|
7692
8219
|
/**
|
|
7693
8220
|
* Ratio between the critical and warning RTT spike multipliers.
|
|
7694
8221
|
* Warning threshold = baseline * warningMultiplier (default 3x)
|
|
@@ -7784,8 +8311,11 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7784
8311
|
emitError(callError) {
|
|
7785
8312
|
if (this._status$.value === "destroyed" || this._status$.value === "failed") return;
|
|
7786
8313
|
this._errors$.next(callError);
|
|
7787
|
-
if (callError.fatal) {
|
|
8314
|
+
if (callError.fatal && this._status$.value !== "disconnecting") {
|
|
7788
8315
|
this._status$.next("failed");
|
|
8316
|
+
this.vertoManager.bye().catch((error) => {
|
|
8317
|
+
logger$12.debug("[Call] fatal-teardown bye failed (signaling likely already dead):", error);
|
|
8318
|
+
});
|
|
7789
8319
|
this.destroy();
|
|
7790
8320
|
}
|
|
7791
8321
|
}
|
|
@@ -7840,7 +8370,7 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7840
8370
|
/** Toggles the call lock state, preventing or allowing new participants from joining. */
|
|
7841
8371
|
async toggleLock() {
|
|
7842
8372
|
const method = this.locked ? "call.unlock" : "call.lock";
|
|
7843
|
-
await this.executeMethod(this.
|
|
8373
|
+
await this.executeMethod(this.callSelf, method, {});
|
|
7844
8374
|
}
|
|
7845
8375
|
/**
|
|
7846
8376
|
* Toggles the hold state of the call (pauses/resumes local media transmission).
|
|
@@ -7889,14 +8419,25 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7889
8419
|
*
|
|
7890
8420
|
* Constructs call context (node_id, call_id, member_id) and sends the RPC request.
|
|
7891
8421
|
*
|
|
7892
|
-
* @param target - Target
|
|
8422
|
+
* @param target - Target {@link MemberTarget} triple, or the local member's
|
|
8423
|
+
* ID string for self-operations (any other string is rejected — a bare
|
|
8424
|
+
* member id cannot carry the remote member's own call context).
|
|
7893
8425
|
* @param method - Verto method name (e.g. `'call.mute'`, `'call.member.remove'`).
|
|
7894
8426
|
* @param args - Parameters for the RPC method.
|
|
7895
8427
|
* @returns The RPC response.
|
|
8428
|
+
* @throws {CallNotReadyError} If the call has no self member context yet.
|
|
8429
|
+
* @throws {InvalidParams} If a string target is not the local member's ID.
|
|
7896
8430
|
* @throws {JSONRPCError} If the RPC call returns an error.
|
|
7897
8431
|
*/
|
|
7898
8432
|
async executeMethod(target, method, args) {
|
|
7899
|
-
const
|
|
8433
|
+
const self = this.callSelf;
|
|
8434
|
+
if (typeof target === "string" && target !== self.member_id) throw new require_operators.InvalidParams(`Target member ID ${target} does not match call's self member ID ${self.member_id}`);
|
|
8435
|
+
if (this.clientSession.callControl === "in-dialog") return this.executeMethodInDialog(target, method, args);
|
|
8436
|
+
const params = {
|
|
8437
|
+
...args,
|
|
8438
|
+
self,
|
|
8439
|
+
target: typeof target === "string" ? self : target
|
|
8440
|
+
};
|
|
7900
8441
|
const request = buildRPCRequest({
|
|
7901
8442
|
method,
|
|
7902
8443
|
params
|
|
@@ -7910,29 +8451,98 @@ var WebRTCCall = class extends Destroyable {
|
|
|
7910
8451
|
throw error;
|
|
7911
8452
|
}
|
|
7912
8453
|
}
|
|
7913
|
-
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
};
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
8454
|
+
/**
|
|
8455
|
+
* `executeMethod` for a call opened with `callControl: 'in-dialog'`.
|
|
8456
|
+
*
|
|
8457
|
+
* Translates the routed transport's calling convention into the in-dialog one. No
|
|
8458
|
+
* `self` tuple is sent, but a `target` is — the same {call_id, member_id} the routed
|
|
8459
|
+
* transport puts in `target` (minus node_id), for self-ops and cross-member ops alike.
|
|
8460
|
+
*
|
|
8461
|
+
* Target shapes are per-verb and irregular, so they are centralised here rather
|
|
8462
|
+
* than left to callers: most verbs take a singular `target`, `call.member.remove`
|
|
8463
|
+
* takes a plural `targets` array, and `call.member.position.set` takes a flat
|
|
8464
|
+
* `targets` of `{call_id, position}` — the one verb keyed on call_id rather than
|
|
8465
|
+
* member_id, so the member triple `Participant.setPosition` built is unwrapped.
|
|
8466
|
+
*/
|
|
8467
|
+
async executeMethodInDialog(target, method, args) {
|
|
8468
|
+
const control = { ...args };
|
|
8469
|
+
if (method === "call.member.position.set") control.targets = (args.targets ?? []).map((entry) => ({
|
|
8470
|
+
call_id: entry.target?.call_id ?? entry.call_id,
|
|
8471
|
+
position: entry.position
|
|
8472
|
+
}));
|
|
8473
|
+
else {
|
|
8474
|
+
const member = typeof target === "object" ? {
|
|
8475
|
+
call_id: target.call_id,
|
|
8476
|
+
member_id: target.member_id
|
|
8477
|
+
} : {
|
|
7933
8478
|
call_id: this.id,
|
|
7934
8479
|
member_id: target
|
|
7935
|
-
}
|
|
8480
|
+
};
|
|
8481
|
+
if (method === "call.member.remove") control.targets = [member];
|
|
8482
|
+
else control.target = member;
|
|
8483
|
+
}
|
|
8484
|
+
return this.sendCommand(method, control);
|
|
8485
|
+
}
|
|
8486
|
+
/**
|
|
8487
|
+
* Sends a `call.*` control verb **in-dialog** via `verto.info`, as an alternative
|
|
8488
|
+
* to the routed {@link executeMethod} transport.
|
|
8489
|
+
*
|
|
8490
|
+
* Why both exist: `executeMethod` addresses the member with an explicit
|
|
8491
|
+
* `{node_id, call_id, member_id}` tuple, which does not resolve for every conference,
|
|
8492
|
+
* so the op can fail. An in-dialog frame carries the verb on the member's own
|
|
8493
|
+
* signaling channel instead, so control works without the client needing to know how
|
|
8494
|
+
* the conference is hosted.
|
|
8495
|
+
*
|
|
8496
|
+
* The trade-off is reach: the in-dialog transport is only accepted for calls that
|
|
8497
|
+
* join a conference over SWML (e.g. an SWML `join_conference`); use the routed
|
|
8498
|
+
* default otherwise.
|
|
8499
|
+
*
|
|
8500
|
+
* `params` are sent verbatim — nothing is built for you, which includes the target.
|
|
8501
|
+
* **A self-directed op still needs one**, or it is refused; name yourself explicitly:
|
|
8502
|
+
*
|
|
8503
|
+
* ```ts
|
|
8504
|
+
* const { call_id, member_id } = call.self.target;
|
|
8505
|
+
* await call.sendCommand('call.mute', { channels: ['audio'], target: { call_id, member_id } });
|
|
8506
|
+
* ```
|
|
8507
|
+
*
|
|
8508
|
+
* Never include `node_id` — only the two ids. The shapes are per-verb: most take a
|
|
8509
|
+
* singular `target`, `call.member.remove` takes a plural `targets` array, and
|
|
8510
|
+
* `call.member.position.set` takes a flat `targets: [{call_id, position}]` (the one
|
|
8511
|
+
* verb keyed on `call_id` rather than `member_id`). Verbs that act on the call as a
|
|
8512
|
+
* whole, or that the SDK does not wrap at all, take no target.
|
|
8513
|
+
*
|
|
8514
|
+
* For the typed alternative that handles all of this, create the client with
|
|
8515
|
+
* `callControl: 'in-dialog'` and use the ordinary `Call`/`Participant` methods.
|
|
8516
|
+
*
|
|
8517
|
+
* @internal Not part of the supported surface while the in-dialog transport is still
|
|
8518
|
+
* rolling out. `WebRTCCall` is exported from the package entry, so without this tag
|
|
8519
|
+
* TypeDoc publishes the method — and the example above — as public API.
|
|
8520
|
+
*
|
|
8521
|
+
* @param method - A `call.*` method name (e.g. `'call.mute'`).
|
|
8522
|
+
* @param params - Method parameters, sent verbatim.
|
|
8523
|
+
* @returns The method's own reply, unwrapped from the `verto.info` envelope.
|
|
8524
|
+
* @throws {JSONRPCError} If the control op fails.
|
|
8525
|
+
*/
|
|
8526
|
+
async sendCommand(method, params = {}) {
|
|
8527
|
+
return unwrapVertoReply(await this.vertoManager.sendCallControl(method, params));
|
|
8528
|
+
}
|
|
8529
|
+
/**
|
|
8530
|
+
* The local leg's member triple — sent as `self` in every member RPC
|
|
8531
|
+
* envelope, and as the `target` of call-scoped self-operations (e.g. lock,
|
|
8532
|
+
* layout).
|
|
8533
|
+
*
|
|
8534
|
+
* @throws {CallNotReadyError} Before `call.joined` delivers the self member
|
|
8535
|
+
* context (`selfId`/`nodeId`) — an RPC without it cannot be routed, so fail
|
|
8536
|
+
* fast instead of sending a doomed request.
|
|
8537
|
+
*/
|
|
8538
|
+
get callSelf() {
|
|
8539
|
+
const node_id = this.nodeId;
|
|
8540
|
+
const member_id = this.vertoManager.selfId;
|
|
8541
|
+
if (!node_id || !member_id) throw new require_operators.CallNotReadyError(this.id);
|
|
8542
|
+
return {
|
|
8543
|
+
node_id,
|
|
8544
|
+
call_id: this.id,
|
|
8545
|
+
member_id
|
|
7936
8546
|
};
|
|
7937
8547
|
}
|
|
7938
8548
|
/** Observable of the current call status (e.g. `'ringing'`, `'connected'`). */
|
|
@@ -8088,13 +8698,13 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8088
8698
|
get bandwidthConstrained$() {
|
|
8089
8699
|
return this.deferEmission(this._bandwidthConstrained$.asObservable());
|
|
8090
8700
|
}
|
|
8091
|
-
/** Observable that emits when server
|
|
8701
|
+
/** Observable that emits when the server pushes media params. */
|
|
8092
8702
|
get mediaParamsUpdated$() {
|
|
8093
8703
|
return this.deferEmission(this._mediaParamsUpdated$.asObservable());
|
|
8094
8704
|
}
|
|
8095
8705
|
/**
|
|
8096
8706
|
* @internal Emit a media params update event.
|
|
8097
|
-
* Called by the VertoManager when server
|
|
8707
|
+
* Called by the VertoManager when the server pushes media params.
|
|
8098
8708
|
*/
|
|
8099
8709
|
emitMediaParamsUpdated(event) {
|
|
8100
8710
|
this._mediaParamsUpdated$.next(event);
|
|
@@ -8248,13 +8858,13 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8248
8858
|
async waitForPeerConnectionConnected() {
|
|
8249
8859
|
const pc = this.rtcPeerConnection;
|
|
8250
8860
|
if (!pc) return false;
|
|
8251
|
-
const deadline = Date.now() + PEER_CONNECTION_RECOVERY_WAIT_MS;
|
|
8861
|
+
const deadline = Date.now() + require_operators.PEER_CONNECTION_RECOVERY_WAIT_MS;
|
|
8252
8862
|
for (;;) {
|
|
8253
8863
|
const state = pc.connectionState;
|
|
8254
8864
|
if (state === "connected") return true;
|
|
8255
8865
|
if (state === "failed" || state === "closed") return false;
|
|
8256
8866
|
if (Date.now() >= deadline) return false;
|
|
8257
|
-
await new Promise((resolve) => setTimeout(resolve, PEER_CONNECTION_RECOVERY_POLL_MS));
|
|
8867
|
+
await new Promise((resolve) => setTimeout(resolve, require_operators.PEER_CONNECTION_RECOVERY_POLL_MS));
|
|
8258
8868
|
}
|
|
8259
8869
|
}
|
|
8260
8870
|
/**
|
|
@@ -8302,6 +8912,10 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8302
8912
|
get selfId$() {
|
|
8303
8913
|
return this.vertoManager.selfId$;
|
|
8304
8914
|
}
|
|
8915
|
+
/** @internal Lets call creation bound the media and signalling phases apart. */
|
|
8916
|
+
get localMediaSettled$() {
|
|
8917
|
+
return this.vertoManager.localMediaSettled$;
|
|
8918
|
+
}
|
|
8305
8919
|
/** Local participant's member ID, or `null` if not joined. */
|
|
8306
8920
|
get selfId() {
|
|
8307
8921
|
return this.vertoManager.selfId;
|
|
@@ -8495,12 +9109,17 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8495
9109
|
*
|
|
8496
9110
|
* **These operations are NOT atomic.** The layout is applied first, then each
|
|
8497
9111
|
* member position sequentially, so members may briefly flash into their
|
|
8498
|
-
* default slots before being moved to the requested positions.
|
|
9112
|
+
* default slots before being moved to the requested positions. Targeted
|
|
9113
|
+
* members are validated upfront, though: when any of them has no
|
|
9114
|
+
* {@link Participant.target | member call context} yet, the whole call
|
|
9115
|
+
* rejects before any request is sent and the layout is left unchanged.
|
|
8499
9116
|
*
|
|
8500
9117
|
* @param layout - Layout name (must be one of {@link layouts}).
|
|
8501
9118
|
* @param positions - Optional map of member IDs to {@link VideoPosition} values.
|
|
8502
9119
|
* When omitted or empty, only the layout is changed.
|
|
8503
9120
|
* @throws {InvalidParams} If the layout is not in the available {@link layouts}.
|
|
9121
|
+
* @throws {ParticipantNotReadyError} If a targeted member's call context has
|
|
9122
|
+
* not been received yet — thrown before any request is sent.
|
|
8504
9123
|
*
|
|
8505
9124
|
* @example
|
|
8506
9125
|
* ```ts
|
|
@@ -8511,18 +9130,19 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8511
9130
|
*/
|
|
8512
9131
|
async setLayout(layout, positions) {
|
|
8513
9132
|
if (!this.layouts.includes(layout)) throw new require_operators.InvalidParams(`Layout ${layout} is not available in the current call layouts: ${this.layouts.join(", ")}`);
|
|
8514
|
-
const
|
|
8515
|
-
|
|
8516
|
-
const positionEntries = Object.entries(positions ?? {});
|
|
8517
|
-
if (positionEntries.length === 0) return;
|
|
8518
|
-
for (const [memberId, position] of positionEntries) {
|
|
9133
|
+
const targets = [];
|
|
9134
|
+
for (const [memberId, position] of Object.entries(positions ?? {})) {
|
|
8519
9135
|
const participant = this.participants.find((p) => p.id === memberId);
|
|
8520
9136
|
if (!participant) {
|
|
8521
9137
|
logger$12.warn(`[Call] setLayout: member ${memberId} not found in participants; skipping position ${position}`);
|
|
8522
9138
|
continue;
|
|
8523
9139
|
}
|
|
8524
|
-
|
|
9140
|
+
participant.target;
|
|
9141
|
+
targets.push([participant, position]);
|
|
8525
9142
|
}
|
|
9143
|
+
const selfId = await (0, rxjs.firstValueFrom)(this.selfId$.pipe((0, rxjs.filter)((id) => id !== null)));
|
|
9144
|
+
await this.executeMethod(selfId, "call.layout.set", { layout });
|
|
9145
|
+
for (const [participant, position] of targets) await participant.setPosition(position);
|
|
8526
9146
|
}
|
|
8527
9147
|
/**
|
|
8528
9148
|
* Transfers the call to another destination.
|
|
@@ -8624,17 +9244,28 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8624
9244
|
* (notably iOS Safari) fall back to re-acquiring the track with the new
|
|
8625
9245
|
* constraint set and plumbing the replacement through the local audio
|
|
8626
9246
|
* pipeline if one is active.
|
|
9247
|
+
*
|
|
9248
|
+
* @returns whether the constraint reached the microphone. `false` is an
|
|
9249
|
+
* outcome rather than an error — a leg sending media the SDK did not capture
|
|
9250
|
+
* is left alone — so a UI that reflects the toggle must read it. Any failure
|
|
9251
|
+
* behind a `false` is also reported on {@link errors$}.
|
|
8627
9252
|
*/
|
|
8628
9253
|
async setEchoCancellation(enabled) {
|
|
8629
|
-
|
|
9254
|
+
return this.vertoManager.updateMediaConstraints({ audio: { echoCancellation: enabled } });
|
|
8630
9255
|
}
|
|
8631
|
-
/**
|
|
9256
|
+
/**
|
|
9257
|
+
* Toggle browser noise suppression on the local mic at runtime.
|
|
9258
|
+
* @returns whether the constraint reached the microphone.
|
|
9259
|
+
*/
|
|
8632
9260
|
async setNoiseSuppression(enabled) {
|
|
8633
|
-
|
|
9261
|
+
return this.vertoManager.updateMediaConstraints({ audio: { noiseSuppression: enabled } });
|
|
8634
9262
|
}
|
|
8635
|
-
/**
|
|
9263
|
+
/**
|
|
9264
|
+
* Toggle browser automatic gain control on the local mic at runtime.
|
|
9265
|
+
* @returns whether the constraint reached the microphone.
|
|
9266
|
+
*/
|
|
8636
9267
|
async setAutoGainControl(enabled) {
|
|
8637
|
-
|
|
9268
|
+
return this.vertoManager.updateMediaConstraints({ audio: { autoGainControl: enabled } });
|
|
8638
9269
|
}
|
|
8639
9270
|
/**
|
|
8640
9271
|
* Observable of the aggregate remote audio level, 0..1 RMS. The server
|
|
@@ -8691,11 +9322,15 @@ var WebRTCCall = class extends Destroyable {
|
|
|
8691
9322
|
/**
|
|
8692
9323
|
* Infers the semantic error category from a raw Error thrown by VertoManager
|
|
8693
9324
|
* or an RTCPeerConnection layer.
|
|
9325
|
+
*
|
|
9326
|
+
* Pure function — exported for unit testing.
|
|
9327
|
+
* @internal
|
|
8694
9328
|
*/
|
|
8695
9329
|
function inferCallErrorKind(error) {
|
|
8696
9330
|
if (error instanceof require_operators.RPCTimeoutError) return "timeout";
|
|
8697
9331
|
if (error instanceof require_operators.JSONRPCError) return "signaling";
|
|
8698
9332
|
if (error instanceof require_operators.MediaTrackError) return "media";
|
|
9333
|
+
if (error instanceof require_operators.MediaAccessError) return "media";
|
|
8699
9334
|
if (error instanceof require_operators.WebSocketConnectionError || error instanceof require_operators.TransportConnectionError) return "network";
|
|
8700
9335
|
return "internal";
|
|
8701
9336
|
}
|
|
@@ -8704,14 +9339,22 @@ function inferCallErrorKind(error) {
|
|
|
8704
9339
|
* destroy the call, because the session will reauthenticate and any pending
|
|
8705
9340
|
* RPC can then be retried. */
|
|
8706
9341
|
const RECOVERABLE_RPC_CODES = new Set([
|
|
8707
|
-
RPC_ERROR_REQUESTER_VALIDATION_FAILED,
|
|
8708
|
-
RPC_ERROR_AUTHENTICATION_FAILED,
|
|
8709
|
-
RPC_ERROR_INVALID_PARAMS
|
|
9342
|
+
require_operators.RPC_ERROR_REQUESTER_VALIDATION_FAILED,
|
|
9343
|
+
require_operators.RPC_ERROR_AUTHENTICATION_FAILED,
|
|
9344
|
+
require_operators.RPC_ERROR_INVALID_PARAMS
|
|
8710
9345
|
]);
|
|
8711
|
-
/**
|
|
9346
|
+
/**
|
|
9347
|
+
* A *fallback*: callers knowing which leg failed pass an explicit `fatal` and
|
|
9348
|
+
* never reach here, so the default-fatal branch only sees call- and main-leg
|
|
9349
|
+
* errors. Auxiliary legs go through `WebRTCVertoManager.reportLegError`.
|
|
9350
|
+
*
|
|
9351
|
+
* Pure function — exported for unit testing.
|
|
9352
|
+
* @internal
|
|
9353
|
+
*/
|
|
8712
9354
|
function isFatalError(error) {
|
|
8713
9355
|
if (error instanceof require_operators.VertoPongError) return false;
|
|
8714
9356
|
if (error instanceof require_operators.MediaTrackError) return false;
|
|
9357
|
+
if (error instanceof require_operators.MediaAccessError) return error.fatal;
|
|
8715
9358
|
if (error instanceof require_operators.RPCTimeoutError) return false;
|
|
8716
9359
|
if (error instanceof require_operators.JSONRPCError && RECOVERABLE_RPC_CODES.has(error.code)) return false;
|
|
8717
9360
|
return true;
|
|
@@ -8737,12 +9380,14 @@ var CallFactory = class {
|
|
|
8737
9380
|
return {
|
|
8738
9381
|
vertoManager: new WebRTCVertoManager(callInstance, this.attachManager, this.deviceController, this.webRTCApiProvider, {
|
|
8739
9382
|
nodeId: options.nodeId,
|
|
8740
|
-
onError: (error) => {
|
|
9383
|
+
onError: (error, options$1) => {
|
|
8741
9384
|
const callError = {
|
|
8742
9385
|
kind: inferCallErrorKind(error),
|
|
8743
|
-
fatal: isFatalError(error),
|
|
9386
|
+
fatal: options$1?.fatal ?? isFatalError(error),
|
|
8744
9387
|
error,
|
|
8745
|
-
callId: callInstance.id
|
|
9388
|
+
callId: callInstance.id,
|
|
9389
|
+
...options$1?.leg ? { leg: options$1.leg } : {},
|
|
9390
|
+
...options$1?.legId ? { legId: options$1.legId } : {}
|
|
8746
9391
|
};
|
|
8747
9392
|
callInstance.emitError(callError);
|
|
8748
9393
|
},
|
|
@@ -9199,6 +9844,41 @@ var PendingRPC = class PendingRPC {
|
|
|
9199
9844
|
//#endregion
|
|
9200
9845
|
//#region src/managers/ClientSessionManager.ts
|
|
9201
9846
|
const logger$9 = require_operators.getLogger();
|
|
9847
|
+
/**
|
|
9848
|
+
* Decide whether an error emitted on `call.errors$` during dial should
|
|
9849
|
+
* abort the dial. A non-fatal MediaAccessError means the call degraded to
|
|
9850
|
+
* receive-only and still connects — everything else rejects `dial()` with
|
|
9851
|
+
* the real cause.
|
|
9852
|
+
*
|
|
9853
|
+
* Pure function — exported for unit testing.
|
|
9854
|
+
*/
|
|
9855
|
+
function shouldAbortDial(callError) {
|
|
9856
|
+
return callError.fatal || !(callError.error instanceof require_operators.MediaAccessError);
|
|
9857
|
+
}
|
|
9858
|
+
/**
|
|
9859
|
+
* Wait for a dialed call to be ready, or for the failure that stops it.
|
|
9860
|
+
*
|
|
9861
|
+
* Local media acquisition is deliberately unbounded: a permission prompt or a
|
|
9862
|
+
* device picker is human time, and `getUserMedia` cannot be cancelled anyway.
|
|
9863
|
+
* The clock starts only once acquisition settles, so a slow human never spends
|
|
9864
|
+
* the server's budget.
|
|
9865
|
+
*
|
|
9866
|
+
* `merge` rather than `race`, because the two legs settle asymmetrically. A
|
|
9867
|
+
* fatal acquisition failure reports the error and then destroys the call in the
|
|
9868
|
+
* same synchronous step; `errors$` defers delivery by a microtask while
|
|
9869
|
+
* `localMediaSettled$` completes immediately. Under `race` that bare completion
|
|
9870
|
+
* ended the wait first and `dial()` rejected with an RxJS `EmptyError`, burying
|
|
9871
|
+
* the `NotAllowedError` applications are told to inspect. Under `merge` the
|
|
9872
|
+
* completed leg is simply spent, and the queued error — enqueued before the
|
|
9873
|
+
* completion, so delivered before it — arrives to reject the wait. A dial
|
|
9874
|
+
* abandoned with no error at all still ends both legs, and the resulting
|
|
9875
|
+
* `EmptyError` remains the benign-cancel signal.
|
|
9876
|
+
*
|
|
9877
|
+
* Exported for unit testing.
|
|
9878
|
+
*/
|
|
9879
|
+
async function awaitDialReady(session, signalingTimeoutMs) {
|
|
9880
|
+
return (0, rxjs.firstValueFrom)((0, rxjs.merge)(session.localMediaSettled$.pipe((0, rxjs.take)(1), (0, rxjs.switchMap)(() => session.selfId$.pipe((0, rxjs.filter)((id) => Boolean(id)), (0, rxjs.take)(1), (0, rxjs.timeout)(signalingTimeoutMs)))), session.errors$.pipe((0, rxjs.filter)(shouldAbortDial), (0, rxjs.take)(1), (0, rxjs.switchMap)((callError) => (0, rxjs.throwError)(() => callError.error)))));
|
|
9881
|
+
}
|
|
9202
9882
|
const getAddressSearchURI = (options) => {
|
|
9203
9883
|
const to = options.to?.split("?")[0];
|
|
9204
9884
|
const from$8 = options.from?.startsWith("subscriber://") ? options.from.replace("subscriber://", "") : options.from;
|
|
@@ -9215,7 +9895,6 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9215
9895
|
this.authorizationStateKey = authorizationStateKey;
|
|
9216
9896
|
this.attachManager = attachManager;
|
|
9217
9897
|
this.dpopManager = dpopManager;
|
|
9218
|
-
this.callCreateTimeout = 6e3;
|
|
9219
9898
|
this.agent = `signalwire-js/4.0.0`;
|
|
9220
9899
|
this.eventAcks = true;
|
|
9221
9900
|
this.authorizationState$ = this.createReplaySubject(1);
|
|
@@ -9224,6 +9903,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9224
9903
|
minor: 0,
|
|
9225
9904
|
revision: 0
|
|
9226
9905
|
};
|
|
9906
|
+
this.callControl = "routed";
|
|
9227
9907
|
this._authorization$ = this.createBehaviorSubject(void 0);
|
|
9228
9908
|
this._errors$ = this.createReplaySubject(1);
|
|
9229
9909
|
this._authState$ = this.createBehaviorSubject({ kind: "unauthenticated" });
|
|
@@ -9424,21 +10104,18 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9424
10104
|
}
|
|
9425
10105
|
async handleAuthenticationError(error) {
|
|
9426
10106
|
logger$9.error("Authentication error:", error);
|
|
9427
|
-
const isRecoverableAuthError = error instanceof require_operators.JSONRPCError && (error.code === RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === RPC_ERROR_INVALID_PARAMS || error.code === RPC_ERROR_AUTHENTICATION_FAILED);
|
|
10107
|
+
const isRecoverableAuthError$1 = error instanceof require_operators.JSONRPCError && (error.code === require_operators.RPC_ERROR_REQUESTER_VALIDATION_FAILED || error.code === require_operators.RPC_ERROR_INVALID_PARAMS || error.code === require_operators.RPC_ERROR_AUTHENTICATION_FAILED);
|
|
9428
10108
|
const hasStoredState = await (0, rxjs.firstValueFrom)(this.authorizationState$.pipe((0, rxjs.take)(1))) !== void 0;
|
|
9429
|
-
if (isRecoverableAuthError && hasStoredState) {
|
|
10109
|
+
if (isRecoverableAuthError$1 && hasStoredState) {
|
|
9430
10110
|
logger$9.debug("[Session] Recoverable auth error — cleaning up stored state and reconnecting fresh");
|
|
9431
|
-
|
|
9432
|
-
await this.cleanupStoredConnectionParams();
|
|
9433
|
-
} catch (cleanupError) {
|
|
9434
|
-
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
9435
|
-
} finally {
|
|
9436
|
-
this.transport.reconnect();
|
|
9437
|
-
}
|
|
10111
|
+
await this.discardResumeStateAndReconnect();
|
|
9438
10112
|
} else this._errors$.next(error);
|
|
9439
10113
|
}
|
|
9440
10114
|
/**
|
|
9441
|
-
* Clear the resume state (authorization_state + protocol)
|
|
10115
|
+
* Clear the resume state (authorization_state + protocol) and ask the
|
|
10116
|
+
* transport to reconnect. The `connected` event re-triggers
|
|
10117
|
+
* `authenticate()`, which now has no stored state and so performs a fresh
|
|
10118
|
+
* connect.
|
|
9442
10119
|
*
|
|
9443
10120
|
* This is the stale-auth-state recovery helper used by handleAuthError:
|
|
9444
10121
|
* the server rejected a reconnect, so the resume state is discarded and a
|
|
@@ -9446,9 +10123,24 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9446
10123
|
* session lives on through the reconnect and reattachCalls() needs the
|
|
9447
10124
|
* stored call references afterwards. Do NOT add detachAll() here.
|
|
9448
10125
|
*
|
|
10126
|
+
* Connect-time recovery only. A *request* refused on an already
|
|
10127
|
+
* authenticated session is never healed here: dropping the resume state
|
|
10128
|
+
* destroys the association between the socket and the previous session,
|
|
10129
|
+
* which is what reattach depends on. That path mints a fresh credential and
|
|
10130
|
+
* reauthenticates instead (see `SignalWire.recoverAndRetry`).
|
|
10131
|
+
*
|
|
9449
10132
|
* For public teardown (disconnect/destroy), use {@link teardownSessionState}
|
|
9450
10133
|
* instead, which clears the attach records as well.
|
|
9451
10134
|
*/
|
|
10135
|
+
async discardResumeStateAndReconnect() {
|
|
10136
|
+
try {
|
|
10137
|
+
await this.cleanupStoredConnectionParams();
|
|
10138
|
+
} catch (cleanupError) {
|
|
10139
|
+
logger$9.error("Failed to cleanup stored connection params:", cleanupError);
|
|
10140
|
+
} finally {
|
|
10141
|
+
this.transport.reconnect();
|
|
10142
|
+
}
|
|
10143
|
+
}
|
|
9452
10144
|
async cleanupStoredConnectionParams() {
|
|
9453
10145
|
await this.transport.setProtocol(void 0);
|
|
9454
10146
|
await this.updateAuthorizationStateInStorage(void 0);
|
|
@@ -9522,11 +10214,15 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9522
10214
|
const isReconnect = hasReconnectState && storedToken;
|
|
9523
10215
|
let dpopToken;
|
|
9524
10216
|
if (isReconnect) logger$9.debug("[Session] Reconnecting with stored jwt_token + authorization_state");
|
|
9525
|
-
else
|
|
9526
|
-
|
|
9527
|
-
|
|
10217
|
+
else {
|
|
10218
|
+
const credential = this.getCredential();
|
|
10219
|
+
const credentialExpired = credential.expiry_at !== void 0 && credential.expiry_at <= Date.now() + require_operators.CREDENTIAL_EXPIRY_SKEW_MS;
|
|
10220
|
+
if (this.onBeforeReconnect && (this.clientBound || credentialExpired)) {
|
|
10221
|
+
logger$9.debug("[Session] Refreshing credentials before fresh connect");
|
|
10222
|
+
await this.onBeforeReconnect();
|
|
10223
|
+
}
|
|
9528
10224
|
}
|
|
9529
|
-
if (
|
|
10225
|
+
if (this.dpopManager?.initialized) try {
|
|
9530
10226
|
dpopToken = await this.dpopManager.createRpcProof({ method: "signalwire.connect" });
|
|
9531
10227
|
} catch (error) {
|
|
9532
10228
|
if (this.clientBound) throw error;
|
|
@@ -9559,6 +10255,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9559
10255
|
});
|
|
9560
10256
|
if (response.protocol) await this.transport.setProtocol(response.protocol);
|
|
9561
10257
|
this._authorization$.next(response.authorization);
|
|
10258
|
+
if (response.authorization.cnf?.jkt) this._wasClientBound = true;
|
|
9562
10259
|
this._iceServers$.next(response.ice_servers ?? []);
|
|
9563
10260
|
this._authState$.next({ kind: "authenticated" });
|
|
9564
10261
|
logger$9.debug("[Session] Authentication completed successfully");
|
|
@@ -9625,7 +10322,7 @@ var ClientSessionManager = class extends Destroyable {
|
|
|
9625
10322
|
to: destinationURI,
|
|
9626
10323
|
...options
|
|
9627
10324
|
});
|
|
9628
|
-
await (
|
|
10325
|
+
await awaitDialReady(callSession, require_operators.DEFAULT_CALL_SIGNALING_TIMEOUT_MS);
|
|
9629
10326
|
this._calls$.next({
|
|
9630
10327
|
[`${callSession.id}`]: callSession,
|
|
9631
10328
|
...this._calls$.value
|
|
@@ -9678,12 +10375,23 @@ var ClientSessionWrapper = class {
|
|
|
9678
10375
|
get authenticated() {
|
|
9679
10376
|
return this.clientSessionManager.authenticated;
|
|
9680
10377
|
}
|
|
10378
|
+
/**
|
|
10379
|
+
* Whether the session is using a Client Bound SAT (DPoP). Sticky — set
|
|
10380
|
+
* when the binding is established or restored from a resumed session's
|
|
10381
|
+
* server authorization.
|
|
10382
|
+
*/
|
|
10383
|
+
get clientBound() {
|
|
10384
|
+
return this.clientSessionManager.clientBound;
|
|
10385
|
+
}
|
|
9681
10386
|
get signalingEvent$() {
|
|
9682
10387
|
return this.clientSessionManager.signalingEvent$;
|
|
9683
10388
|
}
|
|
9684
10389
|
get iceServers() {
|
|
9685
10390
|
return this.clientSessionManager.iceServers;
|
|
9686
10391
|
}
|
|
10392
|
+
get callControl() {
|
|
10393
|
+
return this.clientSessionManager.callControl;
|
|
10394
|
+
}
|
|
9687
10395
|
async execute(request, options) {
|
|
9688
10396
|
return this.clientSessionManager.execute(request, options);
|
|
9689
10397
|
}
|
|
@@ -9789,7 +10497,7 @@ function resolveExpiresAt(data) {
|
|
|
9789
10497
|
if (data.expires_at) return data.expires_at;
|
|
9790
10498
|
if (data.expires_in) return Math.floor(Date.now() / 1e3) + data.expires_in;
|
|
9791
10499
|
logger$7.warn("[DeviceToken] Could not determine token expiry, using default");
|
|
9792
|
-
return Math.floor(Date.now() / 1e3) + DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
|
|
10500
|
+
return Math.floor(Date.now() / 1e3) + require_operators.DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
|
|
9793
10501
|
}
|
|
9794
10502
|
/**
|
|
9795
10503
|
* Resolves the token TTL in seconds from a fresh response.
|
|
@@ -9802,7 +10510,7 @@ function resolveExpiresAt(data) {
|
|
|
9802
10510
|
function resolveExpireIn(data) {
|
|
9803
10511
|
if (data.expires_in) return data.expires_in;
|
|
9804
10512
|
if (data.expires_at) return Math.max(data.expires_at - Math.floor(Date.now() / 1e3), 1);
|
|
9805
|
-
return DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
|
|
10513
|
+
return require_operators.DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
|
|
9806
10514
|
}
|
|
9807
10515
|
/**
|
|
9808
10516
|
* Manages the Client Bound SAT lifecycle: activation, token exchange,
|
|
@@ -9822,10 +10530,10 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9822
10530
|
this._currentToken$ = this.createBehaviorSubject(null);
|
|
9823
10531
|
this._refreshInProgress = false;
|
|
9824
10532
|
this._paused = false;
|
|
9825
|
-
this._effectiveExpireIn = DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
|
|
10533
|
+
this._effectiveExpireIn = require_operators.DEVICE_TOKEN_DEFAULT_EXPIRE_IN;
|
|
9826
10534
|
this.subscribeTo(this._currentToken$.pipe((0, rxjs.filter)(Boolean), (0, rxjs.switchMap)((tokenData) => {
|
|
9827
10535
|
const expiresAt = resolveExpiresAt(tokenData);
|
|
9828
|
-
const refreshIn = Math.max(expiresAt * 1e3 - Date.now() - DEVICE_TOKEN_REFRESH_BUFFER_MS, 1e3);
|
|
10536
|
+
const refreshIn = Math.max(expiresAt * 1e3 - Date.now() - require_operators.DEVICE_TOKEN_REFRESH_BUFFER_MS, 1e3);
|
|
9829
10537
|
logger$7.debug(`[DeviceToken] Scheduling Client Bound SAT refresh in ${refreshIn}ms`);
|
|
9830
10538
|
return (0, rxjs.timer)(refreshIn);
|
|
9831
10539
|
})), () => {
|
|
@@ -9853,7 +10561,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9853
10561
|
*/
|
|
9854
10562
|
async activate(user, session, updateCredential) {
|
|
9855
10563
|
const { satClaims } = user;
|
|
9856
|
-
if (!satClaims?.scope?.includes(SAT_REFRESH_SCOPE)) {
|
|
10564
|
+
if (!satClaims?.scope?.includes(require_operators.SAT_REFRESH_SCOPE)) {
|
|
9857
10565
|
logger$7.debug("[DeviceToken] No sat:refresh scope, skipping Client Bound SAT activation");
|
|
9858
10566
|
return {
|
|
9859
10567
|
activated: false,
|
|
@@ -9878,7 +10586,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9878
10586
|
await session.reauthenticate(tokenData.token, rpcProof, { clientBound: true });
|
|
9879
10587
|
updateCredential({ token: tokenData.token });
|
|
9880
10588
|
logger$7.info("[DeviceToken] Client Bound SAT activated successfully");
|
|
9881
|
-
this.
|
|
10589
|
+
this.emitCurrentToken(tokenData);
|
|
9882
10590
|
return { activated: true };
|
|
9883
10591
|
} catch (error) {
|
|
9884
10592
|
logger$7.error("[DeviceToken] Failed to activate Client Bound SAT:", error);
|
|
@@ -9890,13 +10598,28 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9890
10598
|
}
|
|
9891
10599
|
}
|
|
9892
10600
|
/**
|
|
10601
|
+
* Emit a freshly received token to the reactive pipeline, stamping an
|
|
10602
|
+
* absolute `expires_at` when the response carried only `expires_in`.
|
|
10603
|
+
* Resolving the expiry at RECEIVE time (not at read time) is what lets
|
|
10604
|
+
* {@link refreshNowIfDue} detect due-ness on resume: a bare `expires_in`
|
|
10605
|
+
* re-resolved later would always compute a full TTL from "now" and never
|
|
10606
|
+
* cross the refresh buffer.
|
|
10607
|
+
*/
|
|
10608
|
+
emitCurrentToken(token) {
|
|
10609
|
+
const stamped = token.expires_at ? token : {
|
|
10610
|
+
...token,
|
|
10611
|
+
expires_at: resolveExpiresAt(token)
|
|
10612
|
+
};
|
|
10613
|
+
this._currentToken$.next(stamped);
|
|
10614
|
+
}
|
|
10615
|
+
/**
|
|
9893
10616
|
* Returns true when the cached token has enough headroom before expiry to
|
|
9894
10617
|
* be safely reused on reactivation. The headroom matches the refresh
|
|
9895
10618
|
* buffer, so a token within the refresh window is treated as stale (the
|
|
9896
10619
|
* reactive pipeline is about to refresh it anyway).
|
|
9897
10620
|
*/
|
|
9898
10621
|
isTokenFresh(token) {
|
|
9899
|
-
return resolveExpiresAt(token) * 1e3 - Date.now() > DEVICE_TOKEN_REFRESH_BUFFER_MS;
|
|
10622
|
+
return resolveExpiresAt(token) * 1e3 - Date.now() > require_operators.DEVICE_TOKEN_REFRESH_BUFFER_MS;
|
|
9900
10623
|
}
|
|
9901
10624
|
/**
|
|
9902
10625
|
* Obtains a Client Bound SAT from `/api/fabric/subscriber/devices/token`.
|
|
@@ -9905,14 +10628,14 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9905
10628
|
async obtainToken() {
|
|
9906
10629
|
const dpopProof = await this.dpopManager.createHttpProof({
|
|
9907
10630
|
method: "POST",
|
|
9908
|
-
uri: DEVICE_TOKEN_ENDPOINT
|
|
10631
|
+
uri: require_operators.DEVICE_TOKEN_ENDPOINT
|
|
9909
10632
|
});
|
|
9910
|
-
const response = await this.http.request({
|
|
9911
|
-
url: DEVICE_TOKEN_ENDPOINT,
|
|
10633
|
+
const response = await this.http().request({
|
|
10634
|
+
url: require_operators.DEVICE_TOKEN_ENDPOINT,
|
|
9912
10635
|
...POST_PARAMS,
|
|
9913
10636
|
body: JSON.stringify({
|
|
9914
10637
|
dpop_token: dpopProof,
|
|
9915
|
-
expire_in: DEVICE_TOKEN_DEFAULT_EXPIRE_IN
|
|
10638
|
+
expire_in: require_operators.DEVICE_TOKEN_DEFAULT_EXPIRE_IN
|
|
9916
10639
|
})
|
|
9917
10640
|
});
|
|
9918
10641
|
if (!response.ok || !response.body) throw new require_operators.DeviceTokenError(`Failed to obtain device token: ${response.status} ${response.statusText}`);
|
|
@@ -9931,11 +10654,11 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9931
10654
|
logger$7.debug("[DeviceToken] Refreshing Client Bound SAT");
|
|
9932
10655
|
const dpopProof = await this.dpopManager.createHttpProof({
|
|
9933
10656
|
method: "POST",
|
|
9934
|
-
uri: DEVICE_REFRESH_ENDPOINT,
|
|
10657
|
+
uri: require_operators.DEVICE_REFRESH_ENDPOINT,
|
|
9935
10658
|
accessToken: currentToken
|
|
9936
10659
|
});
|
|
9937
|
-
const response = await this.http.request({
|
|
9938
|
-
url: DEVICE_REFRESH_ENDPOINT,
|
|
10660
|
+
const response = await this.http().request({
|
|
10661
|
+
url: require_operators.DEVICE_REFRESH_ENDPOINT,
|
|
9939
10662
|
...POST_PARAMS,
|
|
9940
10663
|
body: JSON.stringify({
|
|
9941
10664
|
dpop_token: dpopProof,
|
|
@@ -9982,7 +10705,7 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9982
10705
|
const currentToken = this.getCredential().token;
|
|
9983
10706
|
if (!currentToken) throw new require_operators.TokenRefreshError("No current token available for refresh");
|
|
9984
10707
|
const newTokenData = await this.retryRefresh(session, currentToken, updateCredential);
|
|
9985
|
-
this.
|
|
10708
|
+
this.emitCurrentToken(newTokenData);
|
|
9986
10709
|
} catch (error) {
|
|
9987
10710
|
logger$7.error("[DeviceToken] Automatic Client Bound SAT refresh failed:", error);
|
|
9988
10711
|
this.errorHandler(error instanceof require_operators.TokenRefreshError ? error : new require_operators.TokenRefreshError("Automatic token refresh failed", error));
|
|
@@ -9996,12 +10719,12 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
9996
10719
|
*/
|
|
9997
10720
|
async retryRefresh(session, currentToken, updateCredential) {
|
|
9998
10721
|
let lastError;
|
|
9999
|
-
for (let attempt = 0; attempt < DEVICE_TOKEN_REFRESH_MAX_RETRIES; attempt++) try {
|
|
10722
|
+
for (let attempt = 0; attempt < require_operators.DEVICE_TOKEN_REFRESH_MAX_RETRIES; attempt++) try {
|
|
10000
10723
|
return await this.refreshToken(session, currentToken, updateCredential);
|
|
10001
10724
|
} catch (error) {
|
|
10002
10725
|
lastError = error;
|
|
10003
|
-
if (attempt < DEVICE_TOKEN_REFRESH_MAX_RETRIES - 1) {
|
|
10004
|
-
const delay = DEVICE_TOKEN_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt);
|
|
10726
|
+
if (attempt < require_operators.DEVICE_TOKEN_REFRESH_MAX_RETRIES - 1) {
|
|
10727
|
+
const delay = require_operators.DEVICE_TOKEN_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt);
|
|
10005
10728
|
logger$7.warn(`[DeviceToken] Refresh attempt ${attempt + 1} failed, retrying in ${delay}ms`);
|
|
10006
10729
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
10007
10730
|
}
|
|
@@ -10009,6 +10732,22 @@ var DeviceTokenManager = class extends Destroyable {
|
|
|
10009
10732
|
throw lastError instanceof Error ? lastError : new require_operators.TokenRefreshError("All refresh retries exhausted", lastError);
|
|
10010
10733
|
}
|
|
10011
10734
|
/**
|
|
10735
|
+
* Force an immediate refresh when the cached Client Bound SAT is already
|
|
10736
|
+
* past its refresh window. Called on resume from suspension where
|
|
10737
|
+
* background-tab throttling can delay the reactive timer past the buffer.
|
|
10738
|
+
* A no-op when no token is cached or it still has headroom; the normal
|
|
10739
|
+
* {@link executeRefresh} guards (paused / in-progress / unauthenticated)
|
|
10740
|
+
* still apply.
|
|
10741
|
+
*/
|
|
10742
|
+
refreshNowIfDue() {
|
|
10743
|
+
const token = this._currentToken$.value;
|
|
10744
|
+
if (!token) return;
|
|
10745
|
+
if (resolveExpiresAt(token) * 1e3 - Date.now() <= require_operators.DEVICE_TOKEN_REFRESH_BUFFER_MS) {
|
|
10746
|
+
logger$7.debug("[DeviceToken] Resume: cached SAT past refresh window; refreshing now");
|
|
10747
|
+
this.executeRefresh();
|
|
10748
|
+
}
|
|
10749
|
+
}
|
|
10750
|
+
/**
|
|
10012
10751
|
* Stops the reactive refresh pipeline from firing. Use when the underlying
|
|
10013
10752
|
* session is being torn down (e.g., during {@link SignalWire.disconnect})
|
|
10014
10753
|
* so a scheduled refresh cannot fire against a destroyed session.
|
|
@@ -10059,6 +10798,7 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
10059
10798
|
this.deps = deps;
|
|
10060
10799
|
this._activating = false;
|
|
10061
10800
|
this._activationGeneration = 0;
|
|
10801
|
+
this._developerRefreshInProgress = false;
|
|
10062
10802
|
if (dpopManager?.initialized) this._deviceTokenManager = (deps.deviceTokenManagerFactory ?? defaultDeviceTokenManagerFactory)(dpopManager, deps.http, (error) => deps.notifier.onError(error), () => deps.store.read());
|
|
10063
10803
|
}
|
|
10064
10804
|
/** True when the Client Bound SAT path is available (DPoP initialized). */
|
|
@@ -10077,28 +10817,120 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
10077
10817
|
* invokes `deps.onRefreshExhausted` so the orchestrator can disconnect.
|
|
10078
10818
|
*/
|
|
10079
10819
|
scheduleDeveloperRefresh(provider, expiresAt, attempt = 0) {
|
|
10820
|
+
this._activeProvider = provider;
|
|
10080
10821
|
if (this._developerTimerId !== void 0) clearTimeout(this._developerTimerId);
|
|
10081
|
-
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);
|
|
10082
|
-
this._developerTimerId = setTimeout(
|
|
10822
|
+
const refreshInterval = attempt === 0 ? Math.max(expiresAt - Date.now() - require_operators.CREDENTIAL_REFRESH_BUFFER_MS, 1e3) : Math.min(require_operators.CREDENTIAL_REFRESH_RETRY_BASE_MS * Math.pow(2, attempt) * (.5 + Math.random() * .5), require_operators.CREDENTIAL_REFRESH_MAX_DELAY_MS);
|
|
10823
|
+
this._developerTimerId = setTimeout(() => {
|
|
10824
|
+
this._developerTimerId = void 0;
|
|
10825
|
+
this.executeDeveloperRefresh(provider, expiresAt, attempt);
|
|
10826
|
+
}, refreshInterval);
|
|
10827
|
+
}
|
|
10828
|
+
/**
|
|
10829
|
+
* Runs the developer-provided refresh once: mints a new credential, stores
|
|
10830
|
+
* and persists it, reauthenticates the live session (via the notifier), and
|
|
10831
|
+
* reschedules against the new expiry. On failure retries with backoff up to
|
|
10832
|
+
* {@link CREDENTIAL_REFRESH_MAX_RETRIES}, then signals exhaustion.
|
|
10833
|
+
*
|
|
10834
|
+
* Shared by the scheduled timer tick and {@link forceRefreshIfDue}. The
|
|
10835
|
+
* `_developerRefreshInProgress` guard prevents the two from overlapping.
|
|
10836
|
+
*/
|
|
10837
|
+
async executeDeveloperRefresh(provider, expiresAt, attempt) {
|
|
10838
|
+
if (this._developerRefreshInProgress) {
|
|
10839
|
+
logger$6.debug("[Coordinator] Developer refresh already in progress; skipping");
|
|
10840
|
+
return;
|
|
10841
|
+
}
|
|
10842
|
+
this._developerRefreshInProgress = true;
|
|
10843
|
+
try {
|
|
10844
|
+
const newCredentials = await this.refreshCredential(provider);
|
|
10845
|
+
this.deps.store.write(newCredentials);
|
|
10846
|
+
this.deps.store.persist(newCredentials);
|
|
10083
10847
|
try {
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
this.deps.store.persist(newCredentials);
|
|
10088
|
-
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
10089
|
-
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
10090
|
-
} catch (error) {
|
|
10091
|
-
const nextAttempt = attempt + 1;
|
|
10092
|
-
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
10093
|
-
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
10094
|
-
if (nextAttempt < CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
10095
|
-
else {
|
|
10096
|
-
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
10097
|
-
this.deps.notifier.onError(new require_operators.TokenRefreshError("Credential refresh failed after max retries"));
|
|
10098
|
-
this.deps.notifier.onRefreshExhausted();
|
|
10099
|
-
}
|
|
10848
|
+
await this.deps.notifier.onCredentialRefreshed(newCredentials);
|
|
10849
|
+
} catch (reauthError) {
|
|
10850
|
+
logger$6.warn("[Coordinator] onCredentialRefreshed rejected (non-fatal):", reauthError);
|
|
10100
10851
|
}
|
|
10101
|
-
|
|
10852
|
+
logger$6.info("[Coordinator] Credentials refreshed successfully.");
|
|
10853
|
+
if (newCredentials.expiry_at) this.scheduleDeveloperRefresh(provider, newCredentials.expiry_at, 0);
|
|
10854
|
+
} catch (error) {
|
|
10855
|
+
const nextAttempt = attempt + 1;
|
|
10856
|
+
logger$6.error(`[Coordinator] Credential refresh failed (attempt ${nextAttempt}/${require_operators.CREDENTIAL_REFRESH_MAX_RETRIES}):`, error);
|
|
10857
|
+
this.deps.notifier.onError(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
10858
|
+
if (nextAttempt < require_operators.CREDENTIAL_REFRESH_MAX_RETRIES) this.scheduleDeveloperRefresh(provider, expiresAt, nextAttempt);
|
|
10859
|
+
else {
|
|
10860
|
+
logger$6.error("[Coordinator] Credential refresh exhausted all retries. Disconnecting.");
|
|
10861
|
+
this.deps.notifier.onError(new require_operators.TokenRefreshError("Credential refresh failed after max retries"));
|
|
10862
|
+
this.deps.notifier.onRefreshExhausted();
|
|
10863
|
+
}
|
|
10864
|
+
} finally {
|
|
10865
|
+
this._developerRefreshInProgress = false;
|
|
10866
|
+
}
|
|
10867
|
+
}
|
|
10868
|
+
/**
|
|
10869
|
+
* Force an immediate refresh when the current credential is already past its
|
|
10870
|
+
* scheduled refresh window. Called on resume from suspension, where
|
|
10871
|
+
* background-tab timer throttling can delay the armed refresh well past
|
|
10872
|
+
* expiry, leaving the live session stale.
|
|
10873
|
+
*
|
|
10874
|
+
* Routes to whichever mechanism is armed: the developer timer if armed,
|
|
10875
|
+
* otherwise the Client Bound SAT pipeline. A no-op when nothing is due.
|
|
10876
|
+
*/
|
|
10877
|
+
forceRefreshIfDue() {
|
|
10878
|
+
if (this._developerTimerId !== void 0 && this._activeProvider) {
|
|
10879
|
+
const expiry = this.deps.store.read().expiry_at;
|
|
10880
|
+
if (expiry !== void 0 && Date.now() >= expiry - require_operators.CREDENTIAL_REFRESH_BUFFER_MS) {
|
|
10881
|
+
logger$6.debug("[Coordinator] Resume: credential past refresh window; forcing refresh");
|
|
10882
|
+
clearTimeout(this._developerTimerId);
|
|
10883
|
+
this._developerTimerId = void 0;
|
|
10884
|
+
this.executeDeveloperRefresh(this._activeProvider, expiry, 0);
|
|
10885
|
+
}
|
|
10886
|
+
return;
|
|
10887
|
+
}
|
|
10888
|
+
this._deviceTokenManager?.refreshNowIfDue();
|
|
10889
|
+
}
|
|
10890
|
+
/**
|
|
10891
|
+
* Sync the credential's expiry from the server-provided authorization (the
|
|
10892
|
+
* `signalwire.connect` result). SATs are opaque JWE, so
|
|
10893
|
+
* `fabric_subscriber.expires_at` is the authoritative expiry of the token
|
|
10894
|
+
* the session actually connected with — the provider-reported `expiry_at`
|
|
10895
|
+
* is only a hint (and may be wrong or absent). Corrects the stored
|
|
10896
|
+
* credential and re-arms the developer refresh timer against the real
|
|
10897
|
+
* deadline when the provider supports `refresh()`.
|
|
10898
|
+
*/
|
|
10899
|
+
syncExpiryFromAuthorization(authorization, provider) {
|
|
10900
|
+
const expiresAtSec = authorization?.fabric_subscriber?.expires_at;
|
|
10901
|
+
if (!expiresAtSec) return;
|
|
10902
|
+
const expiryAt = expiresAtSec * 1e3;
|
|
10903
|
+
const credential = this.deps.store.read();
|
|
10904
|
+
if (credential.expiry_at === expiryAt) return;
|
|
10905
|
+
logger$6.debug(`[Coordinator] Correcting credential expiry from server authorization: ${new Date(expiryAt).toISOString()}`);
|
|
10906
|
+
const updated = {
|
|
10907
|
+
...credential,
|
|
10908
|
+
expiry_at: expiryAt
|
|
10909
|
+
};
|
|
10910
|
+
this.deps.store.write(updated);
|
|
10911
|
+
this.deps.store.persist(updated);
|
|
10912
|
+
if (provider?.refresh) this.scheduleDeveloperRefresh(provider, expiryAt);
|
|
10913
|
+
}
|
|
10914
|
+
/**
|
|
10915
|
+
* Invoke `provider.refresh()` deduped against any concurrent developer
|
|
10916
|
+
* refresh. Concurrent callers — the scheduled tick, a resume-forced refresh,
|
|
10917
|
+
* and the orchestrator's -32003 recovery / reconnect re-mint — share one
|
|
10918
|
+
* in-flight promise, so a provider backed by one-time-use rotating refresh
|
|
10919
|
+
* tokens is never invoked twice in parallel.
|
|
10920
|
+
*
|
|
10921
|
+
* The caller owns applying the returned credential (store write, session
|
|
10922
|
+
* reauth, rescheduling); this method only serializes the network call.
|
|
10923
|
+
*/
|
|
10924
|
+
async refreshCredential(provider) {
|
|
10925
|
+
if (this._refreshInFlight) return this._refreshInFlight;
|
|
10926
|
+
if (!provider.refresh) throw new require_operators.InvalidCredentialsError("Credential provider does not support refresh");
|
|
10927
|
+
const run = provider.refresh();
|
|
10928
|
+
this._refreshInFlight = run;
|
|
10929
|
+
const clear = () => {
|
|
10930
|
+
if (this._refreshInFlight === run) this._refreshInFlight = void 0;
|
|
10931
|
+
};
|
|
10932
|
+
run.then(clear, clear);
|
|
10933
|
+
return run;
|
|
10102
10934
|
}
|
|
10103
10935
|
/**
|
|
10104
10936
|
* Cancels any scheduled developer-provided refresh. Idempotent.
|
|
@@ -10195,7 +11027,7 @@ var CredentialRefreshCoordinator = class extends Destroyable {
|
|
|
10195
11027
|
activated: false,
|
|
10196
11028
|
reason: "activation-timeout"
|
|
10197
11029
|
});
|
|
10198
|
-
}, CREDENTIAL_ACTIVATE_TIMEOUT_MS);
|
|
11030
|
+
}, require_operators.CREDENTIAL_ACTIVATE_TIMEOUT_MS);
|
|
10199
11031
|
inner.then((result) => {
|
|
10200
11032
|
clearTimeout(timer$3);
|
|
10201
11033
|
resolve(result);
|
|
@@ -10878,6 +11710,13 @@ var TransportManager = class extends Destroyable {
|
|
|
10878
11710
|
//#endregion
|
|
10879
11711
|
//#region src/clients/SignalWire.ts
|
|
10880
11712
|
const logger$1 = require_operators.getLogger();
|
|
11713
|
+
/**
|
|
11714
|
+
* Storage key for the client-bound marker. The SAT and authorization_state are
|
|
11715
|
+
* both opaque to the SDK, so on a page reload the preflight recovery — which
|
|
11716
|
+
* runs before any session exists — has no other way to know the session was
|
|
11717
|
+
* client-bound. See {@link SignalWire.persistClientBoundMarker}.
|
|
11718
|
+
*/
|
|
11719
|
+
const CLIENT_BOUND_STORAGE_KEY = "sw:client_bound";
|
|
10881
11720
|
const buildOptionsFromDestination = (destination) => {
|
|
10882
11721
|
if (typeof destination === "string") {
|
|
10883
11722
|
const queryStartIndex = destination.indexOf("?");
|
|
@@ -10921,6 +11760,7 @@ var SignalWire = class extends Destroyable {
|
|
|
10921
11760
|
this.preferences = new ClientPreferences();
|
|
10922
11761
|
this._user$ = this.createBehaviorSubject(void 0);
|
|
10923
11762
|
this._directory$ = this.createBehaviorSubject(void 0);
|
|
11763
|
+
this._credentialRecovered = false;
|
|
10924
11764
|
this._isConnected$ = this.createBehaviorSubject(false);
|
|
10925
11765
|
this._isRegistered$ = this.createBehaviorSubject(false);
|
|
10926
11766
|
this._errors$ = this.createReplaySubject(1);
|
|
@@ -10959,6 +11799,16 @@ var SignalWire = class extends Destroyable {
|
|
|
10959
11799
|
});
|
|
10960
11800
|
}
|
|
10961
11801
|
/**
|
|
11802
|
+
* Build the refresh path's own HTTP controller, against whatever host is current.
|
|
11803
|
+
*
|
|
11804
|
+
* Called on first use rather than up front, so `apiHost` already reflects the
|
|
11805
|
+
* token's `ch` claim. Same credential source as the container's controller — only
|
|
11806
|
+
* the instance, and therefore its observable streams, is separate.
|
|
11807
|
+
*/
|
|
11808
|
+
createRefreshHttpController() {
|
|
11809
|
+
return new HTTPRequestController(this._deps.apiHost, () => this._deps.credential);
|
|
11810
|
+
}
|
|
11811
|
+
/**
|
|
10962
11812
|
* Initializes DPoP if not already set up. Returns the fingerprint on success.
|
|
10963
11813
|
*/
|
|
10964
11814
|
async initDPoP() {
|
|
@@ -10985,11 +11835,19 @@ var SignalWire = class extends Destroyable {
|
|
|
10985
11835
|
async resolveCredentials() {
|
|
10986
11836
|
const fingerprint = await this.initDPoP();
|
|
10987
11837
|
this._refreshCoordinator = new CredentialRefreshCoordinator(this._dpopManager, {
|
|
10988
|
-
http:
|
|
11838
|
+
http: () => {
|
|
11839
|
+
if (!this._refreshHttp || this._refreshHttpHost !== this._deps.apiHost) {
|
|
11840
|
+
this._refreshHttp?.destroy();
|
|
11841
|
+
this._refreshHttp = this.createRefreshHttpController();
|
|
11842
|
+
this._refreshHttpHost = this._deps.apiHost;
|
|
11843
|
+
}
|
|
11844
|
+
return this._refreshHttp;
|
|
11845
|
+
},
|
|
10989
11846
|
notifier: {
|
|
10990
11847
|
onError: (error) => this._errors$.next(error),
|
|
10991
11848
|
onWarning: (warning) => this._warnings$.next(warning),
|
|
10992
|
-
onRefreshExhausted: () => void this.disconnect()
|
|
11849
|
+
onRefreshExhausted: () => void this.disconnect(),
|
|
11850
|
+
onCredentialRefreshed: async (credential) => this.reauthenticateLiveSession(credential)
|
|
10993
11851
|
},
|
|
10994
11852
|
store: {
|
|
10995
11853
|
read: () => this._deps.credential,
|
|
@@ -11001,6 +11859,7 @@ var SignalWire = class extends Destroyable {
|
|
|
11001
11859
|
...this._deps.credential,
|
|
11002
11860
|
...partial
|
|
11003
11861
|
};
|
|
11862
|
+
this.persistCredential(this._deps.credential);
|
|
11004
11863
|
},
|
|
11005
11864
|
persist: (credential) => this.persistCredential(credential)
|
|
11006
11865
|
}
|
|
@@ -11046,20 +11905,181 @@ var SignalWire = class extends Destroyable {
|
|
|
11046
11905
|
}
|
|
11047
11906
|
this._deps.credential = _credentials;
|
|
11048
11907
|
this.persistCredential(_credentials);
|
|
11049
|
-
|
|
11050
|
-
|
|
11908
|
+
await this.reauthenticateLiveSession(_credentials);
|
|
11909
|
+
}
|
|
11910
|
+
/**
|
|
11911
|
+
* Reauthenticate the currently-open session with a freshly obtained
|
|
11912
|
+
* credential so the new token takes effect on the live socket immediately —
|
|
11913
|
+
* not just on the next reconnect. No-op when the session is not
|
|
11914
|
+
* connected/authenticated or the credential carries no token (e.g. an
|
|
11915
|
+
* authorization-state-only refresh). Non-fatal: reauth failures surface on
|
|
11916
|
+
* `errors$` without aborting the refresh that triggered this.
|
|
11917
|
+
*/
|
|
11918
|
+
async reauthenticateLiveSession(credential) {
|
|
11919
|
+
if (!this.isConnected || !this._clientSession.authenticated || !credential.token) return;
|
|
11920
|
+
try {
|
|
11921
|
+
await this._clientSession.reauthenticate(credential.token);
|
|
11051
11922
|
logger$1.info("[SignalWire] Session refreshed with new credentials.");
|
|
11052
11923
|
} catch (error) {
|
|
11053
11924
|
logger$1.error("[SignalWire] Failed to refresh session with new credentials:", error);
|
|
11054
11925
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
11055
11926
|
}
|
|
11056
11927
|
}
|
|
11928
|
+
/**
|
|
11929
|
+
* Recover a session the server is refusing: mint a fresh credential,
|
|
11930
|
+
* reauthenticate the live session with it, and retry the operation.
|
|
11931
|
+
*
|
|
11932
|
+
* The connection is deliberately kept. A reload authenticates the new socket
|
|
11933
|
+
* against the persisted `authorization_state`, and that handshake is what
|
|
11934
|
+
* associates the socket with the previous session — the association reattach
|
|
11935
|
+
* depends on. `signalwire.reauthenticate` swaps the credential *on that same
|
|
11936
|
+
* session*, so recovery never touches the resume state. Discarding it would
|
|
11937
|
+
* heal the credential by destroying the very thing the caller is trying to
|
|
11938
|
+
* get back to.
|
|
11939
|
+
*
|
|
11940
|
+
* The operation is still the verdict, never the RPC. Reauthenticating with
|
|
11941
|
+
* the in-memory token is accepted by a resume even while requests stay
|
|
11942
|
+
* refused, because the persisted `authorization_state` short-circuits token
|
|
11943
|
+
* validation — and `signalwire.reauthenticate` with a *freshly minted* token
|
|
11944
|
+
* has also been observed accepted while `subscriber.online` keeps being
|
|
11945
|
+
* refused (staging run 33826974634). Both look like success and are not.
|
|
11946
|
+
*
|
|
11947
|
+
* @returns the operation's value, or the reason recovery could not deliver
|
|
11948
|
+
* one. `error` is undefined when there was no way to mint at all.
|
|
11949
|
+
*/
|
|
11950
|
+
async recoverAndRetry(operation) {
|
|
11951
|
+
if (!await this.remintAndReauthenticate()) return { ok: false };
|
|
11952
|
+
try {
|
|
11953
|
+
const value = await operation();
|
|
11954
|
+
this._credentialRecovered = true;
|
|
11955
|
+
return {
|
|
11956
|
+
ok: true,
|
|
11957
|
+
value
|
|
11958
|
+
};
|
|
11959
|
+
} catch (error) {
|
|
11960
|
+
logger$1.warn("[SignalWire] Reauthentication was accepted but the operation is still refused:", error);
|
|
11961
|
+
return {
|
|
11962
|
+
ok: false,
|
|
11963
|
+
error
|
|
11964
|
+
};
|
|
11965
|
+
}
|
|
11966
|
+
}
|
|
11967
|
+
/**
|
|
11968
|
+
* Re-mint a credential and adopt it only if the live session accepts it.
|
|
11969
|
+
*
|
|
11970
|
+
* The mechanism follows the binding: a client-bound session re-mints a bound
|
|
11971
|
+
* base SAT through `authenticate()` with the DPoP fingerprint, because the
|
|
11972
|
+
* developer refresh handler would hand back an unbound token and silently
|
|
11973
|
+
* degrade the session. An unbound session uses the refresh handler. Rotation
|
|
11974
|
+
* cost is not a reason to skip this — the only reason is having no mechanism.
|
|
11975
|
+
*
|
|
11976
|
+
* @returns whether the session is now running on a freshly accepted credential.
|
|
11977
|
+
*/
|
|
11978
|
+
async remintAndReauthenticate() {
|
|
11979
|
+
const provider = this._credentialProvider;
|
|
11980
|
+
if (!provider) return false;
|
|
11981
|
+
const { clientBound } = this._clientSession;
|
|
11982
|
+
if (!clientBound && !provider.refresh) {
|
|
11983
|
+
logger$1.debug("[SignalWire] [SW-NO-REFRESH-HANDLER] Unbound session with no refresh handler; cannot re-mint.");
|
|
11984
|
+
return false;
|
|
11985
|
+
}
|
|
11986
|
+
try {
|
|
11987
|
+
const newCredentials = clientBound ? await provider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0) : await this.remintCredential(provider);
|
|
11988
|
+
if (!newCredentials.token) {
|
|
11989
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the current one.");
|
|
11990
|
+
return false;
|
|
11991
|
+
}
|
|
11992
|
+
await this._clientSession.reauthenticate(newCredentials.token);
|
|
11993
|
+
this._deps.credential = newCredentials;
|
|
11994
|
+
this.persistCredential(newCredentials);
|
|
11995
|
+
if (newCredentials.expiry_at && provider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(provider, newCredentials.expiry_at);
|
|
11996
|
+
return true;
|
|
11997
|
+
} catch (error) {
|
|
11998
|
+
logger$1.warn("[SignalWire] Re-mint recovery failed:", error);
|
|
11999
|
+
return false;
|
|
12000
|
+
}
|
|
12001
|
+
}
|
|
12002
|
+
/**
|
|
12003
|
+
* Re-mint a credential via `provider.refresh()`, routed through the
|
|
12004
|
+
* coordinator's shared in-flight guard so concurrent re-mint paths (a
|
|
12005
|
+
* scheduled/resume refresh, -32003 recovery, and reconnect) never fire a
|
|
12006
|
+
* second `provider.refresh()` in parallel — which rotating one-time-use
|
|
12007
|
+
* refresh tokens reject. Falls back to a direct call only if the coordinator
|
|
12008
|
+
* has not been constructed yet.
|
|
12009
|
+
*/
|
|
12010
|
+
async remintCredential(provider) {
|
|
12011
|
+
if (this._refreshCoordinator) return this._refreshCoordinator.refreshCredential(provider);
|
|
12012
|
+
if (!provider.refresh) throw new require_operators.InvalidCredentialsError("Credential provider does not support refresh");
|
|
12013
|
+
return provider.refresh();
|
|
12014
|
+
}
|
|
12015
|
+
/**
|
|
12016
|
+
* Re-mint credentials before a fresh (re)connect (`onBeforeReconnect` hook).
|
|
12017
|
+
* The session invokes this only when it is client-bound OR the in-memory
|
|
12018
|
+
* token is expired. The re-mint mechanism depends on the binding:
|
|
12019
|
+
* - Client-bound: `authenticate()` with the DPoP fingerprint to obtain a
|
|
12020
|
+
* fresh base SAT the upcoming reconnect can re-bind (the
|
|
12021
|
+
* DeviceTokenManager re-activates afterwards).
|
|
12022
|
+
* - Unbound: the developer's non-interactive `refresh()` handler.
|
|
12023
|
+
* `authenticate()` is deliberately NOT used here — it may be interactive
|
|
12024
|
+
* (a login prompt) and must not fire on a background reconnect.
|
|
12025
|
+
*
|
|
12026
|
+
* Rejects on failure so the session aborts the reconnect rather than
|
|
12027
|
+
* replaying a stale token.
|
|
12028
|
+
*/
|
|
12029
|
+
async refreshCredentialForReconnect() {
|
|
12030
|
+
if (!this._credentialProvider) return;
|
|
12031
|
+
try {
|
|
12032
|
+
let newCredentials;
|
|
12033
|
+
if (this._clientSession?.clientBound ?? await this.wasClientBound()) {
|
|
12034
|
+
logger$1.debug("[SignalWire] Re-minting client-bound base SAT before reconnect");
|
|
12035
|
+
newCredentials = await this._credentialProvider.authenticate(this._dpopManager?.initialized ? { fingerprint: this._dpopManager.fingerprint } : void 0);
|
|
12036
|
+
} else if (this._credentialProvider.refresh) {
|
|
12037
|
+
logger$1.debug("[SignalWire] Refreshing unbound credential before reconnect");
|
|
12038
|
+
newCredentials = await this.remintCredential(this._credentialProvider);
|
|
12039
|
+
} else {
|
|
12040
|
+
logger$1.warn("[SignalWire] [SW-NO-REFRESH-HANDLER] Token expired on reconnect but no refresh handler; reconnecting with the existing token.");
|
|
12041
|
+
return;
|
|
12042
|
+
}
|
|
12043
|
+
if (!newCredentials.token) {
|
|
12044
|
+
logger$1.warn("[SignalWire] Re-minted credential has no token; keeping the existing credential for reconnect.");
|
|
12045
|
+
return;
|
|
12046
|
+
}
|
|
12047
|
+
this._deps.credential = newCredentials;
|
|
12048
|
+
this.persistCredential(newCredentials);
|
|
12049
|
+
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
12050
|
+
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
12051
|
+
} catch (error) {
|
|
12052
|
+
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
12053
|
+
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
12054
|
+
throw error;
|
|
12055
|
+
}
|
|
12056
|
+
}
|
|
11057
12057
|
/** Persist credential to localStorage when persistSession is enabled. */
|
|
11058
12058
|
persistCredential(credential) {
|
|
11059
12059
|
if (!credential.token) return;
|
|
11060
12060
|
this._deps.storage.setItem("sw:cached_credential", credential);
|
|
11061
12061
|
if (this._deps.persistSession) this._deps.storage.setItem("sw:cached_credential", credential, "local");
|
|
11062
12062
|
}
|
|
12063
|
+
/**
|
|
12064
|
+
* Persist whether the session is client-bound, mirroring the credential's
|
|
12065
|
+
* storage scopes so it survives a reload. The preflight recovery reads it
|
|
12066
|
+
* before any session exists to decide whether to re-bind via `authenticate()`
|
|
12067
|
+
* or refresh an unbound token; the marker tracks the latest binding, so an
|
|
12068
|
+
* unbound reconnect clears a stale marker from an earlier client-bound login.
|
|
12069
|
+
*/
|
|
12070
|
+
persistClientBoundMarker(bound) {
|
|
12071
|
+
const scopes = this._deps.persistSession ? ["session", "local"] : ["session"];
|
|
12072
|
+
for (const scope of scopes) if (bound) this._deps.storage.setItem(CLIENT_BOUND_STORAGE_KEY, true, scope);
|
|
12073
|
+
else this._deps.storage.removeItem(CLIENT_BOUND_STORAGE_KEY, scope);
|
|
12074
|
+
}
|
|
12075
|
+
/** Read the persisted client-bound marker (see {@link persistClientBoundMarker}). */
|
|
12076
|
+
async wasClientBound() {
|
|
12077
|
+
const scopes = this._deps.persistSession ? ["local", "session"] : ["session"];
|
|
12078
|
+
for (const scope of scopes) try {
|
|
12079
|
+
if (await this._deps.storage.getItem(CLIENT_BOUND_STORAGE_KEY, scope)) return true;
|
|
12080
|
+
} catch {}
|
|
12081
|
+
return false;
|
|
12082
|
+
}
|
|
11063
12083
|
async init() {
|
|
11064
12084
|
this._user$.next(new User(this._deps.http));
|
|
11065
12085
|
if (!this._options.skipConnection) await this.connect();
|
|
@@ -11083,6 +12103,42 @@ var SignalWire = class extends Destroyable {
|
|
|
11083
12103
|
} catch (error) {
|
|
11084
12104
|
logger$1.error("[SignalWire] Failed to reattach calls:", error);
|
|
11085
12105
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
12106
|
+
} finally {
|
|
12107
|
+
this._credentialRecovered = false;
|
|
12108
|
+
}
|
|
12109
|
+
}
|
|
12110
|
+
/**
|
|
12111
|
+
* Fetch the authenticated user profile, recovering a stale credential.
|
|
12112
|
+
*
|
|
12113
|
+
* On a reload the persisted credential can be expired. Unlike the WS resume —
|
|
12114
|
+
* which the server accepts against the persisted `authorization_state` even
|
|
12115
|
+
* with an expired token — this REST preflight has no such short-circuit and is
|
|
12116
|
+
* refused (401). There is no session yet to reauthenticate, so recovery
|
|
12117
|
+
* re-mints the credential through the provider ({@link refreshCredentialForReconnect})
|
|
12118
|
+
* and retries with a FRESH {@link User}: Fetchable memoizes its result
|
|
12119
|
+
* (shareReplay), so reusing the instance would replay the 401 instead of
|
|
12120
|
+
* re-fetching with the new token. Without the user id the transport/session —
|
|
12121
|
+
* and the reattach a reload is trying to preserve — cannot even be addressed.
|
|
12122
|
+
*/
|
|
12123
|
+
async fetchUserOrRecover() {
|
|
12124
|
+
const fetchUser = async (user$1) => {
|
|
12125
|
+
if (!await (0, rxjs.firstValueFrom)(user$1.fetched$)) throw new require_operators.UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
12126
|
+
this._deps.user = user$1;
|
|
12127
|
+
};
|
|
12128
|
+
const user = this._user$.value;
|
|
12129
|
+
if (!user) throw new require_operators.UnexpectedError("User not initialized before connect");
|
|
12130
|
+
try {
|
|
12131
|
+
await fetchUser(user);
|
|
12132
|
+
} catch (firstError) {
|
|
12133
|
+
logger$1.error(`[SignalWire] Failed to fetch user information: ${firstError instanceof Error ? firstError.message : "Unknown error"}. This usually means the user token is invalid or expired. Re-minting the credential and retrying.`);
|
|
12134
|
+
try {
|
|
12135
|
+
await this.refreshCredentialForReconnect();
|
|
12136
|
+
const refetched = new User(this._deps.http);
|
|
12137
|
+
await fetchUser(refetched);
|
|
12138
|
+
this._user$.next(refetched);
|
|
12139
|
+
} catch (retryError) {
|
|
12140
|
+
throw new require_operators.UnexpectedError("Error fetching user information", { cause: retryError });
|
|
12141
|
+
}
|
|
11086
12142
|
}
|
|
11087
12143
|
}
|
|
11088
12144
|
/**
|
|
@@ -11124,40 +12180,23 @@ var SignalWire = class extends Destroyable {
|
|
|
11124
12180
|
*/
|
|
11125
12181
|
async connect() {
|
|
11126
12182
|
await this.teardownTransportAndSession();
|
|
11127
|
-
|
|
11128
|
-
const user = this._user$.value;
|
|
11129
|
-
if (!user) throw new require_operators.UnexpectedError("User not initialized before connect");
|
|
11130
|
-
if (!await (0, rxjs.firstValueFrom)(user.fetched$)) throw new require_operators.UnexpectedError("Failed to fetch user information - fetched$ emitted false");
|
|
11131
|
-
this._deps.user = user;
|
|
11132
|
-
} catch (error) {
|
|
11133
|
-
logger$1.error(`[SignalWire] Failed to fetch user information: ${error instanceof Error ? error.message : "Unknown error"}. This usually means the user token is invalid or expired.`);
|
|
11134
|
-
throw new require_operators.UnexpectedError("Error fetching user information", { cause: error });
|
|
11135
|
-
}
|
|
12183
|
+
await this.fetchUserOrRecover();
|
|
11136
12184
|
const errorHandler = (error) => {
|
|
11137
12185
|
this._errors$.next(error);
|
|
11138
12186
|
};
|
|
11139
12187
|
this._transport = new TransportManager(this._deps.storage, this._deps.protocolKey, this._deps.WebSocket, PreferencesContainer.instance.relayHost ?? this._deps.relayHost, errorHandler);
|
|
11140
|
-
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey);
|
|
12188
|
+
this._attachManager = new AttachManager(this._deps.storage, this._deps.deviceController, PreferencesContainer.instance.reconnectCallsTimeout, this._deps.attachedCallsKey, () => this._credentialRecovered);
|
|
11141
12189
|
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$);
|
|
11142
12190
|
this._publicSession = new ClientSessionWrapper(this._clientSession);
|
|
11143
|
-
this._clientSession.
|
|
11144
|
-
|
|
11145
|
-
try {
|
|
11146
|
-
const fingerprint = this._dpopManager?.initialized ? this._dpopManager.fingerprint : void 0;
|
|
11147
|
-
logger$1.debug("[SignalWire] Credential expired, refreshing before reconnect");
|
|
11148
|
-
const newCredentials = await this._credentialProvider.authenticate(fingerprint ? { fingerprint } : void 0);
|
|
11149
|
-
this._deps.credential = newCredentials;
|
|
11150
|
-
if (newCredentials.expiry_at && this._credentialProvider.refresh) this._refreshCoordinator?.scheduleDeveloperRefresh(this._credentialProvider, newCredentials.expiry_at);
|
|
11151
|
-
logger$1.debug("[SignalWire] Credential refreshed successfully for reconnect");
|
|
11152
|
-
} catch (error) {
|
|
11153
|
-
logger$1.error("[SignalWire] Failed to refresh credentials for reconnect:", error);
|
|
11154
|
-
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
11155
|
-
throw error;
|
|
11156
|
-
}
|
|
11157
|
-
};
|
|
12191
|
+
this._clientSession.callControl = this._options.callControl ?? "routed";
|
|
12192
|
+
this._clientSession.onBeforeReconnect = async () => this.refreshCredentialForReconnect();
|
|
11158
12193
|
this.subscribeTo(this._clientSession.errors$, (error) => {
|
|
11159
12194
|
this._errors$.next(error);
|
|
11160
12195
|
});
|
|
12196
|
+
this.subscribeTo(this._clientSession.authorization$, (authorization) => {
|
|
12197
|
+
this._refreshCoordinator?.syncExpiryFromAuthorization(authorization, this._credentialProvider);
|
|
12198
|
+
this.persistClientBoundMarker(Boolean(authorization?.cnf?.jkt));
|
|
12199
|
+
});
|
|
11161
12200
|
await this._clientSession.connect();
|
|
11162
12201
|
await this._refreshCoordinator?.activate(this._deps.user, this._clientSession);
|
|
11163
12202
|
this.subscribeTo(this._clientSession.authenticated$.pipe((0, rxjs.skip)(1), (0, rxjs.filter)(Boolean)), async () => {
|
|
@@ -11320,6 +12359,13 @@ var SignalWire = class extends Destroyable {
|
|
|
11320
12359
|
}
|
|
11321
12360
|
try {
|
|
11322
12361
|
this._visibilityController = new VisibilityController();
|
|
12362
|
+
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, rxjs.filter)((event) => event.to === "visible")), () => {
|
|
12363
|
+
try {
|
|
12364
|
+
this._refreshCoordinator?.forceRefreshIfDue();
|
|
12365
|
+
} catch (error) {
|
|
12366
|
+
logger$1.warn("[SignalWire] Resume credential revalidation failed (non-fatal):", error);
|
|
12367
|
+
}
|
|
12368
|
+
});
|
|
11323
12369
|
this.subscribeTo(this._visibilityController.visibilityChange$.pipe((0, rxjs.filter)((event) => event.to === "visible" && PreferencesContainer.instance.refreshDevicesOnVisible)), () => {
|
|
11324
12370
|
logger$1.debug("[SignalWire] Page visible, re-enumerating devices");
|
|
11325
12371
|
try {
|
|
@@ -11331,7 +12377,7 @@ var SignalWire = class extends Destroyable {
|
|
|
11331
12377
|
logger$1.warn("[SignalWire] Failed to initialize VisibilityController:", error);
|
|
11332
12378
|
}
|
|
11333
12379
|
try {
|
|
11334
|
-
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.
|
|
12380
|
+
this._diagnosticsCollector = new DiagnosticsCollector({ sdkVersion: "4.0.0-rc.3" });
|
|
11335
12381
|
} catch (error) {
|
|
11336
12382
|
logger$1.warn("[SignalWire] Failed to initialize DiagnosticsCollector:", error);
|
|
11337
12383
|
}
|
|
@@ -11402,21 +12448,22 @@ var SignalWire = class extends Destroyable {
|
|
|
11402
12448
|
this._errors$.next(error instanceof Error ? error : new Error(String(error), { cause: error }));
|
|
11403
12449
|
throw error;
|
|
11404
12450
|
}
|
|
11405
|
-
logger$1.debug("[SignalWire] Failed to register user,
|
|
11406
|
-
|
|
11407
|
-
await this._clientSession.reauthenticate(this._deps.credential.token);
|
|
11408
|
-
logger$1.debug("[SignalWire] Reauthentication successful, retrying register()");
|
|
12451
|
+
logger$1.debug("[SignalWire] Failed to register user, attempting credential recovery...");
|
|
12452
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
11409
12453
|
await this._transport.execute(RPCExecute({
|
|
11410
12454
|
method: "subscriber.online",
|
|
11411
12455
|
params: {}
|
|
11412
12456
|
}));
|
|
12457
|
+
});
|
|
12458
|
+
if (outcome.ok) {
|
|
12459
|
+
logger$1.debug("[SignalWire] Recovery restored registration");
|
|
11413
12460
|
this._isRegistered$.next(true);
|
|
11414
|
-
|
|
11415
|
-
logger$1.error("[SignalWire] Reauthentication failed during register():", reauthError);
|
|
11416
|
-
const registerError = new require_operators.InvalidCredentialsError("Failed to register user, and reauthentication attempt also failed. Please check your credentials.", { cause: reauthError instanceof Error ? reauthError : new Error(String(reauthError), { cause: reauthError }) });
|
|
11417
|
-
this._errors$.next(registerError);
|
|
11418
|
-
throw registerError;
|
|
12461
|
+
return;
|
|
11419
12462
|
}
|
|
12463
|
+
const failureCause = outcome.error ?? error;
|
|
12464
|
+
const registerError = new require_operators.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 }) });
|
|
12465
|
+
this._errors$.next(registerError);
|
|
12466
|
+
throw registerError;
|
|
11420
12467
|
}
|
|
11421
12468
|
}
|
|
11422
12469
|
/**
|
|
@@ -11447,6 +12494,11 @@ var SignalWire = class extends Destroyable {
|
|
|
11447
12494
|
* Returns a {@link Call} in `'ringing'` state. Subscribe to {@link Call.status$}
|
|
11448
12495
|
* to track progression through `'connected'` → `'disconnected'`.
|
|
11449
12496
|
*
|
|
12497
|
+
* Local media acquisition is deliberately unbounded: an unanswered permission
|
|
12498
|
+
* prompt leaves this promise pending indefinitely, so apply your own bound if
|
|
12499
|
+
* your UI needs one. The 12 s signaling budget starts only once acquisition
|
|
12500
|
+
* settles.
|
|
12501
|
+
*
|
|
11450
12502
|
* @param destination - Address URI string (e.g. `'/public/my-room'`) or {@link Address} instance.
|
|
11451
12503
|
* @param options - Media and dial options (audio/video, device constraints). Overrides defaults.
|
|
11452
12504
|
* @returns The created {@link Call} instance.
|
|
@@ -11469,7 +12521,18 @@ var SignalWire = class extends Destroyable {
|
|
|
11469
12521
|
};
|
|
11470
12522
|
await this.waitAuthentication();
|
|
11471
12523
|
logger$1.debug("[SignalWire] Dialing with options:", computed_options);
|
|
11472
|
-
|
|
12524
|
+
try {
|
|
12525
|
+
return await this._clientSession.createOutboundCall(destination, computed_options);
|
|
12526
|
+
} catch (error) {
|
|
12527
|
+
if (!isRecoverableAuthError(error)) throw error;
|
|
12528
|
+
logger$1.debug("[SignalWire] Dial hit a recoverable auth error; recovering the session and retrying");
|
|
12529
|
+
const outcome = await this.recoverAndRetry(async () => {
|
|
12530
|
+
await this.waitAuthentication();
|
|
12531
|
+
return this._clientSession.createOutboundCall(destination, computed_options);
|
|
12532
|
+
});
|
|
12533
|
+
if (outcome.ok) return outcome.value;
|
|
12534
|
+
throw outcome.error ?? error;
|
|
12535
|
+
}
|
|
11473
12536
|
}
|
|
11474
12537
|
/**
|
|
11475
12538
|
* Runs a multi-phase connectivity test against the given destination.
|
|
@@ -11750,10 +12813,13 @@ var SignalWire = class extends Destroyable {
|
|
|
11750
12813
|
destroy() {
|
|
11751
12814
|
this._refreshCoordinator?.destroy();
|
|
11752
12815
|
this._refreshCoordinator = void 0;
|
|
12816
|
+
this._refreshHttp?.destroy();
|
|
12817
|
+
this._refreshHttp = void 0;
|
|
11753
12818
|
this._dpopManager?.destroy();
|
|
11754
|
-
this._clientSession
|
|
11755
|
-
|
|
11756
|
-
this.
|
|
12819
|
+
const session = this._clientSession;
|
|
12820
|
+
session?.teardownSessionState();
|
|
12821
|
+
this._transport?.destroy();
|
|
12822
|
+
session?.destroy();
|
|
11757
12823
|
try {
|
|
11758
12824
|
this._networkMonitor?.destroy();
|
|
11759
12825
|
} catch {}
|
|
@@ -11865,7 +12931,7 @@ var StaticCredentialProvider = class {
|
|
|
11865
12931
|
/**
|
|
11866
12932
|
* Library version from package.json, injected at build time.
|
|
11867
12933
|
*/
|
|
11868
|
-
const version = "4.0.0-rc.
|
|
12934
|
+
const version = "4.0.0-rc.3";
|
|
11869
12935
|
/**
|
|
11870
12936
|
* Flag indicating the library has been loaded and is ready to use.
|
|
11871
12937
|
* For UMD builds: `window.SignalWire.ready`
|
|
@@ -11887,7 +12953,7 @@ const ready = true;
|
|
|
11887
12953
|
*/
|
|
11888
12954
|
const emitReadyEvent = () => {
|
|
11889
12955
|
if (typeof window !== "undefined") {
|
|
11890
|
-
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.
|
|
12956
|
+
const event = new CustomEvent("signalwire:js:ready", { detail: { version: "4.0.0-rc.3" } });
|
|
11891
12957
|
window.dispatchEvent(event);
|
|
11892
12958
|
}
|
|
11893
12959
|
};
|
|
@@ -11895,19 +12961,25 @@ emitReadyEvent();
|
|
|
11895
12961
|
|
|
11896
12962
|
//#endregion
|
|
11897
12963
|
exports.Address = Address;
|
|
12964
|
+
exports.AuxiliaryLegCancelledError = require_operators.AuxiliaryLegCancelledError;
|
|
12965
|
+
exports.AuxiliaryLegTimeoutError = require_operators.AuxiliaryLegTimeoutError;
|
|
11898
12966
|
exports.CallCreateError = require_operators.CallCreateError;
|
|
12967
|
+
exports.CallNotReadyError = require_operators.CallNotReadyError;
|
|
11899
12968
|
exports.ClientPreferences = ClientPreferences;
|
|
11900
12969
|
exports.CollectionFetchError = require_operators.CollectionFetchError;
|
|
11901
12970
|
exports.DPoPInitError = require_operators.DPoPInitError;
|
|
11902
12971
|
exports.DeviceTokenError = require_operators.DeviceTokenError;
|
|
11903
12972
|
exports.EmbedTokenCredentialProvider = EmbedTokenCredentialProvider;
|
|
11904
12973
|
exports.InvalidCredentialsError = require_operators.InvalidCredentialsError;
|
|
12974
|
+
exports.MediaAccessError = require_operators.MediaAccessError;
|
|
11905
12975
|
exports.MediaTrackError = require_operators.MediaTrackError;
|
|
11906
12976
|
exports.MessageParseError = require_operators.MessageParseError;
|
|
11907
12977
|
exports.OverconstrainedFallbackError = require_operators.OverconstrainedFallbackError;
|
|
11908
12978
|
exports.Participant = Participant;
|
|
12979
|
+
exports.ParticipantNotReadyError = require_operators.ParticipantNotReadyError;
|
|
11909
12980
|
exports.PreflightError = require_operators.PreflightError;
|
|
11910
12981
|
exports.RecoveryError = require_operators.RecoveryError;
|
|
12982
|
+
exports.ScreenShareAlreadyActiveError = require_operators.ScreenShareAlreadyActiveError;
|
|
11911
12983
|
exports.SelfCapabilities = SelfCapabilities;
|
|
11912
12984
|
exports.SelfParticipant = SelfParticipant;
|
|
11913
12985
|
exports.SignalWire = SignalWire;
|