@apocaliss92/nodelink-js 0.1.17 → 0.1.20

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.
@@ -3,10 +3,10 @@ import {
3
3
  BaichuanRtspServer,
4
4
  ReolinkBaichuanApi,
5
5
  autoDetectDeviceType
6
- } from "../chunk-ULSFEQSE.js";
6
+ } from "../chunk-EHWVA3SG.js";
7
7
  import {
8
8
  __require
9
- } from "../chunk-ZE7D7LI4.js";
9
+ } from "../chunk-YPU7RAEY.js";
10
10
 
11
11
  // src/cli/rtsp-server.ts
12
12
  function parseArgs() {
package/dist/index.cjs CHANGED
@@ -1807,6 +1807,22 @@ function detectVideoCodecFromNal(data) {
1807
1807
  if (nalStart < 0 || nalStart >= data.length) return null;
1808
1808
  const nalByte = data[nalStart];
1809
1809
  if (nalByte === void 0) return null;
1810
+ const forbiddenBit264 = nalByte >> 7 & 1;
1811
+ const h264Type = nalByte & 31;
1812
+ if (forbiddenBit264 === 0 && h264Type > 0 && h264Type <= 12) {
1813
+ if (h264Type === 7 || h264Type === 8) {
1814
+ return "H264";
1815
+ }
1816
+ if (h264Type === 5) {
1817
+ return "H264";
1818
+ }
1819
+ if (h264Type === 1) {
1820
+ const nalRefIdc = nalByte >> 5 & 3;
1821
+ if (nalRefIdc >= 1) {
1822
+ return "H264";
1823
+ }
1824
+ }
1825
+ }
1810
1826
  if (nalStart + 1 < data.length) {
1811
1827
  const nalByte2 = data[nalStart + 1];
1812
1828
  if (nalByte2 !== void 0) {
@@ -1820,22 +1836,12 @@ function detectVideoCodecFromNal(data) {
1820
1836
  if (hevcType === 19 || hevcType === 20 || hevcType === 21) {
1821
1837
  return "H265";
1822
1838
  }
1823
- if (hevcType <= 9) {
1839
+ if (hevcType <= 1 && nalByte <= 3) {
1824
1840
  return "H265";
1825
1841
  }
1826
1842
  }
1827
1843
  }
1828
1844
  }
1829
- const forbiddenBit264 = nalByte >> 7 & 1;
1830
- const h264Type = nalByte & 31;
1831
- if (forbiddenBit264 === 0 && h264Type > 0 && h264Type <= 12) {
1832
- if (h264Type === 7 || h264Type === 8) {
1833
- return "H264";
1834
- }
1835
- if (h264Type === 5 || h264Type === 1) {
1836
- return "H264";
1837
- }
1838
- }
1839
1845
  return null;
1840
1846
  }
1841
1847
  var ANNEXB_START_CODE_4B, BcMediaAnnexBDecoder;
@@ -16211,7 +16217,8 @@ var logDebugStreamBlock = (params) => {
16211
16217
  const bitRateText = getXmlText(raw, "bitRate") ?? getXmlText(raw, "BitRate");
16212
16218
  const videoEncTypeText = getXmlText(raw, "videoEncType") ?? getXmlText(raw, "VideoEncType");
16213
16219
  const audioText = getXmlText(raw, "audio") ?? getXmlText(raw, "Audio");
16214
- const enableText = getXmlText(raw, "enable") ?? getXmlText(raw, "Enable");
16220
+ const enableCheckRaw = raw.replace(/<smartH265[\s\S]*?<\/smartH265>/g, "");
16221
+ const enableText = getXmlText(enableCheckRaw, "enable") ?? getXmlText(enableCheckRaw, "Enable");
16215
16222
  const width = Number(widthText ?? "0");
16216
16223
  const height = Number(heightText ?? "0");
16217
16224
  const frameRate = Number(frameText ?? "0");
@@ -16235,7 +16242,8 @@ var buildStream = (params) => {
16235
16242
  const frameRate = Number(getXmlText(streamXml, "frame") ?? "0");
16236
16243
  const bitRate = Number(getXmlText(streamXml, "bitRate") ?? "0");
16237
16244
  const audio = Number(getXmlText(streamXml, "audio") ?? "0");
16238
- const enabled = getXmlText(streamXml, "enable");
16245
+ const enableXml = streamXml.replace(/<smartH265[\s\S]*?<\/smartH265>/g, "");
16246
+ const enabled = getXmlText(enableXml, "enable");
16239
16247
  const isEnabled = isEnabledFromText(enabled);
16240
16248
  if (!isEnabled || !isPlausibleStream({ width, height, frameRate, bitRate }))
16241
16249
  return void 0;
@@ -16924,14 +16932,16 @@ var DUAL_LENS_SINGLE_MOTION_MODELS = /* @__PURE__ */ new Set([
16924
16932
  "Reolink TrackMix",
16925
16933
  "Reolink TrackMix PoE",
16926
16934
  "Reolink TrackMix WiFi",
16927
- "RLC-81MA"
16935
+ "RLC-81MA",
16936
+ "TrackFlex Floodlight WiFi"
16928
16937
  ]);
16929
16938
  var DUAL_LENS_MODELS = /* @__PURE__ */ new Set([
16930
16939
  ...DUAL_LENS_DUAL_MOTION_MODELS,
16931
16940
  ...DUAL_LENS_SINGLE_MOTION_MODELS
16932
16941
  ]);
16933
16942
  var isDualLenseModel = (model) => {
16934
- return DUAL_LENS_MODELS.has(model) || model.toLowerCase().includes("trackmix");
16943
+ const lower = model.toLowerCase();
16944
+ return Array.from(DUAL_LENS_MODELS).some((m) => m.toLowerCase() === lower) || lower.includes("trackmix") || lower.includes("trackflex");
16935
16945
  };
16936
16946
  var NVR_HUB_EXACT_TYPES = ["NVR", "WIFI_NVR", "HOMEHUB"];
16937
16947
  var NVR_HUB_MODEL_PATTERNS = [
@@ -17015,6 +17025,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
17015
17025
  * Set via setIsNvr() from the plugin or auto-detected via isNvrDevice().
17016
17026
  */
17017
17027
  _isNvr;
17028
+ /**
17029
+ * Cached multi-focal detection result.
17030
+ * - true = dual-lens camera (e.g., TrackMix, TrackFlex) with multiple channels on a single device
17031
+ * - false = single-lens camera
17032
+ * Multi-focal cameras reject concurrent streaming TCP connections (response_code 430),
17033
+ * so all channels must multiplex on the same streaming socket.
17034
+ */
17035
+ _isMultiFocal;
17018
17036
  /** Maximum dedicated sessions allowed before triggering a reboot (default: 7). */
17019
17037
  maxDedicatedSessionsBeforeReboot;
17020
17038
  sessionGuardRebootInFlight;
@@ -17045,6 +17063,15 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
17045
17063
  simpleEventResubscribeTimer;
17046
17064
  simpleEventResubscribeInFlight;
17047
17065
  simpleEventResubscribeIntervalMs = 5 * 6e4;
17066
+ // Event watchdog: auto-recovery when events stop flowing or subscription fails
17067
+ simpleEventWatchdogTimer;
17068
+ simpleEventLastReceivedAt = 0;
17069
+ simpleEventWatchdogRecoveryAttempts = 0;
17070
+ simpleEventWatchdogLastRecoveryAt = 0;
17071
+ simpleEventWatchdogIntervalMs = 1e4;
17072
+ // check every 10s
17073
+ simpleEventWatchdogSilenceThresholdMs = 5 * 6e4;
17074
+ // 5 min without events
17048
17075
  statePollingInterval;
17049
17076
  udpSleepInferenceInterval;
17050
17077
  udpLastInferredSleepStateByChannel = /* @__PURE__ */ new Map();
@@ -17429,14 +17456,16 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
17429
17456
  * Determine the socket tag for a given sessionKey.
17430
17457
  * This implements the tag-based allocation strategy:
17431
17458
  *
17432
- * - "general" - commands, events
17433
- * - "streaming:ch{N}" - main + sub for channel N (closed when no streams)
17434
- * - "streaming:ch{N}:ext" - ext for channel N (closed when no streams)
17459
+ * - "general" - commands, events, ext on ch0
17460
+ * - "streaming:ch{N}" - main + sub for channel N (NVR/standalone single-lens)
17461
+ * - "streaming:ch{N}:ext" - ext for channel N (NVR/standalone, N>0)
17462
+ * - "streaming:a" - ch0 main + ch1 sub (multi-focal dedicated socket)
17463
+ * - "general" also carries ch1 main + ch0 sub for multi-focal (merged with commands/events)
17435
17464
  * - "replay:deviceId:ch{N}" - dedicated per device+channel for replay
17436
17465
  *
17437
- * Always uses per-channel tagging for streams (works for both standalone and NVR).
17438
- * Replay uses per-device+channel sockets to allow multiple users to watch
17439
- * different clips simultaneously without interfering with each other.
17466
+ * Multi-focal cameras (TrackMix, TrackFlex, Duo) reject two main or two sub
17467
+ * streams on the same TCP connection (response_code 430). Cross-channel pairing
17468
+ * ensures each socket always has a valid M+S combination using only 2 TCP connections.
17440
17469
  *
17441
17470
  * @param sessionKey - The session key (e.g., "live:device:ch0:main", "replay:device:ch1:file")
17442
17471
  * @returns The socket pool tag to use
@@ -17456,9 +17485,16 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
17456
17485
  if (channel === 0) {
17457
17486
  return "general";
17458
17487
  }
17459
- return `streaming:ch${channel}:ext`;
17488
+ if (this._isMultiFocal) {
17489
+ return "general";
17490
+ }
17491
+ return this._isNvr ? `streaming:ch${channel}:ext` : `streaming:ch${channel}:ext`;
17460
17492
  }
17461
- return `streaming:ch${channel}`;
17493
+ if (this._isMultiFocal) {
17494
+ const isSocketA = channel === 0 && profile === "main" || channel === 1 && profile === "sub";
17495
+ return isSocketA ? "streaming:a" : "general";
17496
+ }
17497
+ return this._isNvr ? `streaming:ch${channel}` : `streaming:ch${channel}`;
17462
17498
  }
17463
17499
  return "general";
17464
17500
  }
@@ -17783,6 +17819,14 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
17783
17819
  async cleanupDedicatedClients() {
17784
17820
  }
17785
17821
  dispatchSimpleEvent(evt) {
17822
+ this.simpleEventLastReceivedAt = Date.now();
17823
+ if (this.simpleEventWatchdogRecoveryAttempts > 0) {
17824
+ (this.logger.info ?? this.logger.log).call(
17825
+ this.logger,
17826
+ `[ReolinkBaichuanApi] event watchdog: events flowing again after ${this.simpleEventWatchdogRecoveryAttempts} recovery attempt(s)`
17827
+ );
17828
+ this.simpleEventWatchdogRecoveryAttempts = 0;
17829
+ }
17786
17830
  const debugCfg = this.client.getDebugConfig?.();
17787
17831
  if (debugCfg) {
17788
17832
  const sid = this.client.getSocketSessionId?.();
@@ -18331,11 +18375,15 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18331
18375
  /**
18332
18376
  * Subscribe to minimal high-level events.
18333
18377
  * The API manages Baichuan subscribe/unsubscribe automatically.
18378
+ * Includes built-in watchdog: if no events arrive for 5 minutes while
18379
+ * the connection is alive, the subscription is automatically renewed.
18334
18380
  */
18335
18381
  async onSimpleEvent(callback) {
18336
18382
  this.simpleEventListeners.add(callback);
18337
18383
  await this.ensureSimpleEventSubscribed();
18384
+ this.simpleEventLastReceivedAt = Date.now();
18338
18385
  this.startSimpleEventResubscribeTimer();
18386
+ this.startSimpleEventWatchdog();
18339
18387
  }
18340
18388
  /**
18341
18389
  * Remove one callback, or all callbacks if omitted.
@@ -18349,6 +18397,7 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18349
18397
  }
18350
18398
  if (this.simpleEventListeners.size === 0) {
18351
18399
  this.stopSimpleEventResubscribeTimer();
18400
+ this.stopSimpleEventWatchdog();
18352
18401
  this.stopUdpSleepInference();
18353
18402
  await this.ensureSimpleEventUnsubscribed();
18354
18403
  } else {
@@ -18372,6 +18421,97 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18372
18421
  clearInterval(this.simpleEventResubscribeTimer);
18373
18422
  this.simpleEventResubscribeTimer = void 0;
18374
18423
  }
18424
+ /**
18425
+ * Event watchdog: monitors whether events are flowing and auto-recovers if they stop.
18426
+ *
18427
+ * Handles two failure modes:
18428
+ * 1. Subscription flag is true but no events arrive for 5+ minutes (device dropped subscription silently)
18429
+ * 2. Subscription flag is false because initial/retry subscribe failed, but connection is now alive
18430
+ *
18431
+ * Uses exponential backoff (30s → 60s → 120s → 240s → max 5min) to avoid hammering the device.
18432
+ */
18433
+ startSimpleEventWatchdog() {
18434
+ if (this.simpleEventWatchdogTimer) return;
18435
+ if (this.simpleEventListeners.size === 0) return;
18436
+ this.simpleEventWatchdogTimer = setInterval(() => {
18437
+ void this.simpleEventWatchdogTick();
18438
+ }, this.simpleEventWatchdogIntervalMs);
18439
+ }
18440
+ stopSimpleEventWatchdog() {
18441
+ if (!this.simpleEventWatchdogTimer) return;
18442
+ clearInterval(this.simpleEventWatchdogTimer);
18443
+ this.simpleEventWatchdogTimer = void 0;
18444
+ this.simpleEventWatchdogRecoveryAttempts = 0;
18445
+ this.simpleEventWatchdogLastRecoveryAt = 0;
18446
+ this.simpleEventLastReceivedAt = 0;
18447
+ }
18448
+ async simpleEventWatchdogTick() {
18449
+ if (this.simpleEventListeners.size === 0) return;
18450
+ if (!this.client.isSocketConnected?.() || !this.client.loggedIn) return;
18451
+ const now = Date.now();
18452
+ if (this.simpleEventSubscribed && this.simpleEventLastReceivedAt > 0) {
18453
+ const silence = now - this.simpleEventLastReceivedAt;
18454
+ if (silence < this.simpleEventWatchdogSilenceThresholdMs) return;
18455
+ (this.logger.warn ?? this.logger.log).call(
18456
+ this.logger,
18457
+ `[ReolinkBaichuanApi] event watchdog: no events for ${Math.round(silence / 6e4)} min, forcing resubscribe`,
18458
+ { host: this.host, silenceMs: silence }
18459
+ );
18460
+ try {
18461
+ this.simpleEventSubscribed = false;
18462
+ this.client.subscribed = false;
18463
+ await this.ensureSimpleEventSubscribed();
18464
+ this.simpleEventLastReceivedAt = Date.now();
18465
+ this.simpleEventWatchdogRecoveryAttempts = 0;
18466
+ (this.logger.info ?? this.logger.log).call(
18467
+ this.logger,
18468
+ `[ReolinkBaichuanApi] event watchdog: resubscribed successfully after silence`
18469
+ );
18470
+ } catch (e) {
18471
+ (this.logger.debug ?? this.logger.log).call(
18472
+ this.logger,
18473
+ `[ReolinkBaichuanApi] event watchdog: resubscribe after silence failed`,
18474
+ formatErrorForLog(e)
18475
+ );
18476
+ }
18477
+ return;
18478
+ }
18479
+ if (!this.simpleEventSubscribed) {
18480
+ const backoffMs = Math.min(
18481
+ 3e4 * Math.pow(2, this.simpleEventWatchdogRecoveryAttempts),
18482
+ this.simpleEventWatchdogSilenceThresholdMs
18483
+ );
18484
+ if (now - this.simpleEventWatchdogLastRecoveryAt < backoffMs) return;
18485
+ this.simpleEventWatchdogRecoveryAttempts++;
18486
+ this.simpleEventWatchdogLastRecoveryAt = now;
18487
+ const nextBackoff = Math.min(
18488
+ 3e4 * Math.pow(2, this.simpleEventWatchdogRecoveryAttempts),
18489
+ this.simpleEventWatchdogSilenceThresholdMs
18490
+ );
18491
+ (this.logger.info ?? this.logger.log).call(
18492
+ this.logger,
18493
+ `[ReolinkBaichuanApi] event watchdog: subscription inactive, attempting auto-recovery (attempt #${this.simpleEventWatchdogRecoveryAttempts}, next backoff ${Math.round(nextBackoff / 1e3)}s)`,
18494
+ { host: this.host }
18495
+ );
18496
+ try {
18497
+ await this.ensureSimpleEventSubscribed();
18498
+ if (this.simpleEventSubscribed) {
18499
+ this.simpleEventLastReceivedAt = Date.now();
18500
+ (this.logger.info ?? this.logger.log).call(
18501
+ this.logger,
18502
+ `[ReolinkBaichuanApi] event watchdog: auto-recovery successful after ${this.simpleEventWatchdogRecoveryAttempts} attempt(s)`
18503
+ );
18504
+ this.simpleEventWatchdogRecoveryAttempts = 0;
18505
+ }
18506
+ } catch (e) {
18507
+ (this.logger.debug ?? this.logger.log).call(
18508
+ this.logger,
18509
+ `[ReolinkBaichuanApi] event watchdog: recovery attempt #${this.simpleEventWatchdogRecoveryAttempts} failed`,
18510
+ formatErrorForLog(e)
18511
+ );
18512
+ }
18513
+ }
18514
+ }
18375
18515
  async renewSimpleEventSubscription() {
18376
18516
  if (this.simpleEventListeners.size === 0) return;
18377
18517
  if (this.simpleEventResubscribeInFlight)
@@ -18510,6 +18650,17 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18510
18650
  this._isNvr = isNvr;
18511
18651
  this.logger.debug?.(`[ReolinkBaichuanApi] setIsNvr: ${isNvr}`);
18512
18652
  }
18653
+ /**
18654
+ * Set the multi-focal flag explicitly.
18655
+ * Call this early (before streaming) to ensure correct socket pooling.
18656
+ * Multi-focal cameras (TrackMix, TrackFlex, Duo, etc.) reject concurrent
18657
+ * streaming TCP connections, so all channels must share a single streaming socket.
18658
+ * @param isMultiFocal - true if this is a dual-lens/multi-focal camera
18659
+ */
18660
+ setIsMultiFocal(isMultiFocal) {
18661
+ this._isMultiFocal = isMultiFocal;
18662
+ this.logger.debug?.(`[ReolinkBaichuanApi] setIsMultiFocal: ${isMultiFocal}`);
18663
+ }
18513
18664
  /**
18514
18665
  * Enable or disable idle disconnect dynamically.
18515
18666
  *
@@ -18560,6 +18711,8 @@ var ReolinkBaichuanApi = class _ReolinkBaichuanApi {
18560
18711
  }
18561
18712
  this.stopStatePolling();
18562
18713
  this.stopUdpSleepInference();
18714
+ this.stopSimpleEventWatchdog();
18715
+ this.stopSimpleEventResubscribeTimer();
18563
18716
  await this.cleanup();
18564
18717
  await this.stopAllActiveStreams();
18565
18718
  await this.cleanupSocketPool();
@@ -24011,7 +24164,7 @@ ${xml}`
24011
24164
  });
24012
24165
  model = typeof info.type === "string" ? info.type.toLowerCase() : "";
24013
24166
  isMultiFocal = isDualLenseModel(model);
24014
- isTrackMix = model.includes("trackmix");
24167
+ isTrackMix = model.includes("trackmix") || model.includes("trackflex");
24015
24168
  } catch (e) {
24016
24169
  logDebug(
24017
24170
  "[ReolinkBaichuanApi] buildVideoStreamOptions: getInfo(type) failed",
@@ -28530,6 +28683,8 @@ var Rfc4571Muxer = class _Rfc4571Muxer {
28530
28683
  videoUsWrapOffset = 0;
28531
28684
  videoLastAbsUs;
28532
28685
  videoAvgDeltaUs;
28686
+ videoFramesSinceAbsUsChange = 0;
28687
+ videoEstimatedFrameDeltaUs;
28533
28688
  videoClockRate = 9e4;
28534
28689
  fallbackVideoIncrement;
28535
28690
  fallbackVideoDeltaUs;
@@ -28655,6 +28810,8 @@ var Rfc4571Muxer = class _Rfc4571Muxer {
28655
28810
  this.videoUsWrapOffset = 0;
28656
28811
  this.videoLastAbsUs = void 0;
28657
28812
  this.videoAvgDeltaUs = void 0;
28813
+ this.videoFramesSinceAbsUsChange = 0;
28814
+ this.videoEstimatedFrameDeltaUs = void 0;
28658
28815
  }
28659
28816
  logVideoTiming(kind, message) {
28660
28817
  const now = Date.now();
@@ -28725,20 +28882,44 @@ var Rfc4571Muxer = class _Rfc4571Muxer {
28725
28882
  this.videoAvgDeltaUs = this.fallbackVideoDeltaUs;
28726
28883
  return;
28727
28884
  }
28728
- const deltaUs = absUs - this.videoLastAbsUs;
28885
+ const lastAbsUs = this.videoLastAbsUs;
28886
+ const deltaUs = absUs - lastAbsUs;
28729
28887
  this.videoLastAbsUs = absUs;
28730
- const trusted = Number.isFinite(deltaUs) && deltaUs > 0 && deltaUs <= this.maxTrustedDeltaUs;
28731
28888
  let effectiveDeltaUs;
28732
- if (trusted) {
28733
- const prevAvg = this.videoAvgDeltaUs ?? deltaUs;
28734
- this.videoAvgDeltaUs = prevAvg + (deltaUs - prevAvg) * this.emaAlpha;
28735
- effectiveDeltaUs = deltaUs;
28889
+ if (!Number.isFinite(deltaUs) || deltaUs < 0) {
28890
+ this.logVideoTiming(
28891
+ "untrusted-delta",
28892
+ `discarded deltaUs=${deltaUs} (absUs=${absUs} lastAbsUs=${lastAbsUs} avgDeltaUs=${this.videoAvgDeltaUs ?? "n/a"}); using fallback`
28893
+ );
28894
+ this.videoFramesSinceAbsUsChange = 0;
28895
+ effectiveDeltaUs = this.videoEstimatedFrameDeltaUs ?? this.fallbackVideoDeltaUs;
28896
+ } else if (deltaUs === 0) {
28897
+ this.videoFramesSinceAbsUsChange++;
28898
+ effectiveDeltaUs = this.videoEstimatedFrameDeltaUs ?? this.fallbackVideoDeltaUs;
28899
+ } else if (deltaUs <= this.maxTrustedDeltaUs) {
28900
+ if (this.videoFramesSinceAbsUsChange > 0) {
28901
+ const framesCovered = this.videoFramesSinceAbsUsChange + 1;
28902
+ const perFrameDeltaUs = Math.max(
28903
+ 1,
28904
+ Math.round(deltaUs / framesCovered)
28905
+ );
28906
+ this.videoEstimatedFrameDeltaUs = perFrameDeltaUs;
28907
+ this.videoFramesSinceAbsUsChange = 0;
28908
+ const prevAvg = this.videoAvgDeltaUs ?? perFrameDeltaUs;
28909
+ this.videoAvgDeltaUs = prevAvg + (perFrameDeltaUs - prevAvg) * this.emaAlpha;
28910
+ effectiveDeltaUs = perFrameDeltaUs;
28911
+ } else {
28912
+ const prevAvg = this.videoAvgDeltaUs ?? deltaUs;
28913
+ this.videoAvgDeltaUs = prevAvg + (deltaUs - prevAvg) * this.emaAlpha;
28914
+ effectiveDeltaUs = deltaUs;
28915
+ }
28736
28916
  } else {
28737
28917
  this.logVideoTiming(
28738
28918
  "untrusted-delta",
28739
- `discarded deltaUs=${deltaUs} (absUs=${absUs} lastAbsUs=${this.videoLastAbsUs} avgDeltaUs=${this.videoAvgDeltaUs ?? "n/a"}); using fallback`
28919
+ `discarded deltaUs=${deltaUs} (absUs=${absUs} lastAbsUs=${lastAbsUs} avgDeltaUs=${this.videoAvgDeltaUs ?? "n/a"} sameCount=${this.videoFramesSinceAbsUsChange}); using fallback`
28740
28920
  );
28741
- effectiveDeltaUs = this.videoAvgDeltaUs ?? this.fallbackVideoDeltaUs;
28921
+ this.videoFramesSinceAbsUsChange = 0;
28922
+ effectiveDeltaUs = this.videoEstimatedFrameDeltaUs ?? this.fallbackVideoDeltaUs;
28742
28923
  }
28743
28924
  const inc = Math.max(
28744
28925
  1,
@@ -32241,6 +32422,9 @@ Error: ${err}`
32241
32422
  }
32242
32423
  const peerConnection = new RTCPeerConnection({
32243
32424
  iceServers,
32425
+ icePortRange: this.options.icePortRange,
32426
+ iceAdditionalHostAddresses: this.options.iceAdditionalHostAddresses,
32427
+ iceTransportPolicy: this.options.iceTransportPolicy,
32244
32428
  codecs: {
32245
32429
  video: [
32246
32430
  new RTCRtpCodecParameters({