@camstack/addon-pipeline 1.2.57 → 1.2.59

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.
@@ -1851,15 +1851,15 @@ async function readGopRange(file, args) {
1851
1851
  bytesRead += chunk.length;
1852
1852
  return chunk;
1853
1853
  };
1854
- const parsed = await parseTail(file, read);
1855
- if (parsed === null) return null;
1856
- const { tail, index } = parsed;
1854
+ const parsed = args.seededIndex === void 0 ? await parseTail(file, read) : null;
1855
+ if (args.seededIndex === void 0 && parsed === null) return null;
1856
+ const index = args.seededIndex ?? parsed.index;
1857
1857
  const firstMoof = index.samples[0]?.moofOffset ?? 0;
1858
1858
  if (firstMoof <= 0 || firstMoof >= file.size) return null;
1859
1859
  const head = await read(0, firstMoof);
1860
1860
  const init = parseInitInfo(head);
1861
1861
  if (init === null) return null;
1862
- const videoIndex = index.trackId === init.videoTrackId ? index : reparseTail(tail, file.size, init.videoTrackId);
1862
+ const videoIndex = index.trackId === init.videoTrackId ? index : parsed === null ? null : reparseTail(parsed.tail, file.size, init.videoTrackId);
1863
1863
  if (videoIndex === null) return null;
1864
1864
  const { start, end, sampleIndex } = fragmentRangeFor(videoIndex, Math.max(0, args.epochMs - args.segmentStartMs) / 1e3 * init.videoTimescale);
1865
1865
  if (start <= 0 || end <= start || end > file.size) return null;
@@ -2668,7 +2668,8 @@ function buildRecordingProvider(deps) {
2668
2668
  const gop = await readGopRange(file, {
2669
2669
  segmentStartMs: seg.startMs,
2670
2670
  segmentDurMs: seg.durMs,
2671
- epochMs
2671
+ epochMs,
2672
+ seededIndex: deps.mfraFor?.(deviceId, profile, startMs)
2672
2673
  });
2673
2674
  if (gop !== null) return {
2674
2675
  data: gop.data,
@@ -5437,6 +5438,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
5437
5438
  capacity: (root) => this.locationCapacity(root),
5438
5439
  hydrateDevice: (deviceId, locations) => hydrateDeviceFromStorage(this.ctx.api, this.index, deviceId, locations, this.ctx.logger),
5439
5440
  calendar: this.calendar,
5441
+ mfraFor: (deviceId, profile, startMs) => this.mfraTables.get(deviceId, profile, startMs),
5440
5442
  hydrateWindow: (deviceId, fromMs, toMs) => hydrateWindowFromStorage(this.index, deviceId, fromMs, toMs, this.resolvedLocations, this.ctx.logger),
5441
5443
  onConfigChanged: (deviceId) => this.controller?.onConfigChanged(deviceId) ?? Promise.resolve(),
5442
5444
  dataDir: this.ctx.dataDir,
@@ -1849,15 +1849,15 @@ async function readGopRange(file, args) {
1849
1849
  bytesRead += chunk.length;
1850
1850
  return chunk;
1851
1851
  };
1852
- const parsed = await parseTail(file, read);
1853
- if (parsed === null) return null;
1854
- const { tail, index } = parsed;
1852
+ const parsed = args.seededIndex === void 0 ? await parseTail(file, read) : null;
1853
+ if (args.seededIndex === void 0 && parsed === null) return null;
1854
+ const index = args.seededIndex ?? parsed.index;
1855
1855
  const firstMoof = index.samples[0]?.moofOffset ?? 0;
1856
1856
  if (firstMoof <= 0 || firstMoof >= file.size) return null;
1857
1857
  const head = await read(0, firstMoof);
1858
1858
  const init = parseInitInfo(head);
1859
1859
  if (init === null) return null;
1860
- const videoIndex = index.trackId === init.videoTrackId ? index : reparseTail(tail, file.size, init.videoTrackId);
1860
+ const videoIndex = index.trackId === init.videoTrackId ? index : parsed === null ? null : reparseTail(parsed.tail, file.size, init.videoTrackId);
1861
1861
  if (videoIndex === null) return null;
1862
1862
  const { start, end, sampleIndex } = fragmentRangeFor(videoIndex, Math.max(0, args.epochMs - args.segmentStartMs) / 1e3 * init.videoTimescale);
1863
1863
  if (start <= 0 || end <= start || end > file.size) return null;
@@ -2666,7 +2666,8 @@ function buildRecordingProvider(deps) {
2666
2666
  const gop = await readGopRange(file, {
2667
2667
  segmentStartMs: seg.startMs,
2668
2668
  segmentDurMs: seg.durMs,
2669
- epochMs
2669
+ epochMs,
2670
+ seededIndex: deps.mfraFor?.(deviceId, profile, startMs)
2670
2671
  });
2671
2672
  if (gop !== null) return {
2672
2673
  data: gop.data,
@@ -5435,6 +5436,7 @@ var RecorderV2Addon = class extends BaseAddon {
5435
5436
  capacity: (root) => this.locationCapacity(root),
5436
5437
  hydrateDevice: (deviceId, locations) => hydrateDeviceFromStorage(this.ctx.api, this.index, deviceId, locations, this.ctx.logger),
5437
5438
  calendar: this.calendar,
5439
+ mfraFor: (deviceId, profile, startMs) => this.mfraTables.get(deviceId, profile, startMs),
5438
5440
  hydrateWindow: (deviceId, fromMs, toMs) => hydrateWindowFromStorage(this.index, deviceId, fromMs, toMs, this.resolvedLocations, this.ctx.logger),
5439
5441
  onConfigChanged: (deviceId) => this.controller?.onConfigChanged(deviceId) ?? Promise.resolve(),
5440
5442
  dataDir: this.ctx.dataDir,
@@ -21718,21 +21718,32 @@ function clampPlaybackRate(rate) {
21718
21718
  if (!Number.isFinite(rate) || rate <= 0) return 0;
21719
21719
  return Math.min(4, Math.max(.25, rate));
21720
21720
  }
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
- })]);
21721
+ var ServerMessageSchema = require_dist.discriminatedUnion("t", [
21722
+ require_dist.object({
21723
+ t: require_dist.literal("state"),
21724
+ state: require_dist._enum([
21725
+ "live",
21726
+ "loading",
21727
+ "playing",
21728
+ "paused",
21729
+ "gap",
21730
+ "ended",
21731
+ "error"
21732
+ ])
21733
+ }),
21734
+ require_dist.object({
21735
+ t: require_dist.literal("position"),
21736
+ epochMs: require_dist.number()
21737
+ }),
21738
+ require_dist.object({
21739
+ t: require_dist.literal("liveProfile"),
21740
+ profile: require_dist._enum([
21741
+ "high",
21742
+ "mid",
21743
+ "low"
21744
+ ])
21745
+ })
21746
+ ]);
21736
21747
  function parseControlMessage(raw) {
21737
21748
  try {
21738
21749
  const parsed = ControlMessageSchema.safeParse(JSON.parse(raw));
@@ -22071,7 +22082,7 @@ var SEEK_DISCONTINUITY_MS = 250;
22071
22082
  * sustainable there — cross-fragment moves remain paced by the demux + the
22072
22083
  * one-in-flight guard, so this cannot spawn parallel demuxes.
22073
22084
  */
22074
- var SCRUB_COALESCE_MS = 60;
22085
+ var SCRUB_COALESCE_MS = 33;
22075
22086
  /**
22076
22087
  * How much forward slack two fragments may have and still count as ADJACENT
22077
22088
  * for sweep continuity. Two real cases live inside it: the ±1 ms rounding of a
@@ -22426,13 +22437,7 @@ var RecordedFeeder = class {
22426
22437
  const loadStartedAt = performance.now();
22427
22438
  let prefixFed = false;
22428
22439
  let gopFirst = false;
22429
- const fullLoad = this.loadAus(loc.startMs, this.rate !== 0, this.deps.readGop ? void 0 : (video) => {
22430
- if (this.disposed || token !== this.seekToken) return;
22431
- this.feedPrefix(loc, video, landMs);
22432
- prefixFed = true;
22433
- firstFrameMs = performance.now() - seekStartedAt;
22434
- });
22435
- fullLoad.catch(() => {});
22440
+ let stateAnnounced = false;
22436
22441
  if (this.deps.readGop) {
22437
22442
  const cached = this.scrubCache.covering(this.deps.profile, landMs);
22438
22443
  let fragment;
@@ -22454,7 +22459,18 @@ var RecordedFeeder = class {
22454
22459
  prefixFed = true;
22455
22460
  gopFirst = true;
22456
22461
  firstFrameMs = performance.now() - seekStartedAt;
22462
+ this.deps.onState(this.rate === 0 ? "paused" : "playing");
22463
+ this.deps.onPosition(this.cursorMs);
22464
+ this.startPositionReports();
22465
+ stateAnnounced = true;
22457
22466
  }
22467
+ const fullLoad = this.loadAus(loc.startMs, this.rate !== 0, this.deps.readGop ? void 0 : (video) => {
22468
+ if (this.disposed || token !== this.seekToken) return;
22469
+ this.feedPrefix(loc, video, landMs);
22470
+ prefixFed = true;
22471
+ firstFrameMs = performance.now() - seekStartedAt;
22472
+ });
22473
+ fullLoad.catch(() => {});
22458
22474
  const loaded = await fullLoad;
22459
22475
  loadMs = performance.now() - loadStartedAt;
22460
22476
  if (this.disposed || token !== this.seekToken || loaded === null) return;
@@ -22478,15 +22494,19 @@ var RecordedFeeder = class {
22478
22494
  }
22479
22495
  }
22480
22496
  if (this.rate === 0) {
22481
- this.deps.onState("paused");
22482
- this.deps.onPosition(this.cursorMs);
22483
- this.startPositionReports();
22497
+ if (!stateAnnounced) {
22498
+ this.deps.onState("paused");
22499
+ this.deps.onPosition(this.cursorMs);
22500
+ this.startPositionReports();
22501
+ }
22484
22502
  this.logSeekReady(epochMs, seekStartedAt, locateMs, travelMs, loadMs, firstFrameMs, "paused");
22485
22503
  return;
22486
22504
  }
22487
- this.deps.onState("playing");
22488
- this.deps.onPosition(this.cursorMs);
22489
- this.startPositionReports();
22505
+ if (!stateAnnounced) {
22506
+ this.deps.onState("playing");
22507
+ this.deps.onPosition(this.cursorMs);
22508
+ this.startPositionReports();
22509
+ }
22490
22510
  this.logSeekReady(epochMs, seekStartedAt, locateMs, travelMs, loadMs, firstFrameMs, "playing");
22491
22511
  const firstFullAu = loaded.video[0];
22492
22512
  const resumeAfterPtsMs = gopFirst ? firstFullAu ? firstFullAu.ptsMs + Math.max(0, this.cursorMs - loc.startMs) : null : this.lastFedVideoIdx >= 0 ? loaded.video[this.lastFedVideoIdx]?.ptsMs ?? null : null;
@@ -23599,6 +23619,12 @@ var TimelineSession = class {
23599
23619
  mode = "live";
23600
23620
  feeder = null;
23601
23621
  profile = "mid";
23622
+ /** Transport rate belongs to the session, not to one profile-specific feeder.
23623
+ * Scrub pins `low`; release restores the selected playback profile and may
23624
+ * therefore rebuild the feeder. Carrying the rate keeps a paused scrub
23625
+ * paused across that required quality transition. */
23626
+ rate = 1;
23627
+ liveProfileReported = false;
23602
23628
  /**
23603
23629
  * The last position the viewer was SHOWN (every `_emitPosition`). Handed to
23604
23630
  * `seek` as `travelFromMs` so the seek travel animation works across feeder
@@ -23622,6 +23648,7 @@ var TimelineSession = class {
23622
23648
  return this.mode;
23623
23649
  }
23624
23650
  async handleControl(msg) {
23651
+ this.emitLiveProfile();
23625
23652
  switch (msg.t) {
23626
23653
  case "playRecorded": {
23627
23654
  const profileChanged = msg.profile !== this.profile;
@@ -23650,6 +23677,7 @@ var TimelineSession = class {
23650
23677
  if (this.mode === "recorded") this.feeder?.scrubAck();
23651
23678
  break;
23652
23679
  case "setRate":
23680
+ this.rate = msg.rate;
23653
23681
  this.feeder?.setRate(msg.rate);
23654
23682
  break;
23655
23683
  case "stepFrame":
@@ -23674,6 +23702,7 @@ var TimelineSession = class {
23674
23702
  this.deps.session.setLiveFeedGate(true);
23675
23703
  this.mode = "recorded";
23676
23704
  const feeder = this.deps.makeFeeder(this.profile);
23705
+ if (this.rate !== 1) feeder.setRate(this.rate);
23677
23706
  this.feeder = feeder;
23678
23707
  return feeder;
23679
23708
  }
@@ -23701,6 +23730,14 @@ var TimelineSession = class {
23701
23730
  epochMs
23702
23731
  }));
23703
23732
  }
23733
+ emitLiveProfile() {
23734
+ if (this.liveProfileReported || this.deps.initialLiveProfile === void 0) return;
23735
+ this.liveProfileReported = true;
23736
+ this.deps.session.sendControl(serializeServerMessage({
23737
+ t: "liveProfile",
23738
+ profile: this.deps.initialLiveProfile
23739
+ }));
23740
+ }
23704
23741
  dispose() {
23705
23742
  this.feeder?.dispose();
23706
23743
  this.feeder = null;
@@ -31360,8 +31397,8 @@ var ADAPTIVE_CONFIG = {
31360
31397
  analyzeWindowMs: 15e3,
31361
31398
  minFailPercent: .05,
31362
31399
  waitAfterResetMs: 8e3,
31363
- upgradeHealthyMs: 6e4,
31364
- minTimeBetweenChangesMs: 6e4,
31400
+ upgradeHealthyMs: 2e4,
31401
+ minTimeBetweenChangesMs: 2e4,
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,
@@ -32522,9 +32560,9 @@ 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`.
@@ -32544,7 +32582,8 @@ var BrokerWebrtcServer = class {
32544
32582
  });
32545
32583
  entry.session.onReceiverReport((s) => controller.onLoss(s, Date.now()));
32546
32584
  entry.adaptiveController = controller;
32547
- let clientLossN = 0;
32585
+ let clientHighestSequence = 0;
32586
+ let clientPacketsLost = 0;
32548
32587
  let tickInFlight = false;
32549
32588
  const CLIENT_LOSS_SPAN = 1e3;
32550
32589
  entry.adaptiveTimer = setInterval(() => {
@@ -32558,11 +32597,12 @@ var BrokerWebrtcServer = class {
32558
32597
  }
32559
32598
  const pct = this.clientLossPct ? await this.clientLossPct(deviceId) : null;
32560
32599
  const now = Date.now();
32561
- if (pct !== null && pct !== void 0 && pct > 0) {
32562
- clientLossN += 1;
32600
+ if (pct !== null && pct !== void 0) {
32601
+ clientHighestSequence += CLIENT_LOSS_SPAN;
32602
+ clientPacketsLost += Math.round(Math.max(0, Math.min(100, pct)) / 100 * CLIENT_LOSS_SPAN);
32563
32603
  controller.onLoss({
32564
- packetsLost: Math.round(pct / 100 * CLIENT_LOSS_SPAN * clientLossN),
32565
- highestSequence: CLIENT_LOSS_SPAN * clientLossN
32604
+ packetsLost: clientPacketsLost,
32605
+ highestSequence: clientHighestSequence
32566
32606
  }, now);
32567
32607
  }
32568
32608
  controller.tick(now);
@@ -21712,21 +21712,32 @@ function clampPlaybackRate(rate) {
21712
21712
  if (!Number.isFinite(rate) || rate <= 0) return 0;
21713
21713
  return Math.min(4, Math.max(.25, rate));
21714
21714
  }
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
- })]);
21715
+ var ServerMessageSchema = discriminatedUnion("t", [
21716
+ object({
21717
+ t: literal("state"),
21718
+ state: _enum([
21719
+ "live",
21720
+ "loading",
21721
+ "playing",
21722
+ "paused",
21723
+ "gap",
21724
+ "ended",
21725
+ "error"
21726
+ ])
21727
+ }),
21728
+ object({
21729
+ t: literal("position"),
21730
+ epochMs: number()
21731
+ }),
21732
+ object({
21733
+ t: literal("liveProfile"),
21734
+ profile: _enum([
21735
+ "high",
21736
+ "mid",
21737
+ "low"
21738
+ ])
21739
+ })
21740
+ ]);
21730
21741
  function parseControlMessage(raw) {
21731
21742
  try {
21732
21743
  const parsed = ControlMessageSchema.safeParse(JSON.parse(raw));
@@ -22065,7 +22076,7 @@ var SEEK_DISCONTINUITY_MS = 250;
22065
22076
  * sustainable there — cross-fragment moves remain paced by the demux + the
22066
22077
  * one-in-flight guard, so this cannot spawn parallel demuxes.
22067
22078
  */
22068
- var SCRUB_COALESCE_MS = 60;
22079
+ var SCRUB_COALESCE_MS = 33;
22069
22080
  /**
22070
22081
  * How much forward slack two fragments may have and still count as ADJACENT
22071
22082
  * for sweep continuity. Two real cases live inside it: the ±1 ms rounding of a
@@ -22420,13 +22431,7 @@ var RecordedFeeder = class {
22420
22431
  const loadStartedAt = performance.now();
22421
22432
  let prefixFed = false;
22422
22433
  let gopFirst = false;
22423
- const fullLoad = this.loadAus(loc.startMs, this.rate !== 0, this.deps.readGop ? void 0 : (video) => {
22424
- if (this.disposed || token !== this.seekToken) return;
22425
- this.feedPrefix(loc, video, landMs);
22426
- prefixFed = true;
22427
- firstFrameMs = performance.now() - seekStartedAt;
22428
- });
22429
- fullLoad.catch(() => {});
22434
+ let stateAnnounced = false;
22430
22435
  if (this.deps.readGop) {
22431
22436
  const cached = this.scrubCache.covering(this.deps.profile, landMs);
22432
22437
  let fragment;
@@ -22448,7 +22453,18 @@ var RecordedFeeder = class {
22448
22453
  prefixFed = true;
22449
22454
  gopFirst = true;
22450
22455
  firstFrameMs = performance.now() - seekStartedAt;
22456
+ this.deps.onState(this.rate === 0 ? "paused" : "playing");
22457
+ this.deps.onPosition(this.cursorMs);
22458
+ this.startPositionReports();
22459
+ stateAnnounced = true;
22451
22460
  }
22461
+ const fullLoad = this.loadAus(loc.startMs, this.rate !== 0, this.deps.readGop ? void 0 : (video) => {
22462
+ if (this.disposed || token !== this.seekToken) return;
22463
+ this.feedPrefix(loc, video, landMs);
22464
+ prefixFed = true;
22465
+ firstFrameMs = performance.now() - seekStartedAt;
22466
+ });
22467
+ fullLoad.catch(() => {});
22452
22468
  const loaded = await fullLoad;
22453
22469
  loadMs = performance.now() - loadStartedAt;
22454
22470
  if (this.disposed || token !== this.seekToken || loaded === null) return;
@@ -22472,15 +22488,19 @@ var RecordedFeeder = class {
22472
22488
  }
22473
22489
  }
22474
22490
  if (this.rate === 0) {
22475
- this.deps.onState("paused");
22476
- this.deps.onPosition(this.cursorMs);
22477
- this.startPositionReports();
22491
+ if (!stateAnnounced) {
22492
+ this.deps.onState("paused");
22493
+ this.deps.onPosition(this.cursorMs);
22494
+ this.startPositionReports();
22495
+ }
22478
22496
  this.logSeekReady(epochMs, seekStartedAt, locateMs, travelMs, loadMs, firstFrameMs, "paused");
22479
22497
  return;
22480
22498
  }
22481
- this.deps.onState("playing");
22482
- this.deps.onPosition(this.cursorMs);
22483
- this.startPositionReports();
22499
+ if (!stateAnnounced) {
22500
+ this.deps.onState("playing");
22501
+ this.deps.onPosition(this.cursorMs);
22502
+ this.startPositionReports();
22503
+ }
22484
22504
  this.logSeekReady(epochMs, seekStartedAt, locateMs, travelMs, loadMs, firstFrameMs, "playing");
22485
22505
  const firstFullAu = loaded.video[0];
22486
22506
  const resumeAfterPtsMs = gopFirst ? firstFullAu ? firstFullAu.ptsMs + Math.max(0, this.cursorMs - loc.startMs) : null : this.lastFedVideoIdx >= 0 ? loaded.video[this.lastFedVideoIdx]?.ptsMs ?? null : null;
@@ -23593,6 +23613,12 @@ var TimelineSession = class {
23593
23613
  mode = "live";
23594
23614
  feeder = null;
23595
23615
  profile = "mid";
23616
+ /** Transport rate belongs to the session, not to one profile-specific feeder.
23617
+ * Scrub pins `low`; release restores the selected playback profile and may
23618
+ * therefore rebuild the feeder. Carrying the rate keeps a paused scrub
23619
+ * paused across that required quality transition. */
23620
+ rate = 1;
23621
+ liveProfileReported = false;
23596
23622
  /**
23597
23623
  * The last position the viewer was SHOWN (every `_emitPosition`). Handed to
23598
23624
  * `seek` as `travelFromMs` so the seek travel animation works across feeder
@@ -23616,6 +23642,7 @@ var TimelineSession = class {
23616
23642
  return this.mode;
23617
23643
  }
23618
23644
  async handleControl(msg) {
23645
+ this.emitLiveProfile();
23619
23646
  switch (msg.t) {
23620
23647
  case "playRecorded": {
23621
23648
  const profileChanged = msg.profile !== this.profile;
@@ -23644,6 +23671,7 @@ var TimelineSession = class {
23644
23671
  if (this.mode === "recorded") this.feeder?.scrubAck();
23645
23672
  break;
23646
23673
  case "setRate":
23674
+ this.rate = msg.rate;
23647
23675
  this.feeder?.setRate(msg.rate);
23648
23676
  break;
23649
23677
  case "stepFrame":
@@ -23668,6 +23696,7 @@ var TimelineSession = class {
23668
23696
  this.deps.session.setLiveFeedGate(true);
23669
23697
  this.mode = "recorded";
23670
23698
  const feeder = this.deps.makeFeeder(this.profile);
23699
+ if (this.rate !== 1) feeder.setRate(this.rate);
23671
23700
  this.feeder = feeder;
23672
23701
  return feeder;
23673
23702
  }
@@ -23695,6 +23724,14 @@ var TimelineSession = class {
23695
23724
  epochMs
23696
23725
  }));
23697
23726
  }
23727
+ emitLiveProfile() {
23728
+ if (this.liveProfileReported || this.deps.initialLiveProfile === void 0) return;
23729
+ this.liveProfileReported = true;
23730
+ this.deps.session.sendControl(serializeServerMessage({
23731
+ t: "liveProfile",
23732
+ profile: this.deps.initialLiveProfile
23733
+ }));
23734
+ }
23698
23735
  dispose() {
23699
23736
  this.feeder?.dispose();
23700
23737
  this.feeder = null;
@@ -31351,8 +31388,8 @@ var ADAPTIVE_CONFIG = {
31351
31388
  analyzeWindowMs: 15e3,
31352
31389
  minFailPercent: .05,
31353
31390
  waitAfterResetMs: 8e3,
31354
- upgradeHealthyMs: 6e4,
31355
- minTimeBetweenChangesMs: 6e4,
31391
+ upgradeHealthyMs: 2e4,
31392
+ minTimeBetweenChangesMs: 2e4,
31356
31393
  maxUpgradeBackoffMs: 9e5
31357
31394
  };
31358
31395
  /**
@@ -31520,8 +31557,7 @@ function scoreBroker(tier, broker, hints) {
31520
31557
  const fallback = tier ? LABEL_DEFAULTS[tier] : void 0;
31521
31558
  const bitrateKbps = stats.bitrateKbps > 0 ? stats.bitrateKbps : fallback?.bitrateKbps ?? 2e3;
31522
31559
  const srcPixels = fallback?.pixels ?? 1920 * 1080;
31523
- const dpr = hints.devicePixelRatio ?? 1;
31524
- const targetPixels = (hints.viewportWidth ?? 1920) * dpr * ((hints.viewportHeight ?? 1080) * dpr);
31560
+ const targetPixels = (hints.viewportWidth ?? 1920) * (hints.viewportHeight ?? 1080);
31525
31561
  const pixelDistance = Math.abs(srcPixels - targetPixels) / 1e6;
31526
31562
  let bandwidthPenalty = 0;
31527
31563
  if (hints.downlinkMbps && hints.downlinkMbps > 0 && bitrateKbps > 0) {
@@ -32266,6 +32302,7 @@ var BrokerWebrtcServer = class {
32266
32302
  }
32267
32303
  });
32268
32304
  const scrubCache = new ScrubFragmentCache();
32305
+ const initialLiveProfile = this.profileTierResolver?.(brokerId) ?? null;
32269
32306
  const readGopAccessor = this.readRecordedGopBytes;
32270
32307
  const readGop = readGopAccessor ? async (args) => {
32271
32308
  const res = await readGopAccessor({
@@ -32285,6 +32322,7 @@ var BrokerWebrtcServer = class {
32285
32322
  timeline = new TimelineSession({
32286
32323
  session,
32287
32324
  deviceId,
32325
+ ...initialLiveProfile !== null ? { initialLiveProfile } : {},
32288
32326
  warm: readGop ? (profile, epochMs) => {
32289
32327
  warmFragment({
32290
32328
  deviceId,
@@ -32513,9 +32551,9 @@ var BrokerWebrtcServer = class {
32513
32551
  * server.
32514
32552
  *
32515
32553
  * The client-loss % is turned into a monotonic {@link LossSample} LOCALLY:
32516
- * a per-controller counter `clientLossN` advances each tick that carries a
32517
- * positive loss %, so consecutive samples have a clean delta whose ratio
32518
- * equals `pct/100` exactly what the controller's `LossTracker` expects.
32554
+ * per-controller cumulative sequence/loss counters advance for every
32555
+ * reported interval (including 0% loss), so a perfectly healthy LAN is real
32556
+ * evidence rather than silence, exactly like RTCP's cumulative fields.
32519
32557
  * Both paths feed the same controller; the tracker naturally honours
32520
32558
  * whichever produces the higher loss ratio across its window. The async
32521
32559
  * poll is guarded against overlapping ticks via `tickInFlight`.
@@ -32535,7 +32573,8 @@ var BrokerWebrtcServer = class {
32535
32573
  });
32536
32574
  entry.session.onReceiverReport((s) => controller.onLoss(s, Date.now()));
32537
32575
  entry.adaptiveController = controller;
32538
- let clientLossN = 0;
32576
+ let clientHighestSequence = 0;
32577
+ let clientPacketsLost = 0;
32539
32578
  let tickInFlight = false;
32540
32579
  const CLIENT_LOSS_SPAN = 1e3;
32541
32580
  entry.adaptiveTimer = setInterval(() => {
@@ -32549,11 +32588,12 @@ var BrokerWebrtcServer = class {
32549
32588
  }
32550
32589
  const pct = this.clientLossPct ? await this.clientLossPct(deviceId) : null;
32551
32590
  const now = Date.now();
32552
- if (pct !== null && pct !== void 0 && pct > 0) {
32553
- clientLossN += 1;
32591
+ if (pct !== null && pct !== void 0) {
32592
+ clientHighestSequence += CLIENT_LOSS_SPAN;
32593
+ clientPacketsLost += Math.round(Math.max(0, Math.min(100, pct)) / 100 * CLIENT_LOSS_SPAN);
32554
32594
  controller.onLoss({
32555
- packetsLost: Math.round(pct / 100 * CLIENT_LOSS_SPAN * clientLossN),
32556
- highestSequence: CLIENT_LOSS_SPAN * clientLossN
32595
+ packetsLost: clientPacketsLost,
32596
+ highestSequence: clientHighestSequence
32557
32597
  }, now);
32558
32598
  }
32559
32599
  controller.tick(now);
@@ -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};