@camstack/system 1.2.262 → 1.2.263

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.
@@ -595,82 +595,50 @@ var GridLastFrameFileStore = class {
595
595
  //#endregion
596
596
  //#region src/builtins/camera-grid/grid-profiles.ts
597
597
  /**
598
- * One id, for every grid, forever — for the HIGH composition.
598
+ * One id, for every grid, forever.
599
599
  *
600
- * D519's reason still holds and is what keeps this id bare: a per-instance or
601
- * newly-spelled id mints a second cam-stream and orphans whatever profile
602
- * assignment the operator had made. `grid` was `high` on the live hub before
603
- * this change, so `high` keeps it and the other two are new.
600
+ * D519's reason is what keeps this id bare: a per-instance or newly-spelled id
601
+ * mints a second cam-stream and orphans whatever profile assignment the
602
+ * operator had made. It was `high` on the live hub before the per-profile
603
+ * detour and it is `high` again, so nothing an operator assigned is disturbed.
604
604
  */
605
605
  var GRID_CAM_STREAM_ID = "grid";
606
- /** Every profile a grid could conceivably compose, in descending quality. */
607
- var GRID_PROFILES = [
608
- "high",
609
- "mid",
610
- "low"
611
- ];
612
- /**
613
- * Canvas scale per profile. Named constants and not a computation: the numbers
614
- * are a policy (what a `mid` grid is FOR), and a policy that lives in an
615
- * expression is a policy nobody can find.
616
- */
617
- var CANVAS_SCALE = {
618
- high: 1,
619
- mid: .5,
620
- low: .25
621
- };
622
606
  /**
623
- * The smallest canvas a composition is allowed to shrink to. A grid of four
624
- * cells at 40x30 is not a cheaper picture, it is no picture.
607
+ * The slot the single composition claims.
608
+ *
609
+ * NOT decoration. A device publishing one stream with no hint is ranked by
610
+ * pixel count and lands in `mid` — the slot `recordings` does not select on its
611
+ * own, that `recordingsLow` ignores, and that scrub never reads. The Dreame
612
+ * robot's single 720p relay landed exactly there and its footage was written
613
+ * where nothing looked.
625
614
  */
626
- var MIN_CANVAS_WIDTH = 160;
627
- var MIN_CANVAS_HEIGHT = 90;
628
- /** The cam-stream id a grid publishes for one profile. */
629
- function gridCamStreamId(profile) {
630
- return profile === "high" ? GRID_CAM_STREAM_ID : `${GRID_CAM_STREAM_ID}-${profile}`;
615
+ var GRID_OUTPUT_PROFILE = "high";
616
+ /** Cheapest first — the order a source is read in. */
617
+ var CHEAPEST_FIRST = [
618
+ "low",
619
+ "mid",
620
+ "high"
621
+ ];
622
+ /** The cheapest stream this source serves, or `null` when it serves none. */
623
+ function lowestServedProfile(served) {
624
+ return CHEAPEST_FIRST.find((profile) => served.includes(profile)) ?? null;
631
625
  }
632
- /** Even, floored, scaled by the profile. libx264 refuses an odd dimension. */
633
- function gridCanvasFor(canvas, profile) {
634
- const scale = CANVAS_SCALE[profile];
626
+ /** Which stream to read each source at, and who cannot be read at all. */
627
+ function gridSourceProfiles(input) {
628
+ const profileBySource = /* @__PURE__ */ new Map();
629
+ const missingSources = [];
630
+ for (const deviceId of input.sourceDeviceIds) {
631
+ const served = input.profilesServedBy(deviceId);
632
+ const chosen = served === null ? null : lowestServedProfile(served);
633
+ if (chosen === null) missingSources.push(deviceId);
634
+ else profileBySource.set(deviceId, chosen);
635
+ }
635
636
  return {
636
- width: evenAtLeast(canvas.width * scale, MIN_CANVAS_WIDTH),
637
- height: evenAtLeast(canvas.height * scale, MIN_CANVAS_HEIGHT)
637
+ offered: input.sourceDeviceIds.length > 0 && missingSources.length === 0,
638
+ profileBySource,
639
+ missingSources
638
640
  };
639
641
  }
640
- function evenAtLeast(value, floor) {
641
- const rounded = Math.max(floor, Math.round(value));
642
- return rounded % 2 === 0 ? rounded : rounded + 1;
643
- }
644
- /**
645
- * What this grid can honestly publish, per profile, with the refusal named.
646
- *
647
- * Every profile gets a row whether it is offered or not: an absent row says
648
- * nothing, and the caller logging a refusal needs the reason more than the
649
- * caller publishing an offer needs the offer.
650
- */
651
- function offeredGridProfiles(input) {
652
- const served = /* @__PURE__ */ new Map();
653
- for (const deviceId of input.sourceDeviceIds) served.set(deviceId, input.profilesServedBy(deviceId));
654
- return GRID_PROFILES.map((profile) => {
655
- if (input.sourceDeviceIds.length === 0) return {
656
- profile,
657
- offered: false,
658
- missingSources: []
659
- };
660
- const missingSources = input.sourceDeviceIds.filter((deviceId) => {
661
- const profiles = served.get(deviceId);
662
- return profiles === null || profiles === void 0 || !profiles.includes(profile);
663
- });
664
- return missingSources.length === 0 ? {
665
- profile,
666
- offered: true
667
- } : {
668
- profile,
669
- offered: false,
670
- missingSources
671
- };
672
- });
673
- }
674
642
  //#endregion
675
643
  //#region src/builtins/camera-grid/grid-sentry-sources.ts
676
644
  function gridSourceDial(acquired) {
@@ -689,6 +657,43 @@ function gridSourceDial(acquired) {
689
657
  //#endregion
690
658
  //#region src/builtins/camera-grid/grid-snapshot.ts
691
659
  /**
660
+ * A grid's snapshot — a frame of the COMPOSITION, never a dial.
661
+ *
662
+ * ## The decision, and the two things it rejects
663
+ *
664
+ * The operator: *"Lo snapshot possiamo sempre farlo dallo stream sempre, non
665
+ * c'e' bisogno di comporre gli snapshots"*. So:
666
+ *
667
+ * - **Not composed from source snapshots.** That would be a SECOND compositor,
668
+ * with its own geometry, its own aspect handling and its own bugs, producing
669
+ * a picture that differs from the real one in ways nobody would ever notice
670
+ * until they mattered.
671
+ * - **Not produced by starting the composition.** The whole of D519 is "a grid
672
+ * costs nothing when nobody watches". A thumbnail that dials N cameras — and
673
+ * on a battery camera, wakes one — spends the entire budget of the feature on
674
+ * a 160-pixel-wide picture in a list.
675
+ *
676
+ * ## The three states, all answered
677
+ *
678
+ * | state | answer |
679
+ * | --- | --- |
680
+ * | a composition is running | one frame sampled out of the bytes it is already emitting |
681
+ * | nothing running, a frame was kept | that frame — stale, and honest about it |
682
+ * | nothing running, nothing ever kept | `null`, logged with the reason |
683
+ *
684
+ * `null` is the courtesy card, which is the correct picture of "this grid has
685
+ * never been watched". What it must never be is a reason to dial.
686
+ *
687
+ * ## Why this is a NATIVE snapshot provider
688
+ *
689
+ * The wrapper's ladder is native-first: a native that produces a frame
690
+ * short-circuits before the broker frame-grab, and the broker frame-grab is the
691
+ * thing that would wake the sources. A grid with a stored frame or a live
692
+ * composition therefore never reaches it. The residual — a grid that has never
693
+ * once been composed — is documented in D519; it cannot wake a battery camera,
694
+ * because a grid never dials one live at all (`grid-sentry-sources.ts`).
695
+ */
696
+ /**
692
697
  * Which of the three answers applies. Pure, so the rule can be read — and
693
698
  * sabotaged — without a socket, a file or an ffmpeg.
694
699
  */
@@ -721,10 +726,7 @@ var GridSnapshotSource = class {
721
726
  return toImage(sampled);
722
727
  }
723
728
  if (stored !== null) return toImage(stored);
724
- this.deps.logger.warn("camera grid: the composition is running but produced no frame, and none was kept", {
725
- tags: { deviceId },
726
- meta: { profile: live.profile }
727
- });
729
+ this.deps.logger.warn("camera grid: the composition is running but produced no frame, and none was kept", { tags: { deviceId } });
728
730
  return null;
729
731
  }
730
732
  if (answer.kind === "stored" && stored !== null) return toImage(stored);
@@ -732,7 +734,7 @@ var GridSnapshotSource = class {
732
734
  tags: { deviceId },
733
735
  meta: {
734
736
  reason: "never-composed",
735
- profiles: sessions.length
737
+ compositions: sessions.length
736
738
  }
737
739
  });
738
740
  return null;
@@ -741,18 +743,12 @@ var GridSnapshotSource = class {
741
743
  try {
742
744
  const jpeg = await this.deps.sampleFrame(session);
743
745
  if (jpeg !== null && jpeg.length > 0) return jpeg;
744
- this.deps.logger.warn("camera grid: could not sample a frame from the live composition", {
745
- tags: { deviceId },
746
- meta: { profile: session.profile }
747
- });
746
+ this.deps.logger.warn("camera grid: could not sample a frame from the live composition", { tags: { deviceId } });
748
747
  return null;
749
748
  } catch (error) {
750
749
  this.deps.logger.warn("camera grid: could not sample a frame from the live composition", {
751
750
  tags: { deviceId },
752
- meta: {
753
- profile: session.profile,
754
- error: errMsg(error)
755
- }
751
+ meta: { error: errMsg(error) }
756
752
  });
757
753
  return null;
758
754
  }
@@ -806,24 +802,15 @@ var PATH_PREFIX = "/grid/";
806
802
  var PATH_SUFFIX = GRID_WIRE_PATH_SUFFIX;
807
803
  /** How long a reader may refuse to drain before it is cut. */
808
804
  var SLOW_READER_MS = 5e3;
809
- function asProfile(raw) {
810
- return GRID_PROFILES.find((profile) => profile === raw) ?? null;
811
- }
812
- /** `/grid/<instanceId>/<profile>.flv`, or `null` when it is anything else. */
805
+ /** `/grid/<instanceId>.flv`, or `null` when it is anything else. */
813
806
  function parseGridStreamPath(url) {
814
807
  const path = (url ?? "").split("?")[0] ?? "";
815
808
  if (!path.startsWith(PATH_PREFIX) || !path.endsWith(PATH_SUFFIX)) return null;
816
809
  const raw = path.slice(6, path.length - PATH_SUFFIX.length);
817
- const at = raw.lastIndexOf("/");
818
- if (at <= 0) return null;
819
- const profile = asProfile(raw.slice(at + 1));
820
- if (profile === null) return null;
810
+ if (raw.length === 0 || raw.includes("/")) return null;
821
811
  try {
822
- const instanceId = decodeURIComponent(raw.slice(0, at));
823
- return instanceId.length > 0 ? {
824
- instanceId,
825
- profile
826
- } : null;
812
+ const instanceId = decodeURIComponent(raw);
813
+ return instanceId.length > 0 ? { instanceId } : null;
827
814
  } catch {
828
815
  return null;
829
816
  }
@@ -862,16 +849,13 @@ var GridStreamRelay = class {
862
849
  streamUrl(request) {
863
850
  if (!this.baseUrl) throw new Error("camera-grid: the stream relay is not started");
864
851
  const instance = encodeURIComponent(request.instanceId);
865
- return `${this.baseUrl}${PATH_PREFIX}${instance}/${request.profile}${PATH_SUFFIX}`;
852
+ return `${this.baseUrl}${PATH_PREFIX}${instance}${PATH_SUFFIX}`;
866
853
  }
867
854
  async serve(request, res) {
868
- const { instanceId, profile } = request;
855
+ const { instanceId } = request;
869
856
  const session = this.deps.sessionFor(request);
870
857
  if (session === null) {
871
- this.deps.logger.warn("camera grid: a stream was requested for an unknown grid", { meta: {
872
- instanceId,
873
- profile
874
- } });
858
+ this.deps.logger.warn("camera grid: a stream was requested for an unknown grid", { meta: { instanceId } });
875
859
  res.writeHead(404).end();
876
860
  return;
877
861
  }
@@ -905,7 +889,6 @@ var GridStreamRelay = class {
905
889
  tags: { deviceId: session.deviceId },
906
890
  meta: {
907
891
  instanceId,
908
- profile,
909
892
  stalledForMs: SLOW_READER_MS
910
893
  }
911
894
  });
@@ -918,7 +901,6 @@ var GridStreamRelay = class {
918
901
  tags: { deviceId: session.deviceId },
919
902
  meta: {
920
903
  instanceId,
921
- profile,
922
904
  reason
923
905
  }
924
906
  });
@@ -931,7 +913,6 @@ var GridStreamRelay = class {
931
913
  tags: { deviceId: session.deviceId },
932
914
  meta: {
933
915
  instanceId,
934
- profile,
935
916
  error: errMsg(error)
936
917
  }
937
918
  });
@@ -967,13 +948,13 @@ var GridStreamRelay = class {
967
948
  * scrub never reads. The Dreame robot's one 720p relay landed exactly there and
968
949
  * its footage was written nowhere anything looked.
969
950
  *
970
- * With several compositions published the hint does a second job: it is the
971
- * only thing that says which slot each one is FOR. Ranked by pixels a grid's
972
- * `mid` would take the `high` slot on any grid whose `high` was not offered.
951
+ * So the single composition claims `high` EXPLICITLY (`GRID_OUTPUT_PROFILE`).
952
+ * It is also the slot this stream held before the per-profile detour, which is
953
+ * what keeps an operator's existing assignment intact.
973
954
  */
974
955
  function gridStreamDescriptor(input) {
975
956
  return {
976
- camStreamId: gridCamStreamId(input.profile),
957
+ camStreamId: GRID_CAM_STREAM_ID,
977
958
  kind: GRID_WIRE_STREAM_KIND,
978
959
  url: input.url,
979
960
  codec: "h264",
@@ -982,8 +963,8 @@ function gridStreamDescriptor(input) {
982
963
  height: input.height
983
964
  },
984
965
  fps: input.fps,
985
- label: `Grid composition (${input.profile})`,
986
- profileHint: input.profile,
966
+ label: "Grid composition",
967
+ profileHint: GRID_OUTPUT_PROFILE,
987
968
  autoEligible: true
988
969
  };
989
970
  }
@@ -1761,21 +1742,18 @@ var DEFAULTS = {
1761
1742
  cameraGrids: [],
1762
1743
  cameraGridTombstones: []
1763
1744
  };
1764
- /** One session per (grid, profile). A grid composes each profile separately. */
1765
- function sessionKey(instanceId, profile) {
1766
- return `${instanceId}::${profile}`;
1767
- }
1768
1745
  /**
1769
- * One grid's composition of one profile, and enough about it for the snapshot
1770
- * to choose between them without reaching into the session.
1746
+ * One grid's composition — there is exactly one, keyed by the grid itself.
1747
+ *
1748
+ * It used to be one per profile, and the key carried the profile. That shape
1749
+ * is what made a viewer's adaptive step tear a composition down and build
1750
+ * another; see `grid-profiles.ts` for the log that showed it.
1771
1751
  */
1772
1752
  var GridSessionEntry = class {
1773
1753
  instanceId;
1774
- profile;
1775
1754
  session;
1776
- constructor(instanceId, profile, session) {
1755
+ constructor(instanceId, session) {
1777
1756
  this.instanceId = instanceId;
1778
- this.profile = profile;
1779
1757
  this.session = session;
1780
1758
  }
1781
1759
  isRunning() {
@@ -1785,7 +1763,7 @@ var GridSessionEntry = class {
1785
1763
  var CameraGridAddon = class extends BaseAddon {
1786
1764
  relay = null;
1787
1765
  reconcileTimer = null;
1788
- /** Keyed by {@link sessionKey} — one composition per grid per profile. */
1766
+ /** Keyed by instance id — ONE composition per grid. */
1789
1767
  sessions = /* @__PURE__ */ new Map();
1790
1768
  deviceIdByInstanceId = /* @__PURE__ */ new Map();
1791
1769
  tombstones = /* @__PURE__ */ new Set();
@@ -1799,7 +1777,7 @@ var CameraGridAddon = class extends BaseAddon {
1799
1777
  * (D224), and 60 s is the window for a fact — "does 615 have a low assigned"
1800
1778
  * — that only changes when an operator changes it.
1801
1779
  */
1802
- offersByInstanceId = /* @__PURE__ */ new Map();
1780
+ planByInstanceId = /* @__PURE__ */ new Map();
1803
1781
  snapshots = null;
1804
1782
  frameSampler = null;
1805
1783
  ffmpegBinaryPath = "ffmpeg";
@@ -1902,7 +1880,7 @@ var CameraGridAddon = class extends BaseAddon {
1902
1880
  if (!this.relay) return;
1903
1881
  const instances = this.instances();
1904
1882
  const declarations = buildGridCameraDeclarations(instances);
1905
- await this.refreshProfileOffers(instances);
1883
+ await this.refreshSourcePlans(instances);
1906
1884
  await this.retireWithdrawnSessions(instances);
1907
1885
  const result = await new DeclaredDevices({
1908
1886
  logger: this.ctx.logger.child("declaration"),
@@ -1969,15 +1947,15 @@ var CameraGridAddon = class extends BaseAddon {
1969
1947
  }
1970
1948
  }
1971
1949
  /**
1972
- * Which profiles each grid can honestly publish, asked of the BROKER.
1950
+ * Which stream each grid reads each of its sources at, asked of the BROKER.
1973
1951
  *
1974
1952
  * `listAllProfileSlots` is the one authority on "does 615 have a low
1975
1953
  * assigned", and `sourceCamStreamId === null` is its way of saying no. A read
1976
- * that FAILS leaves the previous answer in place and says so - it is not an
1954
+ * that FAILS leaves the previous answer in place and says so — it is not an
1977
1955
  * empty catalog, and turning it into one would retract the streams of every
1978
1956
  * grid on the hub.
1979
1957
  */
1980
- async refreshProfileOffers(instances) {
1958
+ async refreshSourcePlans(instances) {
1981
1959
  let slots;
1982
1960
  try {
1983
1961
  slots = await this.ctx.api.streamBroker.listAllProfileSlots.query();
@@ -1996,69 +1974,70 @@ var CameraGridAddon = class extends BaseAddon {
1996
1974
  servedBy.set(slot.deviceId, profiles);
1997
1975
  }
1998
1976
  for (const instance of instances) {
1999
- const offers = offeredGridProfiles({
1977
+ const plan = gridSourceProfiles({
2000
1978
  sourceDeviceIds: [...new Set(instance.cells.map((cell) => cell.deviceId))],
2001
1979
  profilesServedBy: (deviceId) => servedBy.get(deviceId) ?? null
2002
1980
  });
2003
- const previous = this.offersByInstanceId.get(instance.id);
2004
- this.offersByInstanceId.set(instance.id, offers);
2005
- this.logRefusals(instance, offers, previous);
1981
+ const previous = this.planByInstanceId.get(instance.id);
1982
+ this.planByInstanceId.set(instance.id, plan);
1983
+ this.logSourcePlan(instance, plan, previous);
2006
1984
  }
2007
1985
  }
2008
1986
  /**
2009
- * Say which profiles this grid will NOT publish, and why - once per change.
1987
+ * Say WHICH stream each cell is read at, and who could not be read - once per
1988
+ * change.
2010
1989
  *
2011
- * A profile silently missing from a catalog is indistinguishable from a
2012
- * broker that swept it. Logged on the EDGE rather than every minute: a grid
2013
- * that can only serve `high` would otherwise write two lines a minute for
2014
- * ever.
1990
+ * Both halves earn their line. A grid silently missing from a catalog is
1991
+ * indistinguishable from a broker that swept it, so the refusal names the
1992
+ * sources. And the chosen profiles are the answer to "why is this tile
1993
+ * soft?" — a camera whose only assigned slot is `high` is read at `high`, and
1994
+ * nothing else in the system would ever say so.
1995
+ *
1996
+ * Logged on the EDGE: a grid that cannot be composed would otherwise write a
1997
+ * line a minute for ever.
2015
1998
  */
2016
- logRefusals(instance, offers, previous) {
2017
- if (previous !== void 0 && JSON.stringify(previous) === JSON.stringify(offers)) return;
1999
+ logSourcePlan(instance, plan, previous) {
2000
+ const shape = (value) => JSON.stringify([
2001
+ value.offered,
2002
+ [...value.profileBySource],
2003
+ value.missingSources
2004
+ ]);
2005
+ if (previous !== void 0 && shape(previous) === shape(plan)) return;
2018
2006
  const deviceId = this.deviceIdByInstanceId.get(instance.id) ?? 0;
2019
- for (const offer of offers) {
2020
- if (offer.offered) continue;
2021
- if (offer.missingSources.length === 0) continue;
2022
- this.ctx.logger.info("camera grid: a profile is NOT on offer", {
2023
- tags: { deviceId },
2024
- meta: {
2025
- instanceId: instance.id,
2026
- profile: offer.profile,
2027
- missingSources: [...offer.missingSources]
2028
- }
2029
- });
2030
- }
2031
- this.ctx.logger.info("camera grid: published profiles", {
2007
+ if (plan.missingSources.length > 0) this.ctx.logger.info("camera grid: a source cannot be read — the grid is NOT on offer", {
2008
+ tags: { deviceId },
2009
+ meta: {
2010
+ instanceId: instance.id,
2011
+ missingSources: [...plan.missingSources]
2012
+ }
2013
+ });
2014
+ this.ctx.logger.info("camera grid: composing", {
2032
2015
  tags: { deviceId },
2033
2016
  meta: {
2034
2017
  instanceId: instance.id,
2035
- offered: offers.filter((offer) => offer.offered).map((offer) => offer.profile)
2018
+ offered: plan.offered,
2019
+ readAt: Object.fromEntries([...plan.profileBySource].map(([id, p]) => [String(id), p]))
2036
2020
  }
2037
2021
  });
2038
2022
  }
2039
- offeredProfiles(instanceId) {
2040
- const offers = this.offersByInstanceId.get(instanceId);
2041
- if (offers === void 0) return null;
2042
- return offers.filter((offer) => offer.offered).map((offer) => offer.profile);
2023
+ planFor(instanceId) {
2024
+ return this.planByInstanceId.get(instanceId) ?? null;
2043
2025
  }
2044
2026
  ensureSessions(instanceId, deviceId) {
2045
- const offered = this.offeredProfiles(instanceId) ?? [];
2046
- for (const profile of GRID_PROFILES) {
2047
- const key = sessionKey(instanceId, profile);
2048
- if (!offered.includes(profile)) {
2049
- const stale = this.sessions.get(key);
2050
- if (stale) {
2051
- this.sessions.delete(key);
2052
- stale.session.dispose();
2053
- }
2054
- continue;
2027
+ const plan = this.planFor(instanceId);
2028
+ if (plan === null || !plan.offered) {
2029
+ const stale = this.sessions.get(instanceId);
2030
+ if (stale) {
2031
+ this.sessions.delete(instanceId);
2032
+ stale.session.dispose();
2055
2033
  }
2056
- if (this.sessions.has(key)) continue;
2057
- const session = this.makeSession(instanceId, deviceId, profile);
2058
- this.sessions.set(key, new GridSessionEntry(instanceId, profile, session));
2034
+ return;
2059
2035
  }
2036
+ if (this.sessions.has(instanceId)) return;
2037
+ const session = this.makeSession(instanceId, deviceId);
2038
+ this.sessions.set(instanceId, new GridSessionEntry(instanceId, session));
2060
2039
  }
2061
- makeSession(instanceId, deviceId, profile) {
2040
+ makeSession(instanceId, deviceId) {
2062
2041
  const childContext = {
2063
2042
  binaryPath: this.ffmpegBinaryPath,
2064
2043
  logger: this.ctx.logger,
@@ -2069,6 +2048,8 @@ var CameraGridAddon = class extends BaseAddon {
2069
2048
  deps: {
2070
2049
  logger: this.ctx.logger,
2071
2050
  acquireSource: async (sourceDeviceId) => {
2051
+ const profile = this.planFor(instanceId)?.profileBySource.get(sourceDeviceId);
2052
+ if (profile === void 0) throw new Error(`camera-grid: no stream is assigned for source ${String(sourceDeviceId)}`);
2072
2053
  const dial = gridSourceDial(await acquireWithRetry(() => this.ctx.api.streamBroker.getStreamWithCodec.mutate({
2073
2054
  deviceId: sourceDeviceId,
2074
2055
  video: "copy",
@@ -2118,11 +2099,7 @@ var CameraGridAddon = class extends BaseAddon {
2118
2099
  if (!instance) return null;
2119
2100
  return gridPlanFor(instance, {
2120
2101
  encoder: "libx264",
2121
- decodeHwAccel: null,
2122
- canvas: gridCanvasFor({
2123
- width: instance.width,
2124
- height: instance.height
2125
- }, profile)
2102
+ decodeHwAccel: null
2126
2103
  });
2127
2104
  }
2128
2105
  });
@@ -2130,18 +2107,14 @@ var CameraGridAddon = class extends BaseAddon {
2130
2107
  }
2131
2108
  /** The session a dial is asking for, or `null` - which the relay answers 404 to. */
2132
2109
  sessionFor(request) {
2133
- return this.sessions.get(sessionKey(request.instanceId, request.profile))?.session ?? null;
2110
+ return this.sessions.get(request.instanceId)?.session ?? null;
2134
2111
  }
2135
- /** This grid's sessions, in DESCENDING quality - the snapshot takes the first running one. */
2112
+ /** This grid's composition, as the list the snapshot source expects. */
2136
2113
  sessionsForDevice(deviceId) {
2137
2114
  const instanceId = this.instanceIdForDevice(deviceId);
2138
2115
  if (instanceId === null) return [];
2139
- const entries = [];
2140
- for (const profile of GRID_PROFILES) {
2141
- const entry = this.sessions.get(sessionKey(instanceId, profile));
2142
- if (entry) entries.push(entry);
2143
- }
2144
- return entries;
2116
+ const entry = this.sessions.get(instanceId);
2117
+ return entry ? [entry] : [];
2145
2118
  }
2146
2119
  /** A grid that is gone stops immediately - it must not keep N sources dialled. */
2147
2120
  async retireWithdrawnSessions(instances) {
@@ -2150,10 +2123,7 @@ var CameraGridAddon = class extends BaseAddon {
2150
2123
  if (live.has(entry.instanceId)) continue;
2151
2124
  this.ctx.logger.info("camera grid: retiring a withdrawn grid", {
2152
2125
  tags: { deviceId: this.deviceIdByInstanceId.get(entry.instanceId) ?? 0 },
2153
- meta: {
2154
- instanceId: entry.instanceId,
2155
- profile: entry.profile
2156
- }
2126
+ meta: { instanceId: entry.instanceId }
2157
2127
  });
2158
2128
  this.sessions.delete(key);
2159
2129
  await entry.session.dispose();
@@ -2161,7 +2131,7 @@ var CameraGridAddon = class extends BaseAddon {
2161
2131
  for (const instanceId of [...this.deviceIdByInstanceId.keys()]) {
2162
2132
  if (live.has(instanceId)) continue;
2163
2133
  this.deviceIdByInstanceId.delete(instanceId);
2164
- this.offersByInstanceId.delete(instanceId);
2134
+ this.planByInstanceId.delete(instanceId);
2165
2135
  }
2166
2136
  }
2167
2137
  /**
@@ -2174,10 +2144,7 @@ var CameraGridAddon = class extends BaseAddon {
2174
2144
  if (!entry.session.isRunning()) continue;
2175
2145
  this.ctx.logger.info("camera grid: the layout changed - restarting the composition", {
2176
2146
  tags: { deviceId: this.deviceIdByInstanceId.get(entry.instanceId) ?? 0 },
2177
- meta: {
2178
- instanceId: entry.instanceId,
2179
- profile: entry.profile
2180
- }
2147
+ meta: { instanceId: entry.instanceId }
2181
2148
  });
2182
2149
  entry.session.onChildEnded("layout-changed");
2183
2150
  }
@@ -2212,24 +2179,15 @@ var CameraGridAddon = class extends BaseAddon {
2212
2179
  descriptorsFor(instanceId) {
2213
2180
  const instance = this.instanceById(instanceId);
2214
2181
  const relay = this.relay;
2215
- const offered = this.offeredProfiles(instanceId);
2216
- if (!instance || !relay || offered === null) return null;
2217
- return offered.map((profile) => {
2218
- const canvas = gridCanvasFor({
2219
- width: instance.width,
2220
- height: instance.height
2221
- }, profile);
2222
- return gridStreamDescriptor({
2223
- profile,
2224
- width: canvas.width,
2225
- height: canvas.height,
2226
- fps: instance.fps,
2227
- url: relay.streamUrl({
2228
- instanceId,
2229
- profile
2230
- })
2231
- });
2232
- });
2182
+ const plan = this.planFor(instanceId);
2183
+ if (!instance || !relay || plan === null) return null;
2184
+ if (!plan.offered) return [];
2185
+ return [gridStreamDescriptor({
2186
+ width: instance.width,
2187
+ height: instance.height,
2188
+ fps: instance.fps,
2189
+ url: relay.streamUrl({ instanceId })
2190
+ })];
2233
2191
  }
2234
2192
  instanceIdForDevice(deviceId) {
2235
2193
  for (const [instanceId, id] of this.deviceIdByInstanceId) if (id === deviceId) return instanceId;
@@ -2254,11 +2212,15 @@ var CameraGridAddon = class extends BaseAddon {
2254
2212
  source: cell.source,
2255
2213
  cell: cell.cell
2256
2214
  })),
2257
- profiles: (this.offersByInstanceId.get(instance.id) ?? []).map((offer) => ({
2258
- profile: offer.profile,
2259
- offered: offer.offered,
2260
- missingSources: offer.offered ? [] : [...offer.missingSources]
2261
- }))
2215
+ profiles: (() => {
2216
+ const plan = this.planByInstanceId.get(instance.id);
2217
+ if (plan === void 0) return [];
2218
+ return [{
2219
+ profile: GRID_OUTPUT_PROFILE,
2220
+ offered: plan.offered,
2221
+ missingSources: [...plan.missingSources]
2222
+ }];
2223
+ })()
2262
2224
  };
2263
2225
  }
2264
2226
  async saveGridLayout(input) {
@@ -2284,7 +2246,7 @@ var CameraGridAddon = class extends BaseAddon {
2284
2246
  ...row,
2285
2247
  ...updated
2286
2248
  } : row) });
2287
- await this.refreshProfileOffers(this.instances());
2249
+ await this.refreshSourcePlans(this.instances());
2288
2250
  this.ensureSessions(instance.id, input.deviceId);
2289
2251
  const view = this.gridViewFor(input.deviceId);
2290
2252
  if (!view) throw new Error("camera-grid: the grid vanished while it was being saved");
@@ -2350,4 +2312,4 @@ var CameraGridAddon = class extends BaseAddon {
2350
2312
  }
2351
2313
  };
2352
2314
  //#endregion
2353
- export { CameraGridAddon, CameraGridAddon as default, GRID_CAM_STREAM_ID, GRID_PROFILES, GRID_SILENCED_CAP_NAMES, GridCameraDevice, GridFrameSampler, GridLastFrameFileStore, GridSnapshotSource, GridStreamRelay, GridStreamSession, buildGridCameraDeclarations, buildGridFilterGraph, gridCamStreamId, gridCameraReconciliationIndex, gridCanvasFor, gridPlanFor, gridSnapshotAnswerFor, gridSourceDial, gridStreamDescriptor, gridStreamInvocation, installGridCameraRuntime, newGridCameraStableId, normalizeGridRows, offeredGridProfiles, parseGridStreamPath, readGridInstances, silenceAnalysisFor, startGridChild };
2315
+ export { CameraGridAddon, CameraGridAddon as default, GRID_CAM_STREAM_ID, GRID_OUTPUT_PROFILE, GRID_SILENCED_CAP_NAMES, GridCameraDevice, GridFrameSampler, GridLastFrameFileStore, GridSnapshotSource, GridStreamRelay, GridStreamSession, buildGridCameraDeclarations, buildGridFilterGraph, gridCameraReconciliationIndex, gridPlanFor, gridSnapshotAnswerFor, gridSourceDial, gridSourceProfiles, gridStreamDescriptor, gridStreamInvocation, installGridCameraRuntime, lowestServedProfile, newGridCameraStableId, normalizeGridRows, parseGridStreamPath, readGridInstances, silenceAnalysisFor, startGridChild };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.262",
3
+ "version": "1.2.263",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",