@camstack/addon-pipeline 1.2.58 → 1.2.60

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.
@@ -21552,11 +21552,13 @@ var AdaptiveController = class {
21552
21552
  */
21553
21553
  armed = false;
21554
21554
  failures;
21555
+ upgradeCount;
21555
21556
  constructor(opts) {
21556
21557
  this.opts = opts;
21557
21558
  this.tracker = new LossTracker(opts.config);
21558
21559
  this.tier = opts.initialTier;
21559
21560
  this.failures = new Map(opts.priorFailures ?? []);
21561
+ this.upgradeCount = Math.max(0, opts.priorUpgradeCount ?? 0);
21560
21562
  }
21561
21563
  get currentTier() {
21562
21564
  return this.tier;
@@ -21569,6 +21571,9 @@ var AdaptiveController = class {
21569
21571
  get failureCounts() {
21570
21572
  return this.failures;
21571
21573
  }
21574
+ get successfulUpgradeCount() {
21575
+ return this.upgradeCount;
21576
+ }
21572
21577
  onLoss(sample, now) {
21573
21578
  this.tracker.track(sample, now);
21574
21579
  }
@@ -21621,8 +21626,9 @@ var AdaptiveController = class {
21621
21626
  */
21622
21627
  requiredHealthyMs(tier) {
21623
21628
  const failures = this.failures.get(tier) ?? 0;
21624
- if (failures <= 0) return this.opts.config.upgradeHealthyMs;
21625
- return Math.min(this.opts.config.upgradeHealthyMs * 2 ** failures, this.opts.config.maxUpgradeBackoffMs);
21629
+ const progressiveMs = Math.min(this.opts.config.upgradeHealthyMs * this.opts.config.upgradeHealthyGrowthFactor ** this.upgradeCount, this.opts.config.maxProgressiveUpgradeHealthyMs);
21630
+ if (failures <= 0) return progressiveMs;
21631
+ return Math.min(progressiveMs * 2 ** failures, this.opts.config.maxUpgradeBackoffMs);
21626
21632
  }
21627
21633
  commitChange(now) {
21628
21634
  this.lastChangeAt = now;
@@ -21659,6 +21665,7 @@ var AdaptiveController = class {
21659
21665
  this.tier = next;
21660
21666
  this.mtuReduced = false;
21661
21667
  this.transcodeEngaged = false;
21668
+ this.upgradeCount += 1;
21662
21669
  this.opts.onSwitchTier(next);
21663
21670
  this.commitChange(now);
21664
21671
  this.healthySince = now;
@@ -21718,21 +21725,32 @@ function clampPlaybackRate(rate) {
21718
21725
  if (!Number.isFinite(rate) || rate <= 0) return 0;
21719
21726
  return Math.min(4, Math.max(.25, rate));
21720
21727
  }
21721
- var ServerMessageSchema = require_dist.discriminatedUnion("t", [require_dist.object({
21722
- t: require_dist.literal("state"),
21723
- state: require_dist._enum([
21724
- "live",
21725
- "loading",
21726
- "playing",
21727
- "paused",
21728
- "gap",
21729
- "ended",
21730
- "error"
21731
- ])
21732
- }), require_dist.object({
21733
- t: require_dist.literal("position"),
21734
- epochMs: require_dist.number()
21735
- })]);
21728
+ var ServerMessageSchema = require_dist.discriminatedUnion("t", [
21729
+ require_dist.object({
21730
+ t: require_dist.literal("state"),
21731
+ state: require_dist._enum([
21732
+ "live",
21733
+ "loading",
21734
+ "playing",
21735
+ "paused",
21736
+ "gap",
21737
+ "ended",
21738
+ "error"
21739
+ ])
21740
+ }),
21741
+ require_dist.object({
21742
+ t: require_dist.literal("position"),
21743
+ epochMs: require_dist.number()
21744
+ }),
21745
+ require_dist.object({
21746
+ t: require_dist.literal("liveProfile"),
21747
+ profile: require_dist._enum([
21748
+ "high",
21749
+ "mid",
21750
+ "low"
21751
+ ])
21752
+ })
21753
+ ]);
21736
21754
  function parseControlMessage(raw) {
21737
21755
  try {
21738
21756
  const parsed = ControlMessageSchema.safeParse(JSON.parse(raw));
@@ -22071,7 +22089,7 @@ var SEEK_DISCONTINUITY_MS = 250;
22071
22089
  * sustainable there — cross-fragment moves remain paced by the demux + the
22072
22090
  * one-in-flight guard, so this cannot spawn parallel demuxes.
22073
22091
  */
22074
- var SCRUB_COALESCE_MS = 60;
22092
+ var SCRUB_COALESCE_MS = 33;
22075
22093
  /**
22076
22094
  * How much forward slack two fragments may have and still count as ADJACENT
22077
22095
  * for sweep continuity. Two real cases live inside it: the ±1 ms rounding of a
@@ -23608,6 +23626,12 @@ var TimelineSession = class {
23608
23626
  mode = "live";
23609
23627
  feeder = null;
23610
23628
  profile = "mid";
23629
+ /** Transport rate belongs to the session, not to one profile-specific feeder.
23630
+ * Scrub pins `low`; release restores the selected playback profile and may
23631
+ * therefore rebuild the feeder. Carrying the rate keeps a paused scrub
23632
+ * paused across that required quality transition. */
23633
+ rate = 1;
23634
+ liveProfileReported = false;
23611
23635
  /**
23612
23636
  * The last position the viewer was SHOWN (every `_emitPosition`). Handed to
23613
23637
  * `seek` as `travelFromMs` so the seek travel animation works across feeder
@@ -23631,6 +23655,7 @@ var TimelineSession = class {
23631
23655
  return this.mode;
23632
23656
  }
23633
23657
  async handleControl(msg) {
23658
+ this.emitLiveProfile();
23634
23659
  switch (msg.t) {
23635
23660
  case "playRecorded": {
23636
23661
  const profileChanged = msg.profile !== this.profile;
@@ -23659,6 +23684,7 @@ var TimelineSession = class {
23659
23684
  if (this.mode === "recorded") this.feeder?.scrubAck();
23660
23685
  break;
23661
23686
  case "setRate":
23687
+ this.rate = msg.rate;
23662
23688
  this.feeder?.setRate(msg.rate);
23663
23689
  break;
23664
23690
  case "stepFrame":
@@ -23683,6 +23709,7 @@ var TimelineSession = class {
23683
23709
  this.deps.session.setLiveFeedGate(true);
23684
23710
  this.mode = "recorded";
23685
23711
  const feeder = this.deps.makeFeeder(this.profile);
23712
+ if (this.rate !== 1) feeder.setRate(this.rate);
23686
23713
  this.feeder = feeder;
23687
23714
  return feeder;
23688
23715
  }
@@ -23710,6 +23737,14 @@ var TimelineSession = class {
23710
23737
  epochMs
23711
23738
  }));
23712
23739
  }
23740
+ emitLiveProfile() {
23741
+ if (this.liveProfileReported || this.deps.initialLiveProfile === void 0) return;
23742
+ this.liveProfileReported = true;
23743
+ this.deps.session.sendControl(serializeServerMessage({
23744
+ t: "liveProfile",
23745
+ profile: this.deps.initialLiveProfile
23746
+ }));
23747
+ }
23713
23748
  dispose() {
23714
23749
  this.feeder?.dispose();
23715
23750
  this.feeder = null;
@@ -31369,8 +31404,10 @@ var ADAPTIVE_CONFIG = {
31369
31404
  analyzeWindowMs: 15e3,
31370
31405
  minFailPercent: .05,
31371
31406
  waitAfterResetMs: 8e3,
31372
- upgradeHealthyMs: 6e4,
31373
- minTimeBetweenChangesMs: 6e4,
31407
+ upgradeHealthyMs: 4e3,
31408
+ upgradeHealthyGrowthFactor: 3,
31409
+ maxProgressiveUpgradeHealthyMs: 3e4,
31410
+ minTimeBetweenChangesMs: 4e3,
31374
31411
  maxUpgradeBackoffMs: 9e5
31375
31412
  };
31376
31413
  /**
@@ -31538,8 +31575,7 @@ function scoreBroker(tier, broker, hints) {
31538
31575
  const fallback = tier ? LABEL_DEFAULTS[tier] : void 0;
31539
31576
  const bitrateKbps = stats.bitrateKbps > 0 ? stats.bitrateKbps : fallback?.bitrateKbps ?? 2e3;
31540
31577
  const srcPixels = fallback?.pixels ?? 1920 * 1080;
31541
- const dpr = hints.devicePixelRatio ?? 1;
31542
- const targetPixels = (hints.viewportWidth ?? 1920) * dpr * ((hints.viewportHeight ?? 1080) * dpr);
31578
+ const targetPixels = (hints.viewportWidth ?? 1920) * (hints.viewportHeight ?? 1080);
31543
31579
  const pixelDistance = Math.abs(srcPixels - targetPixels) / 1e6;
31544
31580
  let bandwidthPenalty = 0;
31545
31581
  if (hints.downlinkMbps && hints.downlinkMbps > 0 && bitrateKbps > 0) {
@@ -32284,6 +32320,7 @@ var BrokerWebrtcServer = class {
32284
32320
  }
32285
32321
  });
32286
32322
  const scrubCache = new ScrubFragmentCache();
32323
+ const initialLiveProfile = this.profileTierResolver?.(brokerId) ?? null;
32287
32324
  const readGopAccessor = this.readRecordedGopBytes;
32288
32325
  const readGop = readGopAccessor ? async (args) => {
32289
32326
  const res = await readGopAccessor({
@@ -32303,6 +32340,7 @@ var BrokerWebrtcServer = class {
32303
32340
  timeline = new TimelineSession({
32304
32341
  session,
32305
32342
  deviceId,
32343
+ ...initialLiveProfile !== null ? { initialLiveProfile } : {},
32306
32344
  warm: readGop ? (profile, epochMs) => {
32307
32345
  warmFragment({
32308
32346
  deviceId,
@@ -32437,7 +32475,7 @@ var BrokerWebrtcServer = class {
32437
32475
  };
32438
32476
  if (streamId.endsWith("/adaptive")) {
32439
32477
  const initialTier = this.profileTierResolver?.(brokerId) ?? "high";
32440
- this.startAdaptive(entry, deviceId, initialTier, sessionId, intent?.failures);
32478
+ this.startAdaptive(entry, deviceId, initialTier, sessionId, intent?.failures, intent?.upgradeCount);
32441
32479
  }
32442
32480
  return {
32443
32481
  sessionId,
@@ -32531,17 +32569,18 @@ var BrokerWebrtcServer = class {
32531
32569
  * server.
32532
32570
  *
32533
32571
  * The client-loss % is turned into a monotonic {@link LossSample} LOCALLY:
32534
- * a per-controller counter `clientLossN` advances each tick that carries a
32535
- * positive loss %, so consecutive samples have a clean delta whose ratio
32536
- * equals `pct/100` exactly what the controller's `LossTracker` expects.
32572
+ * per-controller cumulative sequence/loss counters advance for every
32573
+ * reported interval (including 0% loss), so a perfectly healthy LAN is real
32574
+ * evidence rather than silence, exactly like RTCP's cumulative fields.
32537
32575
  * Both paths feed the same controller; the tracker naturally honours
32538
32576
  * whichever produces the higher loss ratio across its window. The async
32539
32577
  * poll is guarded against overlapping ticks via `tickInFlight`.
32540
32578
  */
32541
- startAdaptive(entry, deviceId, initialTier, sessionId, priorFailures) {
32579
+ startAdaptive(entry, deviceId, initialTier, sessionId, priorFailures, priorUpgradeCount) {
32542
32580
  const controller = new AdaptiveController({
32543
32581
  initialTier,
32544
32582
  priorFailures,
32583
+ priorUpgradeCount,
32545
32584
  config: ADAPTIVE_CONFIG,
32546
32585
  onSwitchTier: (tier) => {
32547
32586
  this.switchSessionTier(entry, deviceId, tier, sessionId);
@@ -32553,7 +32592,8 @@ var BrokerWebrtcServer = class {
32553
32592
  });
32554
32593
  entry.session.onReceiverReport((s) => controller.onLoss(s, Date.now()));
32555
32594
  entry.adaptiveController = controller;
32556
- let clientLossN = 0;
32595
+ let clientHighestSequence = 0;
32596
+ let clientPacketsLost = 0;
32557
32597
  let tickInFlight = false;
32558
32598
  const CLIENT_LOSS_SPAN = 1e3;
32559
32599
  entry.adaptiveTimer = setInterval(() => {
@@ -32567,11 +32607,12 @@ var BrokerWebrtcServer = class {
32567
32607
  }
32568
32608
  const pct = this.clientLossPct ? await this.clientLossPct(deviceId) : null;
32569
32609
  const now = Date.now();
32570
- if (pct !== null && pct !== void 0 && pct > 0) {
32571
- clientLossN += 1;
32610
+ if (pct !== null && pct !== void 0) {
32611
+ clientHighestSequence += CLIENT_LOSS_SPAN;
32612
+ clientPacketsLost += Math.round(Math.max(0, Math.min(100, pct)) / 100 * CLIENT_LOSS_SPAN);
32572
32613
  controller.onLoss({
32573
- packetsLost: Math.round(pct / 100 * CLIENT_LOSS_SPAN * clientLossN),
32574
- highestSequence: CLIENT_LOSS_SPAN * clientLossN
32614
+ packetsLost: clientPacketsLost,
32615
+ highestSequence: clientHighestSequence
32575
32616
  }, now);
32576
32617
  }
32577
32618
  controller.tick(now);
@@ -32597,10 +32638,12 @@ var BrokerWebrtcServer = class {
32597
32638
  switchSessionTier(entry, deviceId, tier, sessionId, forceTranscodeDown = false) {
32598
32639
  const prev = this.adaptiveIntent.get(sessionId);
32599
32640
  const failures = new Map(entry.adaptiveController?.failureCounts ?? prev?.failures);
32641
+ const upgradeCount = entry.adaptiveController?.successfulUpgradeCount ?? prev?.upgradeCount ?? 0;
32600
32642
  this.adaptiveIntent.set(sessionId, {
32601
32643
  prefersTier: tier,
32602
32644
  forceTranscodeDown: forceTranscodeDown || (prev?.forceTranscodeDown ?? false),
32603
- failures
32645
+ failures,
32646
+ upgradeCount
32604
32647
  });
32605
32648
  const epoch = (entry.session.pendingRenegotiation?.epoch ?? 0) + 1;
32606
32649
  entry.session.pendingRenegotiation = {
@@ -32615,7 +32658,8 @@ var BrokerWebrtcServer = class {
32615
32658
  tier,
32616
32659
  forceTranscodeDown,
32617
32660
  epoch,
32618
- failures: Object.fromEntries(failures)
32661
+ failures: Object.fromEntries(failures),
32662
+ upgradeCount
32619
32663
  }
32620
32664
  });
32621
32665
  }
@@ -21546,11 +21546,13 @@ var AdaptiveController = class {
21546
21546
  */
21547
21547
  armed = false;
21548
21548
  failures;
21549
+ upgradeCount;
21549
21550
  constructor(opts) {
21550
21551
  this.opts = opts;
21551
21552
  this.tracker = new LossTracker(opts.config);
21552
21553
  this.tier = opts.initialTier;
21553
21554
  this.failures = new Map(opts.priorFailures ?? []);
21555
+ this.upgradeCount = Math.max(0, opts.priorUpgradeCount ?? 0);
21554
21556
  }
21555
21557
  get currentTier() {
21556
21558
  return this.tier;
@@ -21563,6 +21565,9 @@ var AdaptiveController = class {
21563
21565
  get failureCounts() {
21564
21566
  return this.failures;
21565
21567
  }
21568
+ get successfulUpgradeCount() {
21569
+ return this.upgradeCount;
21570
+ }
21566
21571
  onLoss(sample, now) {
21567
21572
  this.tracker.track(sample, now);
21568
21573
  }
@@ -21615,8 +21620,9 @@ var AdaptiveController = class {
21615
21620
  */
21616
21621
  requiredHealthyMs(tier) {
21617
21622
  const failures = this.failures.get(tier) ?? 0;
21618
- if (failures <= 0) return this.opts.config.upgradeHealthyMs;
21619
- return Math.min(this.opts.config.upgradeHealthyMs * 2 ** failures, this.opts.config.maxUpgradeBackoffMs);
21623
+ const progressiveMs = Math.min(this.opts.config.upgradeHealthyMs * this.opts.config.upgradeHealthyGrowthFactor ** this.upgradeCount, this.opts.config.maxProgressiveUpgradeHealthyMs);
21624
+ if (failures <= 0) return progressiveMs;
21625
+ return Math.min(progressiveMs * 2 ** failures, this.opts.config.maxUpgradeBackoffMs);
21620
21626
  }
21621
21627
  commitChange(now) {
21622
21628
  this.lastChangeAt = now;
@@ -21653,6 +21659,7 @@ var AdaptiveController = class {
21653
21659
  this.tier = next;
21654
21660
  this.mtuReduced = false;
21655
21661
  this.transcodeEngaged = false;
21662
+ this.upgradeCount += 1;
21656
21663
  this.opts.onSwitchTier(next);
21657
21664
  this.commitChange(now);
21658
21665
  this.healthySince = now;
@@ -21712,21 +21719,32 @@ function clampPlaybackRate(rate) {
21712
21719
  if (!Number.isFinite(rate) || rate <= 0) return 0;
21713
21720
  return Math.min(4, Math.max(.25, rate));
21714
21721
  }
21715
- var ServerMessageSchema = discriminatedUnion("t", [object({
21716
- t: literal("state"),
21717
- state: _enum([
21718
- "live",
21719
- "loading",
21720
- "playing",
21721
- "paused",
21722
- "gap",
21723
- "ended",
21724
- "error"
21725
- ])
21726
- }), object({
21727
- t: literal("position"),
21728
- epochMs: number()
21729
- })]);
21722
+ var ServerMessageSchema = discriminatedUnion("t", [
21723
+ object({
21724
+ t: literal("state"),
21725
+ state: _enum([
21726
+ "live",
21727
+ "loading",
21728
+ "playing",
21729
+ "paused",
21730
+ "gap",
21731
+ "ended",
21732
+ "error"
21733
+ ])
21734
+ }),
21735
+ object({
21736
+ t: literal("position"),
21737
+ epochMs: number()
21738
+ }),
21739
+ object({
21740
+ t: literal("liveProfile"),
21741
+ profile: _enum([
21742
+ "high",
21743
+ "mid",
21744
+ "low"
21745
+ ])
21746
+ })
21747
+ ]);
21730
21748
  function parseControlMessage(raw) {
21731
21749
  try {
21732
21750
  const parsed = ControlMessageSchema.safeParse(JSON.parse(raw));
@@ -22065,7 +22083,7 @@ var SEEK_DISCONTINUITY_MS = 250;
22065
22083
  * sustainable there — cross-fragment moves remain paced by the demux + the
22066
22084
  * one-in-flight guard, so this cannot spawn parallel demuxes.
22067
22085
  */
22068
- var SCRUB_COALESCE_MS = 60;
22086
+ var SCRUB_COALESCE_MS = 33;
22069
22087
  /**
22070
22088
  * How much forward slack two fragments may have and still count as ADJACENT
22071
22089
  * for sweep continuity. Two real cases live inside it: the ±1 ms rounding of a
@@ -23602,6 +23620,12 @@ var TimelineSession = class {
23602
23620
  mode = "live";
23603
23621
  feeder = null;
23604
23622
  profile = "mid";
23623
+ /** Transport rate belongs to the session, not to one profile-specific feeder.
23624
+ * Scrub pins `low`; release restores the selected playback profile and may
23625
+ * therefore rebuild the feeder. Carrying the rate keeps a paused scrub
23626
+ * paused across that required quality transition. */
23627
+ rate = 1;
23628
+ liveProfileReported = false;
23605
23629
  /**
23606
23630
  * The last position the viewer was SHOWN (every `_emitPosition`). Handed to
23607
23631
  * `seek` as `travelFromMs` so the seek travel animation works across feeder
@@ -23625,6 +23649,7 @@ var TimelineSession = class {
23625
23649
  return this.mode;
23626
23650
  }
23627
23651
  async handleControl(msg) {
23652
+ this.emitLiveProfile();
23628
23653
  switch (msg.t) {
23629
23654
  case "playRecorded": {
23630
23655
  const profileChanged = msg.profile !== this.profile;
@@ -23653,6 +23678,7 @@ var TimelineSession = class {
23653
23678
  if (this.mode === "recorded") this.feeder?.scrubAck();
23654
23679
  break;
23655
23680
  case "setRate":
23681
+ this.rate = msg.rate;
23656
23682
  this.feeder?.setRate(msg.rate);
23657
23683
  break;
23658
23684
  case "stepFrame":
@@ -23677,6 +23703,7 @@ var TimelineSession = class {
23677
23703
  this.deps.session.setLiveFeedGate(true);
23678
23704
  this.mode = "recorded";
23679
23705
  const feeder = this.deps.makeFeeder(this.profile);
23706
+ if (this.rate !== 1) feeder.setRate(this.rate);
23680
23707
  this.feeder = feeder;
23681
23708
  return feeder;
23682
23709
  }
@@ -23704,6 +23731,14 @@ var TimelineSession = class {
23704
23731
  epochMs
23705
23732
  }));
23706
23733
  }
23734
+ emitLiveProfile() {
23735
+ if (this.liveProfileReported || this.deps.initialLiveProfile === void 0) return;
23736
+ this.liveProfileReported = true;
23737
+ this.deps.session.sendControl(serializeServerMessage({
23738
+ t: "liveProfile",
23739
+ profile: this.deps.initialLiveProfile
23740
+ }));
23741
+ }
23707
23742
  dispose() {
23708
23743
  this.feeder?.dispose();
23709
23744
  this.feeder = null;
@@ -31360,8 +31395,10 @@ var ADAPTIVE_CONFIG = {
31360
31395
  analyzeWindowMs: 15e3,
31361
31396
  minFailPercent: .05,
31362
31397
  waitAfterResetMs: 8e3,
31363
- upgradeHealthyMs: 6e4,
31364
- minTimeBetweenChangesMs: 6e4,
31398
+ upgradeHealthyMs: 4e3,
31399
+ upgradeHealthyGrowthFactor: 3,
31400
+ maxProgressiveUpgradeHealthyMs: 3e4,
31401
+ minTimeBetweenChangesMs: 4e3,
31365
31402
  maxUpgradeBackoffMs: 9e5
31366
31403
  };
31367
31404
  /**
@@ -31529,8 +31566,7 @@ function scoreBroker(tier, broker, hints) {
31529
31566
  const fallback = tier ? LABEL_DEFAULTS[tier] : void 0;
31530
31567
  const bitrateKbps = stats.bitrateKbps > 0 ? stats.bitrateKbps : fallback?.bitrateKbps ?? 2e3;
31531
31568
  const srcPixels = fallback?.pixels ?? 1920 * 1080;
31532
- const dpr = hints.devicePixelRatio ?? 1;
31533
- const targetPixels = (hints.viewportWidth ?? 1920) * dpr * ((hints.viewportHeight ?? 1080) * dpr);
31569
+ const targetPixels = (hints.viewportWidth ?? 1920) * (hints.viewportHeight ?? 1080);
31534
31570
  const pixelDistance = Math.abs(srcPixels - targetPixels) / 1e6;
31535
31571
  let bandwidthPenalty = 0;
31536
31572
  if (hints.downlinkMbps && hints.downlinkMbps > 0 && bitrateKbps > 0) {
@@ -32275,6 +32311,7 @@ var BrokerWebrtcServer = class {
32275
32311
  }
32276
32312
  });
32277
32313
  const scrubCache = new ScrubFragmentCache();
32314
+ const initialLiveProfile = this.profileTierResolver?.(brokerId) ?? null;
32278
32315
  const readGopAccessor = this.readRecordedGopBytes;
32279
32316
  const readGop = readGopAccessor ? async (args) => {
32280
32317
  const res = await readGopAccessor({
@@ -32294,6 +32331,7 @@ var BrokerWebrtcServer = class {
32294
32331
  timeline = new TimelineSession({
32295
32332
  session,
32296
32333
  deviceId,
32334
+ ...initialLiveProfile !== null ? { initialLiveProfile } : {},
32297
32335
  warm: readGop ? (profile, epochMs) => {
32298
32336
  warmFragment({
32299
32337
  deviceId,
@@ -32428,7 +32466,7 @@ var BrokerWebrtcServer = class {
32428
32466
  };
32429
32467
  if (streamId.endsWith("/adaptive")) {
32430
32468
  const initialTier = this.profileTierResolver?.(brokerId) ?? "high";
32431
- this.startAdaptive(entry, deviceId, initialTier, sessionId, intent?.failures);
32469
+ this.startAdaptive(entry, deviceId, initialTier, sessionId, intent?.failures, intent?.upgradeCount);
32432
32470
  }
32433
32471
  return {
32434
32472
  sessionId,
@@ -32522,17 +32560,18 @@ var BrokerWebrtcServer = class {
32522
32560
  * server.
32523
32561
  *
32524
32562
  * The client-loss % is turned into a monotonic {@link LossSample} LOCALLY:
32525
- * a per-controller counter `clientLossN` advances each tick that carries a
32526
- * positive loss %, so consecutive samples have a clean delta whose ratio
32527
- * equals `pct/100` exactly what the controller's `LossTracker` expects.
32563
+ * per-controller cumulative sequence/loss counters advance for every
32564
+ * reported interval (including 0% loss), so a perfectly healthy LAN is real
32565
+ * evidence rather than silence, exactly like RTCP's cumulative fields.
32528
32566
  * Both paths feed the same controller; the tracker naturally honours
32529
32567
  * whichever produces the higher loss ratio across its window. The async
32530
32568
  * poll is guarded against overlapping ticks via `tickInFlight`.
32531
32569
  */
32532
- startAdaptive(entry, deviceId, initialTier, sessionId, priorFailures) {
32570
+ startAdaptive(entry, deviceId, initialTier, sessionId, priorFailures, priorUpgradeCount) {
32533
32571
  const controller = new AdaptiveController({
32534
32572
  initialTier,
32535
32573
  priorFailures,
32574
+ priorUpgradeCount,
32536
32575
  config: ADAPTIVE_CONFIG,
32537
32576
  onSwitchTier: (tier) => {
32538
32577
  this.switchSessionTier(entry, deviceId, tier, sessionId);
@@ -32544,7 +32583,8 @@ var BrokerWebrtcServer = class {
32544
32583
  });
32545
32584
  entry.session.onReceiverReport((s) => controller.onLoss(s, Date.now()));
32546
32585
  entry.adaptiveController = controller;
32547
- let clientLossN = 0;
32586
+ let clientHighestSequence = 0;
32587
+ let clientPacketsLost = 0;
32548
32588
  let tickInFlight = false;
32549
32589
  const CLIENT_LOSS_SPAN = 1e3;
32550
32590
  entry.adaptiveTimer = setInterval(() => {
@@ -32558,11 +32598,12 @@ var BrokerWebrtcServer = class {
32558
32598
  }
32559
32599
  const pct = this.clientLossPct ? await this.clientLossPct(deviceId) : null;
32560
32600
  const now = Date.now();
32561
- if (pct !== null && pct !== void 0 && pct > 0) {
32562
- clientLossN += 1;
32601
+ if (pct !== null && pct !== void 0) {
32602
+ clientHighestSequence += CLIENT_LOSS_SPAN;
32603
+ clientPacketsLost += Math.round(Math.max(0, Math.min(100, pct)) / 100 * CLIENT_LOSS_SPAN);
32563
32604
  controller.onLoss({
32564
- packetsLost: Math.round(pct / 100 * CLIENT_LOSS_SPAN * clientLossN),
32565
- highestSequence: CLIENT_LOSS_SPAN * clientLossN
32605
+ packetsLost: clientPacketsLost,
32606
+ highestSequence: clientHighestSequence
32566
32607
  }, now);
32567
32608
  }
32568
32609
  controller.tick(now);
@@ -32588,10 +32629,12 @@ var BrokerWebrtcServer = class {
32588
32629
  switchSessionTier(entry, deviceId, tier, sessionId, forceTranscodeDown = false) {
32589
32630
  const prev = this.adaptiveIntent.get(sessionId);
32590
32631
  const failures = new Map(entry.adaptiveController?.failureCounts ?? prev?.failures);
32632
+ const upgradeCount = entry.adaptiveController?.successfulUpgradeCount ?? prev?.upgradeCount ?? 0;
32591
32633
  this.adaptiveIntent.set(sessionId, {
32592
32634
  prefersTier: tier,
32593
32635
  forceTranscodeDown: forceTranscodeDown || (prev?.forceTranscodeDown ?? false),
32594
- failures
32636
+ failures,
32637
+ upgradeCount
32595
32638
  });
32596
32639
  const epoch = (entry.session.pendingRenegotiation?.epoch ?? 0) + 1;
32597
32640
  entry.session.pendingRenegotiation = {
@@ -32606,7 +32649,8 @@ var BrokerWebrtcServer = class {
32606
32649
  tier,
32607
32650
  forceTranscodeDown,
32608
32651
  epoch,
32609
- failures: Object.fromEntries(failures)
32652
+ failures: Object.fromEntries(failures),
32653
+ upgradeCount
32610
32654
  }
32611
32655
  });
32612
32656
  }
@@ -1,4 +1,4 @@
1
- import{d as e,f as t,g as n,h as r,m as i,p as a}from"./index-hICPL_hd.js";var o=n(t()),s=n(e(),1),c=Math.PI/180;function l(){return typeof window<`u`&&({}.toString.call(window)===`[object Window]`||{}.toString.call(window)===`[object global]`)}var u=typeof global<`u`?global:typeof window<`u`?window:typeof WorkerGlobalScope<`u`?self:{},d={_global:u,version:`10.3.0`,isBrowser:l(),isUnminified:/param/.test(function(e){}.toString()),dblClickWindow:400,getAngle(e){return d.angleDeg?e*c:e},enableTrace:!1,pointerEventsEnabled:!0,autoDrawEnabled:!0,hitOnDragEnabled:!1,capturePointerEventsEnabled:!1,_mouseListenClick:!1,_touchListenClick:!1,_pointerListenClick:!1,_mouseInDblClickWindow:!1,_touchInDblClickWindow:!1,_pointerInDblClickWindow:!1,_mouseDblClickPointerId:null,_touchDblClickPointerId:null,_pointerDblClickPointerId:null,_renderBackend:`web`,legacyTextRendering:!1,pixelRatio:typeof window<`u`&&window.devicePixelRatio||1,dragDistance:3,angleDeg:!0,showWarnings:!0,dragButtons:[0,1],isDragging(){return d.DD.isDragging},isTransforming(){return d.Transformer?.isTransforming()??!1},isDragReady(){return!!d.DD.node},releaseCanvasOnDestroy:!0,document:u.document,_injectGlobal(e){u.Konva!==void 0&&console.error(`Several Konva instances detected. It is not recommended to use multiple Konva instances in the same environment.`),u.Konva=e}},f=e=>{d[e.prototype.getClassName()]=e};d._injectGlobal(d);var p=`Konva.js unsupported environment.
1
+ import{d as e,f as t,g as n,h as r,m as i,p as a}from"./index-Dcfe-5Ig.js";var o=n(t()),s=n(e(),1),c=Math.PI/180;function l(){return typeof window<`u`&&({}.toString.call(window)===`[object Window]`||{}.toString.call(window)===`[object global]`)}var u=typeof global<`u`?global:typeof window<`u`?window:typeof WorkerGlobalScope<`u`?self:{},d={_global:u,version:`10.3.0`,isBrowser:l(),isUnminified:/param/.test(function(e){}.toString()),dblClickWindow:400,getAngle(e){return d.angleDeg?e*c:e},enableTrace:!1,pointerEventsEnabled:!0,autoDrawEnabled:!0,hitOnDragEnabled:!1,capturePointerEventsEnabled:!1,_mouseListenClick:!1,_touchListenClick:!1,_pointerListenClick:!1,_mouseInDblClickWindow:!1,_touchInDblClickWindow:!1,_pointerInDblClickWindow:!1,_mouseDblClickPointerId:null,_touchDblClickPointerId:null,_pointerDblClickPointerId:null,_renderBackend:`web`,legacyTextRendering:!1,pixelRatio:typeof window<`u`&&window.devicePixelRatio||1,dragDistance:3,angleDeg:!0,showWarnings:!0,dragButtons:[0,1],isDragging(){return d.DD.isDragging},isTransforming(){return d.Transformer?.isTransforming()??!1},isDragReady(){return!!d.DD.node},releaseCanvasOnDestroy:!0,document:u.document,_injectGlobal(e){u.Konva!==void 0&&console.error(`Several Konva instances detected. It is not recommended to use multiple Konva instances in the same environment.`),u.Konva=e}},f=e=>{d[e.prototype.getClassName()]=e};d._injectGlobal(d);var p=`Konva.js unsupported environment.
2
2
 
3
3
  Looks like you are trying to use Konva.js in Node.js environment. because "document" object is undefined.
4
4
 
@@ -1 +1 @@
1
- import{a as e,c as t,d as n,f as r,g as i,l as a,o,r as s,s as c,u as l}from"./index-hICPL_hd.js";import{MaskShapeCanvas as u}from"./MaskShapeCanvas-DI4BY7W2-Bxm34WFr.js";var d=i(r(),1),f=i(n(),1),p=o(`grid-2x2`,[[`path`,{d:`M12 3v18`,key:`108xh3`}],[`path`,{d:`M3 12h18`,key:`1i2n21`}],[`rect`,{x:`3`,y:`3`,width:`18`,height:`18`,rx:`2`,key:`h1oib`}]]),m=110,h=`motion-zones`,g=0,_=[1,2,3],v=1,y=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,b=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`;function x(e,t,n){let r=t*n,i=Array.from({length:r});for(let t=0;t<r;t+=1)i[t]=e[t]===!0;return i}function S(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1)if(e[n]!==t[n])return!1;return!0}function C(e,t){return Math.ceil(e/t)}function w(e,t,n){return Math.min(n-1,Math.floor((e+.5)/t*n))}function T(e,t,n,r){let i=C(t,r),a=C(n,r),o=Array.from({length:i*a},()=>!1);for(let r=0;r<n;r+=1){let s=w(r,n,a);for(let n=0;n<t;n+=1)e[r*t+n]===!0&&(o[s*i+w(n,t,i)]=!0)}return o}function E(e,t,n,r){let i=C(t,r),a=C(n,r),o=Array.from({length:t*n},()=>!1);for(let r=0;r<n;r+=1){let s=w(r,n,a);for(let n=0;n<t;n+=1)o[r*t+n]=e[s*i+w(n,t,i)]===!0}return o}function D({deviceId:n}){let r=t(l().trpcClient,n),[i,o]=(0,d.useState)(null),[w,D]=(0,d.useState)(!1),[O,k]=(0,d.useState)(null),[A,j]=(0,d.useState)(null),[M,N]=(0,d.useState)(v),[P,F]=(0,d.useState)(!1),[I,L]=(0,d.useState)(!1),R=(0,d.useRef)(!1);(0,d.useEffect)(()=>{if(!r)return;let e=!1;return R.current=!1,o(null),D(!1),k(null),j(null),N(v),L(!1),(async()=>{try{let t=await r.motionZones?.getOptions({});if(e)return;if(!t)throw Error(`device proxy not ready`);if(o(t),R.current)return;let n=await r.motionZones?.getStatus({});if(e)return;if(!n)throw Error(`device proxy not ready`);R.current=!0;let i=n.regions.find(e=>e.shape.kind===`grid`),a=x(i?i.shape.cells:[],t.grid.width,t.grid.height);j(a),k(T(a,t.grid.width,t.grid.height,v))}catch(t){if(e)return;c(t)?D(!0):console.error(`Motion Zones load failed`,t)}})(),()=>{e=!0}},[r]);let z=i?i.grid.width:0,B=i?i.grid.height:0,V=C(z,M),H=C(B,M),U=(0,d.useMemo)(()=>O&&i?E(O,z,B,M):null,[O,i,z,B,M]),W=(0,d.useMemo)(()=>U!==null&&A!==null&&!S(U,A),[U,A]),G=(0,d.useMemo)(()=>U?U.reduce((e,t)=>t?e+1:e,0):0,[U]),K=z*B,q=V*H,J=(0,d.useCallback)(e=>{q>0&&k(Array.from({length:q},()=>e))},[q]),Y=(0,d.useCallback)(()=>{k(e=>e&&e.map(e=>!e))},[]),X=(0,d.useCallback)(()=>{A&&i&&k(T(A,z,B,M))},[A,i,z,B,M]),Z=(0,d.useCallback)(e=>{e===M||!i||k(t=>{if(!t)return N(e),t;let n=T(E(t,z,B,M),z,B,e);return N(e),n})},[M,i,z,B]),Q=(0,d.useMemo)(()=>i&&O?[{id:g,shape:{kind:`grid`,gridWidth:V,gridHeight:H,cells:[...O]}}]:[],[i,O,V,H]),$=(0,d.useCallback)((e,t)=>{t.kind===`grid`&&k(t.cells)},[]),ee=(0,d.useCallback)(async()=>{if(!(!r||!O||!i)){F(!0);try{let e=E(O,i.grid.width,i.grid.height,M),t={kind:`grid`,gridWidth:i.grid.width,gridHeight:i.grid.height,cells:e};await r.motionZones?.setZone({patch:{regions:[{id:g,enabled:!0,shape:t}]}});let n=await r.motionZones?.getStatus({});if(n){let e=n.regions.find(e=>e.shape.kind===`grid`),t=x(e?e.shape.cells:[],i.grid.width,i.grid.height);j(t),k(T(t,i.grid.width,i.grid.height,M))}}catch(e){console.error(`Motion Zones save failed`,e)}finally{F(!1)}}},[r,O,i,M]);a((0,d.useMemo)(()=>I&&!w&&i&&O?{id:h,order:m,node:(0,f.jsx)(u,{transparent:!0,items:Q,supportedShapes:[`grid`],grid:{width:V,height:H},selectedId:g,onSelect:()=>{},onShapeChange:$,onDrawComplete:()=>{},drawingKind:null})}:null,[I,w,i,O,Q,$,V,H]));let te=!w&&i!==null&&O!==null;return r?(0,f.jsx)(e,{title:`Motion Zones`,icon:(0,f.jsx)(p,{className:`h-3.5 w-3.5 text-foreground-subtle`}),children:(0,f.jsx)(`div`,{className:`flex flex-col gap-3`,children:w?(0,f.jsx)(`p`,{className:`${s} leading-relaxed`,children:`This camera doesn't expose an on-board motion zones grid.`}):te?(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(`p`,{className:`${s} leading-relaxed`,children:[`Toggle `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Edit grid`}),` to paint the region directly on the live frame, then `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Save`}),` to push the mask to the camera. Pick a bigger`,` `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Cell size`}),` for quicker, broad-stroke painting — it's resampled to the camera's native `,i.grid.width,`×`,i.grid.height,` grid on save (×1 is the finest).`]}),(0,f.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[(0,f.jsx)(`button`,{type:`button`,onClick:()=>L(e=>!e),disabled:P,"aria-pressed":I,className:I?b:y,children:I?`Done editing`:`Edit grid`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>J(!0),disabled:P,className:y,children:`All on`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>J(!1),disabled:P,className:y,children:`All off`}),(0,f.jsx)(`button`,{type:`button`,onClick:Y,disabled:P,className:y,children:`Invert`}),(0,f.jsxs)(`div`,{className:`flex items-center gap-1 ml-1`,role:`group`,"aria-label":`Cell size`,children:[(0,f.jsx)(`span`,{className:`${s} mr-0.5`,children:`Cell size`}),_.map(e=>(0,f.jsxs)(`button`,{type:`button`,onClick:()=>Z(e),disabled:P,"aria-pressed":M===e,title:e===1?`Camera grid ${z}×${B} (finest)`:`${C(z,e)}×${C(B,e)} painting grid · cells ×${e} bigger`,className:M===e?b:y,children:[`×`,e]},e))]}),(0,f.jsxs)(`span`,{className:`${s} ml-1 tabular-nums`,children:[G,` / `,K,` cells · `,i.grid.width,`×`,i.grid.height,M===1?``:` · paint ${V}×${H}`]}),(0,f.jsx)(`span`,{className:`flex-1`}),(0,f.jsx)(`button`,{type:`button`,onClick:X,disabled:P||!W,className:`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,children:`Revert`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>void ee(),disabled:P||!W,className:b,children:P?`Saving…`:`Save`})]})]}):(0,f.jsx)(`p`,{className:`${s} leading-relaxed`,children:`Loading the camera's grid…`})})}):null}export{D as MotionZonesSettings};
1
+ import{a as e,c as t,d as n,f as r,g as i,l as a,o,r as s,s as c,u as l}from"./index-Dcfe-5Ig.js";import{MaskShapeCanvas as u}from"./MaskShapeCanvas-DI4BY7W2-BWAtF7b-.js";var d=i(r(),1),f=i(n(),1),p=o(`grid-2x2`,[[`path`,{d:`M12 3v18`,key:`108xh3`}],[`path`,{d:`M3 12h18`,key:`1i2n21`}],[`rect`,{x:`3`,y:`3`,width:`18`,height:`18`,rx:`2`,key:`h1oib`}]]),m=110,h=`motion-zones`,g=0,_=[1,2,3],v=1,y=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,b=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`;function x(e,t,n){let r=t*n,i=Array.from({length:r});for(let t=0;t<r;t+=1)i[t]=e[t]===!0;return i}function S(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1)if(e[n]!==t[n])return!1;return!0}function C(e,t){return Math.ceil(e/t)}function w(e,t,n){return Math.min(n-1,Math.floor((e+.5)/t*n))}function T(e,t,n,r){let i=C(t,r),a=C(n,r),o=Array.from({length:i*a},()=>!1);for(let r=0;r<n;r+=1){let s=w(r,n,a);for(let n=0;n<t;n+=1)e[r*t+n]===!0&&(o[s*i+w(n,t,i)]=!0)}return o}function E(e,t,n,r){let i=C(t,r),a=C(n,r),o=Array.from({length:t*n},()=>!1);for(let r=0;r<n;r+=1){let s=w(r,n,a);for(let n=0;n<t;n+=1)o[r*t+n]=e[s*i+w(n,t,i)]===!0}return o}function D({deviceId:n}){let r=t(l().trpcClient,n),[i,o]=(0,d.useState)(null),[w,D]=(0,d.useState)(!1),[O,k]=(0,d.useState)(null),[A,j]=(0,d.useState)(null),[M,N]=(0,d.useState)(v),[P,F]=(0,d.useState)(!1),[I,L]=(0,d.useState)(!1),R=(0,d.useRef)(!1);(0,d.useEffect)(()=>{if(!r)return;let e=!1;return R.current=!1,o(null),D(!1),k(null),j(null),N(v),L(!1),(async()=>{try{let t=await r.motionZones?.getOptions({});if(e)return;if(!t)throw Error(`device proxy not ready`);if(o(t),R.current)return;let n=await r.motionZones?.getStatus({});if(e)return;if(!n)throw Error(`device proxy not ready`);R.current=!0;let i=n.regions.find(e=>e.shape.kind===`grid`),a=x(i?i.shape.cells:[],t.grid.width,t.grid.height);j(a),k(T(a,t.grid.width,t.grid.height,v))}catch(t){if(e)return;c(t)?D(!0):console.error(`Motion Zones load failed`,t)}})(),()=>{e=!0}},[r]);let z=i?i.grid.width:0,B=i?i.grid.height:0,V=C(z,M),H=C(B,M),U=(0,d.useMemo)(()=>O&&i?E(O,z,B,M):null,[O,i,z,B,M]),W=(0,d.useMemo)(()=>U!==null&&A!==null&&!S(U,A),[U,A]),G=(0,d.useMemo)(()=>U?U.reduce((e,t)=>t?e+1:e,0):0,[U]),K=z*B,q=V*H,J=(0,d.useCallback)(e=>{q>0&&k(Array.from({length:q},()=>e))},[q]),Y=(0,d.useCallback)(()=>{k(e=>e&&e.map(e=>!e))},[]),X=(0,d.useCallback)(()=>{A&&i&&k(T(A,z,B,M))},[A,i,z,B,M]),Z=(0,d.useCallback)(e=>{e===M||!i||k(t=>{if(!t)return N(e),t;let n=T(E(t,z,B,M),z,B,e);return N(e),n})},[M,i,z,B]),Q=(0,d.useMemo)(()=>i&&O?[{id:g,shape:{kind:`grid`,gridWidth:V,gridHeight:H,cells:[...O]}}]:[],[i,O,V,H]),$=(0,d.useCallback)((e,t)=>{t.kind===`grid`&&k(t.cells)},[]),ee=(0,d.useCallback)(async()=>{if(!(!r||!O||!i)){F(!0);try{let e=E(O,i.grid.width,i.grid.height,M),t={kind:`grid`,gridWidth:i.grid.width,gridHeight:i.grid.height,cells:e};await r.motionZones?.setZone({patch:{regions:[{id:g,enabled:!0,shape:t}]}});let n=await r.motionZones?.getStatus({});if(n){let e=n.regions.find(e=>e.shape.kind===`grid`),t=x(e?e.shape.cells:[],i.grid.width,i.grid.height);j(t),k(T(t,i.grid.width,i.grid.height,M))}}catch(e){console.error(`Motion Zones save failed`,e)}finally{F(!1)}}},[r,O,i,M]);a((0,d.useMemo)(()=>I&&!w&&i&&O?{id:h,order:m,node:(0,f.jsx)(u,{transparent:!0,items:Q,supportedShapes:[`grid`],grid:{width:V,height:H},selectedId:g,onSelect:()=>{},onShapeChange:$,onDrawComplete:()=>{},drawingKind:null})}:null,[I,w,i,O,Q,$,V,H]));let te=!w&&i!==null&&O!==null;return r?(0,f.jsx)(e,{title:`Motion Zones`,icon:(0,f.jsx)(p,{className:`h-3.5 w-3.5 text-foreground-subtle`}),children:(0,f.jsx)(`div`,{className:`flex flex-col gap-3`,children:w?(0,f.jsx)(`p`,{className:`${s} leading-relaxed`,children:`This camera doesn't expose an on-board motion zones grid.`}):te?(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(`p`,{className:`${s} leading-relaxed`,children:[`Toggle `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Edit grid`}),` to paint the region directly on the live frame, then `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Save`}),` to push the mask to the camera. Pick a bigger`,` `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Cell size`}),` for quicker, broad-stroke painting — it's resampled to the camera's native `,i.grid.width,`×`,i.grid.height,` grid on save (×1 is the finest).`]}),(0,f.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[(0,f.jsx)(`button`,{type:`button`,onClick:()=>L(e=>!e),disabled:P,"aria-pressed":I,className:I?b:y,children:I?`Done editing`:`Edit grid`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>J(!0),disabled:P,className:y,children:`All on`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>J(!1),disabled:P,className:y,children:`All off`}),(0,f.jsx)(`button`,{type:`button`,onClick:Y,disabled:P,className:y,children:`Invert`}),(0,f.jsxs)(`div`,{className:`flex items-center gap-1 ml-1`,role:`group`,"aria-label":`Cell size`,children:[(0,f.jsx)(`span`,{className:`${s} mr-0.5`,children:`Cell size`}),_.map(e=>(0,f.jsxs)(`button`,{type:`button`,onClick:()=>Z(e),disabled:P,"aria-pressed":M===e,title:e===1?`Camera grid ${z}×${B} (finest)`:`${C(z,e)}×${C(B,e)} painting grid · cells ×${e} bigger`,className:M===e?b:y,children:[`×`,e]},e))]}),(0,f.jsxs)(`span`,{className:`${s} ml-1 tabular-nums`,children:[G,` / `,K,` cells · `,i.grid.width,`×`,i.grid.height,M===1?``:` · paint ${V}×${H}`]}),(0,f.jsx)(`span`,{className:`flex-1`}),(0,f.jsx)(`button`,{type:`button`,onClick:X,disabled:P||!W,className:`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,children:`Revert`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>void ee(),disabled:P||!W,className:b,children:P?`Saving…`:`Save`})]})]}):(0,f.jsx)(`p`,{className:`${s} leading-relaxed`,children:`Loading the camera's grid…`})})}):null}export{D as MotionZonesSettings};
@@ -1 +1 @@
1
- import{a as e,c as t,d as n,f as r,g as i,i as a,l as o,n as s,o as c,r as l,s as u,t as d,u as ee}from"./index-hICPL_hd.js";import{MaskShapeCanvas as te}from"./MaskShapeCanvas-DI4BY7W2-Bxm34WFr.js";var f=i(r(),1),p=i(n(),1),m=c(`hexagon`,[[`path`,{d:`M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z`,key:`yt0hxn`}]]),h=120,g=`privacy-mask`,_=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,v=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`;function y(e){return e.kind===`rect`||e.kind===`polygon`?e:null}function b(e){let t=new Set(e.map(e=>e.id)),n=0;for(;t.has(n);)n+=1;return n}function x(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1)if(JSON.stringify(e[n])!==JSON.stringify(t[n]))return!1;return!0}function S({deviceId:n}){let r=t(ee().trpcClient,n),[i,c]=(0,f.useState)(null),[S,C]=(0,f.useState)(!1),[w,T]=(0,f.useState)(null),[E,D]=(0,f.useState)(null),[O,k]=(0,f.useState)(!1),[A,j]=(0,f.useState)(!1),[M,N]=(0,f.useState)(null),[P,F]=(0,f.useState)(null),I=(0,f.useRef)(!1);(0,f.useEffect)(()=>{if(!r)return;let e=!1;return I.current=!1,c(null),C(!1),T(null),D(null),j(!1),N(null),F(null),(async()=>{try{let t=await r.privacyMask?.getOptions({});if(e)return;if(!t)throw Error(`device proxy not ready`);if(c(t),I.current)return;let n=await r.privacyMask?.getStatus({});if(e)return;if(!n)throw Error(`device proxy not ready`);I.current=!0;let i={enabled:n.enabled,regions:n.regions};D(i),T(i)}catch(t){if(e)return;u(t)?C(!0):console.error(`Privacy Mask load failed`,t)}})(),()=>{e=!0}},[r]);let L=(0,f.useMemo)(()=>w!==null&&E!==null&&(w.enabled!==E.enabled||!x(w.regions,E.regions)),[w,E]),R=w?w.regions.length:0,z=i?i.maxRegions:0,B=(0,f.useRef)(0);(0,f.useEffect)(()=>{B.current=z},[z]);let V=z>0&&R>=z,H=(0,f.useCallback)(()=>{T(e=>e&&{...e,enabled:!e.enabled})},[]),U=(0,f.useCallback)(e=>{N(typeof e==`number`?e:null)},[]),W=(0,f.useMemo)(()=>w?w.regions.map(e=>({id:e.id,shape:e.shape,enabled:e.enabled,label:`Zone ${String(e.id)}`})):[],[w]),G=(0,f.useCallback)((e,t)=>{let n=y(t);n&&T(t=>t&&{...t,regions:t.regions.map(t=>t.id===e?{...t,shape:n}:t)})},[]),K=(0,f.useCallback)(e=>{let t=y(e);t&&(F(null),T(e=>{if(!e)return e;let n=B.current;if(n>0&&e.regions.length>=n)return e;let r=b(e.regions),i={id:r,enabled:!0,shape:t};return N(r),{...e,regions:[...e.regions,i]}}))},[]),q=(0,f.useCallback)(e=>{j(!0),N(null),F(e)},[]),J=(0,f.useCallback)(e=>{j(!0),F(null),N(e)},[]),Y=(0,f.useCallback)(e=>{N(t=>t===e?null:t),T(t=>t&&{...t,regions:t.regions.filter(t=>t.id!==e)})},[]),X=(0,f.useCallback)(()=>{F(null),N(null),E&&T({enabled:E.enabled,regions:E.regions})},[E]),Z=(0,f.useCallback)(async()=>{if(!(!r||!w)){k(!0);try{await r.privacyMask?.setMask({patch:{enabled:w.enabled,regions:[...w.regions]}});let e=await r.privacyMask?.getStatus({});if(e){let t={enabled:e.enabled,regions:e.regions};D(t),T(t)}}catch(e){console.error(`Privacy Mask save failed`,e)}finally{k(!1)}}},[r,w]),ne=(0,f.useCallback)(()=>{j(e=>(e&&(F(null),N(null)),!e))},[]),Q=i?.supportedShapes??[];o((0,f.useMemo)(()=>A&&!S&&i&&w?{id:g,order:h,node:(0,p.jsx)(te,{transparent:!0,items:W,supportedShapes:Q,polygonVertices:i.polygonVertices,selectedId:M,onSelect:U,onShapeChange:G,onDrawComplete:K,drawingKind:P})}:null,[A,S,i,w,W,Q,M,U,G,K,P]));let re=i?.supportedShapes.includes(`rect`)??!1,ie=i?.supportedShapes.includes(`polygon`)??!1,$=i!==null&&(i.maxRegions<=0||i.supportedShapes.length===0),ae=!S&&!$&&i!==null&&w!==null;return r?(0,p.jsx)(e,{title:`Privacy Mask`,icon:(0,p.jsx)(d,{className:`h-3.5 w-3.5 text-foreground-subtle`}),children:(0,p.jsx)(`div`,{className:`flex flex-col gap-3`,children:S||$?(0,p.jsx)(`p`,{className:`${l} leading-relaxed`,children:`This camera doesn't support an on-board privacy mask.`}):ae?(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(`p`,{className:`${l} leading-relaxed`,children:[`Toggle `,(0,p.jsx)(`strong`,{className:`text-foreground`,children:`Edit mask`}),` to draw blanked-out zones on the live frame, then `,(0,p.jsx)(`strong`,{className:`text-foreground`,children:`Save`}),` to push them to the camera. Drag a rectangle to move, its corner to resize; drag polygon vertices, click an edge midpoint to add one, or right-click a vertex to remove it.`]}),(0,p.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[(0,p.jsx)(`button`,{type:`button`,onClick:ne,disabled:O,"aria-pressed":A,className:A?v:_,children:A?`Done editing`:`Edit mask`}),(0,p.jsx)(`button`,{type:`button`,onClick:H,disabled:O,"aria-pressed":w.enabled,className:w.enabled?v:_,children:w.enabled?`Mask on`:`Mask off`}),re&&(0,p.jsx)(`button`,{type:`button`,onClick:()=>q(`rect`),disabled:O||V,"aria-pressed":P===`rect`,className:P===`rect`?v:_,title:V?`Maximum zones reached`:`Add a rectangle zone`,children:`+ Rect`}),ie&&(0,p.jsx)(`button`,{type:`button`,onClick:()=>q(`polygon`),disabled:O||V,"aria-pressed":P===`polygon`,className:P===`polygon`?v:_,title:V?`Maximum zones reached`:`Add a polygon zone`,children:`+ Polygon`}),(0,p.jsxs)(`span`,{className:`${l} ml-1 tabular-nums`,children:[R,` / `,z,` zones`]}),(0,p.jsx)(`span`,{className:`flex-1`}),(0,p.jsx)(`button`,{type:`button`,onClick:X,disabled:O||!L,className:`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,children:`Revert`}),(0,p.jsx)(`button`,{type:`button`,onClick:()=>void Z(),disabled:O||!L,className:v,children:O?`Saving…`:`Save`})]}),R>0?(0,p.jsx)(`div`,{className:`flex flex-col gap-1`,children:w.regions.map(e=>{let t=M===e.id,n=e.shape.kind===`polygon`?m:s;return(0,p.jsxs)(`div`,{className:`flex items-center gap-2 rounded-md border px-2 py-1 transition-colors ${t?`border-primary/50 bg-primary/10`:`border-border bg-surface`}`,children:[(0,p.jsxs)(`button`,{type:`button`,onClick:()=>J(e.id),disabled:O,className:`flex flex-1 items-center gap-2 text-left text-[11px] font-medium text-foreground-subtle hover:text-foreground disabled:opacity-40 transition-colors`,children:[(0,p.jsx)(n,{className:`h-3.5 w-3.5 shrink-0`}),(0,p.jsxs)(`span`,{children:[`Zone `,e.id]}),(0,p.jsx)(`span`,{className:`text-foreground-faint capitalize`,children:e.shape.kind})]}),(0,p.jsx)(`button`,{type:`button`,onClick:()=>Y(e.id),disabled:O,"aria-label":`Delete zone ${String(e.id)}`,title:`Delete zone`,className:`inline-flex h-6 w-6 items-center justify-center rounded border border-border bg-surface text-foreground-subtle hover:border-red-400/40 hover:bg-red-500/10 hover:text-red-400 disabled:opacity-40 transition-colors`,children:(0,p.jsx)(a,{className:`h-3.5 w-3.5`})})]},e.id)})}):null]}):(0,p.jsx)(`p`,{className:`${l} leading-relaxed`,children:`Loading the camera's privacy mask…`})})}):null}export{S as PrivacyMaskSettings};
1
+ import{a as e,c as t,d as n,f as r,g as i,i as a,l as o,n as s,o as c,r as l,s as u,t as d,u as ee}from"./index-Dcfe-5Ig.js";import{MaskShapeCanvas as te}from"./MaskShapeCanvas-DI4BY7W2-BWAtF7b-.js";var f=i(r(),1),p=i(n(),1),m=c(`hexagon`,[[`path`,{d:`M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z`,key:`yt0hxn`}]]),h=120,g=`privacy-mask`,_=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,v=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`;function y(e){return e.kind===`rect`||e.kind===`polygon`?e:null}function b(e){let t=new Set(e.map(e=>e.id)),n=0;for(;t.has(n);)n+=1;return n}function x(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1)if(JSON.stringify(e[n])!==JSON.stringify(t[n]))return!1;return!0}function S({deviceId:n}){let r=t(ee().trpcClient,n),[i,c]=(0,f.useState)(null),[S,C]=(0,f.useState)(!1),[w,T]=(0,f.useState)(null),[E,D]=(0,f.useState)(null),[O,k]=(0,f.useState)(!1),[A,j]=(0,f.useState)(!1),[M,N]=(0,f.useState)(null),[P,F]=(0,f.useState)(null),I=(0,f.useRef)(!1);(0,f.useEffect)(()=>{if(!r)return;let e=!1;return I.current=!1,c(null),C(!1),T(null),D(null),j(!1),N(null),F(null),(async()=>{try{let t=await r.privacyMask?.getOptions({});if(e)return;if(!t)throw Error(`device proxy not ready`);if(c(t),I.current)return;let n=await r.privacyMask?.getStatus({});if(e)return;if(!n)throw Error(`device proxy not ready`);I.current=!0;let i={enabled:n.enabled,regions:n.regions};D(i),T(i)}catch(t){if(e)return;u(t)?C(!0):console.error(`Privacy Mask load failed`,t)}})(),()=>{e=!0}},[r]);let L=(0,f.useMemo)(()=>w!==null&&E!==null&&(w.enabled!==E.enabled||!x(w.regions,E.regions)),[w,E]),R=w?w.regions.length:0,z=i?i.maxRegions:0,B=(0,f.useRef)(0);(0,f.useEffect)(()=>{B.current=z},[z]);let V=z>0&&R>=z,H=(0,f.useCallback)(()=>{T(e=>e&&{...e,enabled:!e.enabled})},[]),U=(0,f.useCallback)(e=>{N(typeof e==`number`?e:null)},[]),W=(0,f.useMemo)(()=>w?w.regions.map(e=>({id:e.id,shape:e.shape,enabled:e.enabled,label:`Zone ${String(e.id)}`})):[],[w]),G=(0,f.useCallback)((e,t)=>{let n=y(t);n&&T(t=>t&&{...t,regions:t.regions.map(t=>t.id===e?{...t,shape:n}:t)})},[]),K=(0,f.useCallback)(e=>{let t=y(e);t&&(F(null),T(e=>{if(!e)return e;let n=B.current;if(n>0&&e.regions.length>=n)return e;let r=b(e.regions),i={id:r,enabled:!0,shape:t};return N(r),{...e,regions:[...e.regions,i]}}))},[]),q=(0,f.useCallback)(e=>{j(!0),N(null),F(e)},[]),J=(0,f.useCallback)(e=>{j(!0),F(null),N(e)},[]),Y=(0,f.useCallback)(e=>{N(t=>t===e?null:t),T(t=>t&&{...t,regions:t.regions.filter(t=>t.id!==e)})},[]),X=(0,f.useCallback)(()=>{F(null),N(null),E&&T({enabled:E.enabled,regions:E.regions})},[E]),Z=(0,f.useCallback)(async()=>{if(!(!r||!w)){k(!0);try{await r.privacyMask?.setMask({patch:{enabled:w.enabled,regions:[...w.regions]}});let e=await r.privacyMask?.getStatus({});if(e){let t={enabled:e.enabled,regions:e.regions};D(t),T(t)}}catch(e){console.error(`Privacy Mask save failed`,e)}finally{k(!1)}}},[r,w]),ne=(0,f.useCallback)(()=>{j(e=>(e&&(F(null),N(null)),!e))},[]),Q=i?.supportedShapes??[];o((0,f.useMemo)(()=>A&&!S&&i&&w?{id:g,order:h,node:(0,p.jsx)(te,{transparent:!0,items:W,supportedShapes:Q,polygonVertices:i.polygonVertices,selectedId:M,onSelect:U,onShapeChange:G,onDrawComplete:K,drawingKind:P})}:null,[A,S,i,w,W,Q,M,U,G,K,P]));let re=i?.supportedShapes.includes(`rect`)??!1,ie=i?.supportedShapes.includes(`polygon`)??!1,$=i!==null&&(i.maxRegions<=0||i.supportedShapes.length===0),ae=!S&&!$&&i!==null&&w!==null;return r?(0,p.jsx)(e,{title:`Privacy Mask`,icon:(0,p.jsx)(d,{className:`h-3.5 w-3.5 text-foreground-subtle`}),children:(0,p.jsx)(`div`,{className:`flex flex-col gap-3`,children:S||$?(0,p.jsx)(`p`,{className:`${l} leading-relaxed`,children:`This camera doesn't support an on-board privacy mask.`}):ae?(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(`p`,{className:`${l} leading-relaxed`,children:[`Toggle `,(0,p.jsx)(`strong`,{className:`text-foreground`,children:`Edit mask`}),` to draw blanked-out zones on the live frame, then `,(0,p.jsx)(`strong`,{className:`text-foreground`,children:`Save`}),` to push them to the camera. Drag a rectangle to move, its corner to resize; drag polygon vertices, click an edge midpoint to add one, or right-click a vertex to remove it.`]}),(0,p.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[(0,p.jsx)(`button`,{type:`button`,onClick:ne,disabled:O,"aria-pressed":A,className:A?v:_,children:A?`Done editing`:`Edit mask`}),(0,p.jsx)(`button`,{type:`button`,onClick:H,disabled:O,"aria-pressed":w.enabled,className:w.enabled?v:_,children:w.enabled?`Mask on`:`Mask off`}),re&&(0,p.jsx)(`button`,{type:`button`,onClick:()=>q(`rect`),disabled:O||V,"aria-pressed":P===`rect`,className:P===`rect`?v:_,title:V?`Maximum zones reached`:`Add a rectangle zone`,children:`+ Rect`}),ie&&(0,p.jsx)(`button`,{type:`button`,onClick:()=>q(`polygon`),disabled:O||V,"aria-pressed":P===`polygon`,className:P===`polygon`?v:_,title:V?`Maximum zones reached`:`Add a polygon zone`,children:`+ Polygon`}),(0,p.jsxs)(`span`,{className:`${l} ml-1 tabular-nums`,children:[R,` / `,z,` zones`]}),(0,p.jsx)(`span`,{className:`flex-1`}),(0,p.jsx)(`button`,{type:`button`,onClick:X,disabled:O||!L,className:`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,children:`Revert`}),(0,p.jsx)(`button`,{type:`button`,onClick:()=>void Z(),disabled:O||!L,className:v,children:O?`Saving…`:`Save`})]}),R>0?(0,p.jsx)(`div`,{className:`flex flex-col gap-1`,children:w.regions.map(e=>{let t=M===e.id,n=e.shape.kind===`polygon`?m:s;return(0,p.jsxs)(`div`,{className:`flex items-center gap-2 rounded-md border px-2 py-1 transition-colors ${t?`border-primary/50 bg-primary/10`:`border-border bg-surface`}`,children:[(0,p.jsxs)(`button`,{type:`button`,onClick:()=>J(e.id),disabled:O,className:`flex flex-1 items-center gap-2 text-left text-[11px] font-medium text-foreground-subtle hover:text-foreground disabled:opacity-40 transition-colors`,children:[(0,p.jsx)(n,{className:`h-3.5 w-3.5 shrink-0`}),(0,p.jsxs)(`span`,{children:[`Zone `,e.id]}),(0,p.jsx)(`span`,{className:`text-foreground-faint capitalize`,children:e.shape.kind})]}),(0,p.jsx)(`button`,{type:`button`,onClick:()=>Y(e.id),disabled:O,"aria-label":`Delete zone ${String(e.id)}`,title:`Delete zone`,className:`inline-flex h-6 w-6 items-center justify-center rounded border border-border bg-surface text-foreground-subtle hover:border-red-400/40 hover:bg-red-500/10 hover:text-red-400 disabled:opacity-40 transition-colors`,children:(0,p.jsx)(a,{className:`h-3.5 w-3.5`})})]},e.id)})}):null]}):(0,p.jsx)(`p`,{className:`${l} leading-relaxed`,children:`Loading the camera's privacy mask…`})})}):null}export{S as PrivacyMaskSettings};