@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.
@@ -600,82 +600,50 @@ var GridLastFrameFileStore = class {
600
600
  //#endregion
601
601
  //#region src/builtins/camera-grid/grid-profiles.ts
602
602
  /**
603
- * One id, for every grid, forever — for the HIGH composition.
603
+ * One id, for every grid, forever.
604
604
  *
605
- * D519's reason still holds and is what keeps this id bare: a per-instance or
606
- * newly-spelled id mints a second cam-stream and orphans whatever profile
607
- * assignment the operator had made. `grid` was `high` on the live hub before
608
- * this change, so `high` keeps it and the other two are new.
605
+ * D519's reason is what keeps this id bare: a per-instance or newly-spelled id
606
+ * mints a second cam-stream and orphans whatever profile assignment the
607
+ * operator had made. It was `high` on the live hub before the per-profile
608
+ * detour and it is `high` again, so nothing an operator assigned is disturbed.
609
609
  */
610
610
  var GRID_CAM_STREAM_ID = "grid";
611
- /** Every profile a grid could conceivably compose, in descending quality. */
612
- var GRID_PROFILES = [
613
- "high",
614
- "mid",
615
- "low"
616
- ];
617
- /**
618
- * Canvas scale per profile. Named constants and not a computation: the numbers
619
- * are a policy (what a `mid` grid is FOR), and a policy that lives in an
620
- * expression is a policy nobody can find.
621
- */
622
- var CANVAS_SCALE = {
623
- high: 1,
624
- mid: .5,
625
- low: .25
626
- };
627
611
  /**
628
- * The smallest canvas a composition is allowed to shrink to. A grid of four
629
- * cells at 40x30 is not a cheaper picture, it is no picture.
612
+ * The slot the single composition claims.
613
+ *
614
+ * NOT decoration. A device publishing one stream with no hint is ranked by
615
+ * pixel count and lands in `mid` — the slot `recordings` does not select on its
616
+ * own, that `recordingsLow` ignores, and that scrub never reads. The Dreame
617
+ * robot's single 720p relay landed exactly there and its footage was written
618
+ * where nothing looked.
630
619
  */
631
- var MIN_CANVAS_WIDTH = 160;
632
- var MIN_CANVAS_HEIGHT = 90;
633
- /** The cam-stream id a grid publishes for one profile. */
634
- function gridCamStreamId(profile) {
635
- return profile === "high" ? GRID_CAM_STREAM_ID : `${GRID_CAM_STREAM_ID}-${profile}`;
620
+ var GRID_OUTPUT_PROFILE = "high";
621
+ /** Cheapest first — the order a source is read in. */
622
+ var CHEAPEST_FIRST = [
623
+ "low",
624
+ "mid",
625
+ "high"
626
+ ];
627
+ /** The cheapest stream this source serves, or `null` when it serves none. */
628
+ function lowestServedProfile(served) {
629
+ return CHEAPEST_FIRST.find((profile) => served.includes(profile)) ?? null;
636
630
  }
637
- /** Even, floored, scaled by the profile. libx264 refuses an odd dimension. */
638
- function gridCanvasFor(canvas, profile) {
639
- const scale = CANVAS_SCALE[profile];
631
+ /** Which stream to read each source at, and who cannot be read at all. */
632
+ function gridSourceProfiles(input) {
633
+ const profileBySource = /* @__PURE__ */ new Map();
634
+ const missingSources = [];
635
+ for (const deviceId of input.sourceDeviceIds) {
636
+ const served = input.profilesServedBy(deviceId);
637
+ const chosen = served === null ? null : lowestServedProfile(served);
638
+ if (chosen === null) missingSources.push(deviceId);
639
+ else profileBySource.set(deviceId, chosen);
640
+ }
640
641
  return {
641
- width: evenAtLeast(canvas.width * scale, MIN_CANVAS_WIDTH),
642
- height: evenAtLeast(canvas.height * scale, MIN_CANVAS_HEIGHT)
642
+ offered: input.sourceDeviceIds.length > 0 && missingSources.length === 0,
643
+ profileBySource,
644
+ missingSources
643
645
  };
644
646
  }
645
- function evenAtLeast(value, floor) {
646
- const rounded = Math.max(floor, Math.round(value));
647
- return rounded % 2 === 0 ? rounded : rounded + 1;
648
- }
649
- /**
650
- * What this grid can honestly publish, per profile, with the refusal named.
651
- *
652
- * Every profile gets a row whether it is offered or not: an absent row says
653
- * nothing, and the caller logging a refusal needs the reason more than the
654
- * caller publishing an offer needs the offer.
655
- */
656
- function offeredGridProfiles(input) {
657
- const served = /* @__PURE__ */ new Map();
658
- for (const deviceId of input.sourceDeviceIds) served.set(deviceId, input.profilesServedBy(deviceId));
659
- return GRID_PROFILES.map((profile) => {
660
- if (input.sourceDeviceIds.length === 0) return {
661
- profile,
662
- offered: false,
663
- missingSources: []
664
- };
665
- const missingSources = input.sourceDeviceIds.filter((deviceId) => {
666
- const profiles = served.get(deviceId);
667
- return profiles === null || profiles === void 0 || !profiles.includes(profile);
668
- });
669
- return missingSources.length === 0 ? {
670
- profile,
671
- offered: true
672
- } : {
673
- profile,
674
- offered: false,
675
- missingSources
676
- };
677
- });
678
- }
679
647
  //#endregion
680
648
  //#region src/builtins/camera-grid/grid-sentry-sources.ts
681
649
  function gridSourceDial(acquired) {
@@ -694,6 +662,43 @@ function gridSourceDial(acquired) {
694
662
  //#endregion
695
663
  //#region src/builtins/camera-grid/grid-snapshot.ts
696
664
  /**
665
+ * A grid's snapshot — a frame of the COMPOSITION, never a dial.
666
+ *
667
+ * ## The decision, and the two things it rejects
668
+ *
669
+ * The operator: *"Lo snapshot possiamo sempre farlo dallo stream sempre, non
670
+ * c'e' bisogno di comporre gli snapshots"*. So:
671
+ *
672
+ * - **Not composed from source snapshots.** That would be a SECOND compositor,
673
+ * with its own geometry, its own aspect handling and its own bugs, producing
674
+ * a picture that differs from the real one in ways nobody would ever notice
675
+ * until they mattered.
676
+ * - **Not produced by starting the composition.** The whole of D519 is "a grid
677
+ * costs nothing when nobody watches". A thumbnail that dials N cameras — and
678
+ * on a battery camera, wakes one — spends the entire budget of the feature on
679
+ * a 160-pixel-wide picture in a list.
680
+ *
681
+ * ## The three states, all answered
682
+ *
683
+ * | state | answer |
684
+ * | --- | --- |
685
+ * | a composition is running | one frame sampled out of the bytes it is already emitting |
686
+ * | nothing running, a frame was kept | that frame — stale, and honest about it |
687
+ * | nothing running, nothing ever kept | `null`, logged with the reason |
688
+ *
689
+ * `null` is the courtesy card, which is the correct picture of "this grid has
690
+ * never been watched". What it must never be is a reason to dial.
691
+ *
692
+ * ## Why this is a NATIVE snapshot provider
693
+ *
694
+ * The wrapper's ladder is native-first: a native that produces a frame
695
+ * short-circuits before the broker frame-grab, and the broker frame-grab is the
696
+ * thing that would wake the sources. A grid with a stored frame or a live
697
+ * composition therefore never reaches it. The residual — a grid that has never
698
+ * once been composed — is documented in D519; it cannot wake a battery camera,
699
+ * because a grid never dials one live at all (`grid-sentry-sources.ts`).
700
+ */
701
+ /**
697
702
  * Which of the three answers applies. Pure, so the rule can be read — and
698
703
  * sabotaged — without a socket, a file or an ffmpeg.
699
704
  */
@@ -726,10 +731,7 @@ var GridSnapshotSource = class {
726
731
  return toImage(sampled);
727
732
  }
728
733
  if (stored !== null) return toImage(stored);
729
- this.deps.logger.warn("camera grid: the composition is running but produced no frame, and none was kept", {
730
- tags: { deviceId },
731
- meta: { profile: live.profile }
732
- });
734
+ this.deps.logger.warn("camera grid: the composition is running but produced no frame, and none was kept", { tags: { deviceId } });
733
735
  return null;
734
736
  }
735
737
  if (answer.kind === "stored" && stored !== null) return toImage(stored);
@@ -737,7 +739,7 @@ var GridSnapshotSource = class {
737
739
  tags: { deviceId },
738
740
  meta: {
739
741
  reason: "never-composed",
740
- profiles: sessions.length
742
+ compositions: sessions.length
741
743
  }
742
744
  });
743
745
  return null;
@@ -746,18 +748,12 @@ var GridSnapshotSource = class {
746
748
  try {
747
749
  const jpeg = await this.deps.sampleFrame(session);
748
750
  if (jpeg !== null && jpeg.length > 0) return jpeg;
749
- this.deps.logger.warn("camera grid: could not sample a frame from the live composition", {
750
- tags: { deviceId },
751
- meta: { profile: session.profile }
752
- });
751
+ this.deps.logger.warn("camera grid: could not sample a frame from the live composition", { tags: { deviceId } });
753
752
  return null;
754
753
  } catch (error) {
755
754
  this.deps.logger.warn("camera grid: could not sample a frame from the live composition", {
756
755
  tags: { deviceId },
757
- meta: {
758
- profile: session.profile,
759
- error: require_dist.errMsg(error)
760
- }
756
+ meta: { error: require_dist.errMsg(error) }
761
757
  });
762
758
  return null;
763
759
  }
@@ -811,24 +807,15 @@ var PATH_PREFIX = "/grid/";
811
807
  var PATH_SUFFIX = GRID_WIRE_PATH_SUFFIX;
812
808
  /** How long a reader may refuse to drain before it is cut. */
813
809
  var SLOW_READER_MS = 5e3;
814
- function asProfile(raw) {
815
- return GRID_PROFILES.find((profile) => profile === raw) ?? null;
816
- }
817
- /** `/grid/<instanceId>/<profile>.flv`, or `null` when it is anything else. */
810
+ /** `/grid/<instanceId>.flv`, or `null` when it is anything else. */
818
811
  function parseGridStreamPath(url) {
819
812
  const path = (url ?? "").split("?")[0] ?? "";
820
813
  if (!path.startsWith(PATH_PREFIX) || !path.endsWith(PATH_SUFFIX)) return null;
821
814
  const raw = path.slice(6, path.length - PATH_SUFFIX.length);
822
- const at = raw.lastIndexOf("/");
823
- if (at <= 0) return null;
824
- const profile = asProfile(raw.slice(at + 1));
825
- if (profile === null) return null;
815
+ if (raw.length === 0 || raw.includes("/")) return null;
826
816
  try {
827
- const instanceId = decodeURIComponent(raw.slice(0, at));
828
- return instanceId.length > 0 ? {
829
- instanceId,
830
- profile
831
- } : null;
817
+ const instanceId = decodeURIComponent(raw);
818
+ return instanceId.length > 0 ? { instanceId } : null;
832
819
  } catch {
833
820
  return null;
834
821
  }
@@ -867,16 +854,13 @@ var GridStreamRelay = class {
867
854
  streamUrl(request) {
868
855
  if (!this.baseUrl) throw new Error("camera-grid: the stream relay is not started");
869
856
  const instance = encodeURIComponent(request.instanceId);
870
- return `${this.baseUrl}${PATH_PREFIX}${instance}/${request.profile}${PATH_SUFFIX}`;
857
+ return `${this.baseUrl}${PATH_PREFIX}${instance}${PATH_SUFFIX}`;
871
858
  }
872
859
  async serve(request, res) {
873
- const { instanceId, profile } = request;
860
+ const { instanceId } = request;
874
861
  const session = this.deps.sessionFor(request);
875
862
  if (session === null) {
876
- this.deps.logger.warn("camera grid: a stream was requested for an unknown grid", { meta: {
877
- instanceId,
878
- profile
879
- } });
863
+ this.deps.logger.warn("camera grid: a stream was requested for an unknown grid", { meta: { instanceId } });
880
864
  res.writeHead(404).end();
881
865
  return;
882
866
  }
@@ -910,7 +894,6 @@ var GridStreamRelay = class {
910
894
  tags: { deviceId: session.deviceId },
911
895
  meta: {
912
896
  instanceId,
913
- profile,
914
897
  stalledForMs: SLOW_READER_MS
915
898
  }
916
899
  });
@@ -923,7 +906,6 @@ var GridStreamRelay = class {
923
906
  tags: { deviceId: session.deviceId },
924
907
  meta: {
925
908
  instanceId,
926
- profile,
927
909
  reason
928
910
  }
929
911
  });
@@ -936,7 +918,6 @@ var GridStreamRelay = class {
936
918
  tags: { deviceId: session.deviceId },
937
919
  meta: {
938
920
  instanceId,
939
- profile,
940
921
  error: require_dist.errMsg(error)
941
922
  }
942
923
  });
@@ -972,13 +953,13 @@ var GridStreamRelay = class {
972
953
  * scrub never reads. The Dreame robot's one 720p relay landed exactly there and
973
954
  * its footage was written nowhere anything looked.
974
955
  *
975
- * With several compositions published the hint does a second job: it is the
976
- * only thing that says which slot each one is FOR. Ranked by pixels a grid's
977
- * `mid` would take the `high` slot on any grid whose `high` was not offered.
956
+ * So the single composition claims `high` EXPLICITLY (`GRID_OUTPUT_PROFILE`).
957
+ * It is also the slot this stream held before the per-profile detour, which is
958
+ * what keeps an operator's existing assignment intact.
978
959
  */
979
960
  function gridStreamDescriptor(input) {
980
961
  return {
981
- camStreamId: gridCamStreamId(input.profile),
962
+ camStreamId: GRID_CAM_STREAM_ID,
982
963
  kind: GRID_WIRE_STREAM_KIND,
983
964
  url: input.url,
984
965
  codec: "h264",
@@ -987,8 +968,8 @@ function gridStreamDescriptor(input) {
987
968
  height: input.height
988
969
  },
989
970
  fps: input.fps,
990
- label: `Grid composition (${input.profile})`,
991
- profileHint: input.profile,
971
+ label: "Grid composition",
972
+ profileHint: GRID_OUTPUT_PROFILE,
992
973
  autoEligible: true
993
974
  };
994
975
  }
@@ -1766,21 +1747,18 @@ var DEFAULTS = {
1766
1747
  cameraGrids: [],
1767
1748
  cameraGridTombstones: []
1768
1749
  };
1769
- /** One session per (grid, profile). A grid composes each profile separately. */
1770
- function sessionKey(instanceId, profile) {
1771
- return `${instanceId}::${profile}`;
1772
- }
1773
1750
  /**
1774
- * One grid's composition of one profile, and enough about it for the snapshot
1775
- * to choose between them without reaching into the session.
1751
+ * One grid's composition — there is exactly one, keyed by the grid itself.
1752
+ *
1753
+ * It used to be one per profile, and the key carried the profile. That shape
1754
+ * is what made a viewer's adaptive step tear a composition down and build
1755
+ * another; see `grid-profiles.ts` for the log that showed it.
1776
1756
  */
1777
1757
  var GridSessionEntry = class {
1778
1758
  instanceId;
1779
- profile;
1780
1759
  session;
1781
- constructor(instanceId, profile, session) {
1760
+ constructor(instanceId, session) {
1782
1761
  this.instanceId = instanceId;
1783
- this.profile = profile;
1784
1762
  this.session = session;
1785
1763
  }
1786
1764
  isRunning() {
@@ -1790,7 +1768,7 @@ var GridSessionEntry = class {
1790
1768
  var CameraGridAddon = class extends require_dist.BaseAddon {
1791
1769
  relay = null;
1792
1770
  reconcileTimer = null;
1793
- /** Keyed by {@link sessionKey} — one composition per grid per profile. */
1771
+ /** Keyed by instance id — ONE composition per grid. */
1794
1772
  sessions = /* @__PURE__ */ new Map();
1795
1773
  deviceIdByInstanceId = /* @__PURE__ */ new Map();
1796
1774
  tombstones = /* @__PURE__ */ new Set();
@@ -1804,7 +1782,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
1804
1782
  * (D224), and 60 s is the window for a fact — "does 615 have a low assigned"
1805
1783
  * — that only changes when an operator changes it.
1806
1784
  */
1807
- offersByInstanceId = /* @__PURE__ */ new Map();
1785
+ planByInstanceId = /* @__PURE__ */ new Map();
1808
1786
  snapshots = null;
1809
1787
  frameSampler = null;
1810
1788
  ffmpegBinaryPath = "ffmpeg";
@@ -1907,7 +1885,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
1907
1885
  if (!this.relay) return;
1908
1886
  const instances = this.instances();
1909
1887
  const declarations = buildGridCameraDeclarations(instances);
1910
- await this.refreshProfileOffers(instances);
1888
+ await this.refreshSourcePlans(instances);
1911
1889
  await this.retireWithdrawnSessions(instances);
1912
1890
  const result = await new require_dist.DeclaredDevices({
1913
1891
  logger: this.ctx.logger.child("declaration"),
@@ -1974,15 +1952,15 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
1974
1952
  }
1975
1953
  }
1976
1954
  /**
1977
- * Which profiles each grid can honestly publish, asked of the BROKER.
1955
+ * Which stream each grid reads each of its sources at, asked of the BROKER.
1978
1956
  *
1979
1957
  * `listAllProfileSlots` is the one authority on "does 615 have a low
1980
1958
  * assigned", and `sourceCamStreamId === null` is its way of saying no. A read
1981
- * that FAILS leaves the previous answer in place and says so - it is not an
1959
+ * that FAILS leaves the previous answer in place and says so — it is not an
1982
1960
  * empty catalog, and turning it into one would retract the streams of every
1983
1961
  * grid on the hub.
1984
1962
  */
1985
- async refreshProfileOffers(instances) {
1963
+ async refreshSourcePlans(instances) {
1986
1964
  let slots;
1987
1965
  try {
1988
1966
  slots = await this.ctx.api.streamBroker.listAllProfileSlots.query();
@@ -2001,69 +1979,70 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
2001
1979
  servedBy.set(slot.deviceId, profiles);
2002
1980
  }
2003
1981
  for (const instance of instances) {
2004
- const offers = offeredGridProfiles({
1982
+ const plan = gridSourceProfiles({
2005
1983
  sourceDeviceIds: [...new Set(instance.cells.map((cell) => cell.deviceId))],
2006
1984
  profilesServedBy: (deviceId) => servedBy.get(deviceId) ?? null
2007
1985
  });
2008
- const previous = this.offersByInstanceId.get(instance.id);
2009
- this.offersByInstanceId.set(instance.id, offers);
2010
- this.logRefusals(instance, offers, previous);
1986
+ const previous = this.planByInstanceId.get(instance.id);
1987
+ this.planByInstanceId.set(instance.id, plan);
1988
+ this.logSourcePlan(instance, plan, previous);
2011
1989
  }
2012
1990
  }
2013
1991
  /**
2014
- * Say which profiles this grid will NOT publish, and why - once per change.
1992
+ * Say WHICH stream each cell is read at, and who could not be read - once per
1993
+ * change.
2015
1994
  *
2016
- * A profile silently missing from a catalog is indistinguishable from a
2017
- * broker that swept it. Logged on the EDGE rather than every minute: a grid
2018
- * that can only serve `high` would otherwise write two lines a minute for
2019
- * ever.
1995
+ * Both halves earn their line. A grid silently missing from a catalog is
1996
+ * indistinguishable from a broker that swept it, so the refusal names the
1997
+ * sources. And the chosen profiles are the answer to "why is this tile
1998
+ * soft?" — a camera whose only assigned slot is `high` is read at `high`, and
1999
+ * nothing else in the system would ever say so.
2000
+ *
2001
+ * Logged on the EDGE: a grid that cannot be composed would otherwise write a
2002
+ * line a minute for ever.
2020
2003
  */
2021
- logRefusals(instance, offers, previous) {
2022
- if (previous !== void 0 && JSON.stringify(previous) === JSON.stringify(offers)) return;
2004
+ logSourcePlan(instance, plan, previous) {
2005
+ const shape = (value) => JSON.stringify([
2006
+ value.offered,
2007
+ [...value.profileBySource],
2008
+ value.missingSources
2009
+ ]);
2010
+ if (previous !== void 0 && shape(previous) === shape(plan)) return;
2023
2011
  const deviceId = this.deviceIdByInstanceId.get(instance.id) ?? 0;
2024
- for (const offer of offers) {
2025
- if (offer.offered) continue;
2026
- if (offer.missingSources.length === 0) continue;
2027
- this.ctx.logger.info("camera grid: a profile is NOT on offer", {
2028
- tags: { deviceId },
2029
- meta: {
2030
- instanceId: instance.id,
2031
- profile: offer.profile,
2032
- missingSources: [...offer.missingSources]
2033
- }
2034
- });
2035
- }
2036
- this.ctx.logger.info("camera grid: published profiles", {
2012
+ if (plan.missingSources.length > 0) this.ctx.logger.info("camera grid: a source cannot be read — the grid is NOT on offer", {
2013
+ tags: { deviceId },
2014
+ meta: {
2015
+ instanceId: instance.id,
2016
+ missingSources: [...plan.missingSources]
2017
+ }
2018
+ });
2019
+ this.ctx.logger.info("camera grid: composing", {
2037
2020
  tags: { deviceId },
2038
2021
  meta: {
2039
2022
  instanceId: instance.id,
2040
- offered: offers.filter((offer) => offer.offered).map((offer) => offer.profile)
2023
+ offered: plan.offered,
2024
+ readAt: Object.fromEntries([...plan.profileBySource].map(([id, p]) => [String(id), p]))
2041
2025
  }
2042
2026
  });
2043
2027
  }
2044
- offeredProfiles(instanceId) {
2045
- const offers = this.offersByInstanceId.get(instanceId);
2046
- if (offers === void 0) return null;
2047
- return offers.filter((offer) => offer.offered).map((offer) => offer.profile);
2028
+ planFor(instanceId) {
2029
+ return this.planByInstanceId.get(instanceId) ?? null;
2048
2030
  }
2049
2031
  ensureSessions(instanceId, deviceId) {
2050
- const offered = this.offeredProfiles(instanceId) ?? [];
2051
- for (const profile of GRID_PROFILES) {
2052
- const key = sessionKey(instanceId, profile);
2053
- if (!offered.includes(profile)) {
2054
- const stale = this.sessions.get(key);
2055
- if (stale) {
2056
- this.sessions.delete(key);
2057
- stale.session.dispose();
2058
- }
2059
- continue;
2032
+ const plan = this.planFor(instanceId);
2033
+ if (plan === null || !plan.offered) {
2034
+ const stale = this.sessions.get(instanceId);
2035
+ if (stale) {
2036
+ this.sessions.delete(instanceId);
2037
+ stale.session.dispose();
2060
2038
  }
2061
- if (this.sessions.has(key)) continue;
2062
- const session = this.makeSession(instanceId, deviceId, profile);
2063
- this.sessions.set(key, new GridSessionEntry(instanceId, profile, session));
2039
+ return;
2064
2040
  }
2041
+ if (this.sessions.has(instanceId)) return;
2042
+ const session = this.makeSession(instanceId, deviceId);
2043
+ this.sessions.set(instanceId, new GridSessionEntry(instanceId, session));
2065
2044
  }
2066
- makeSession(instanceId, deviceId, profile) {
2045
+ makeSession(instanceId, deviceId) {
2067
2046
  const childContext = {
2068
2047
  binaryPath: this.ffmpegBinaryPath,
2069
2048
  logger: this.ctx.logger,
@@ -2074,6 +2053,8 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
2074
2053
  deps: {
2075
2054
  logger: this.ctx.logger,
2076
2055
  acquireSource: async (sourceDeviceId) => {
2056
+ const profile = this.planFor(instanceId)?.profileBySource.get(sourceDeviceId);
2057
+ if (profile === void 0) throw new Error(`camera-grid: no stream is assigned for source ${String(sourceDeviceId)}`);
2077
2058
  const dial = gridSourceDial(await acquireWithRetry(() => this.ctx.api.streamBroker.getStreamWithCodec.mutate({
2078
2059
  deviceId: sourceDeviceId,
2079
2060
  video: "copy",
@@ -2123,11 +2104,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
2123
2104
  if (!instance) return null;
2124
2105
  return gridPlanFor(instance, {
2125
2106
  encoder: "libx264",
2126
- decodeHwAccel: null,
2127
- canvas: gridCanvasFor({
2128
- width: instance.width,
2129
- height: instance.height
2130
- }, profile)
2107
+ decodeHwAccel: null
2131
2108
  });
2132
2109
  }
2133
2110
  });
@@ -2135,18 +2112,14 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
2135
2112
  }
2136
2113
  /** The session a dial is asking for, or `null` - which the relay answers 404 to. */
2137
2114
  sessionFor(request) {
2138
- return this.sessions.get(sessionKey(request.instanceId, request.profile))?.session ?? null;
2115
+ return this.sessions.get(request.instanceId)?.session ?? null;
2139
2116
  }
2140
- /** This grid's sessions, in DESCENDING quality - the snapshot takes the first running one. */
2117
+ /** This grid's composition, as the list the snapshot source expects. */
2141
2118
  sessionsForDevice(deviceId) {
2142
2119
  const instanceId = this.instanceIdForDevice(deviceId);
2143
2120
  if (instanceId === null) return [];
2144
- const entries = [];
2145
- for (const profile of GRID_PROFILES) {
2146
- const entry = this.sessions.get(sessionKey(instanceId, profile));
2147
- if (entry) entries.push(entry);
2148
- }
2149
- return entries;
2121
+ const entry = this.sessions.get(instanceId);
2122
+ return entry ? [entry] : [];
2150
2123
  }
2151
2124
  /** A grid that is gone stops immediately - it must not keep N sources dialled. */
2152
2125
  async retireWithdrawnSessions(instances) {
@@ -2155,10 +2128,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
2155
2128
  if (live.has(entry.instanceId)) continue;
2156
2129
  this.ctx.logger.info("camera grid: retiring a withdrawn grid", {
2157
2130
  tags: { deviceId: this.deviceIdByInstanceId.get(entry.instanceId) ?? 0 },
2158
- meta: {
2159
- instanceId: entry.instanceId,
2160
- profile: entry.profile
2161
- }
2131
+ meta: { instanceId: entry.instanceId }
2162
2132
  });
2163
2133
  this.sessions.delete(key);
2164
2134
  await entry.session.dispose();
@@ -2166,7 +2136,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
2166
2136
  for (const instanceId of [...this.deviceIdByInstanceId.keys()]) {
2167
2137
  if (live.has(instanceId)) continue;
2168
2138
  this.deviceIdByInstanceId.delete(instanceId);
2169
- this.offersByInstanceId.delete(instanceId);
2139
+ this.planByInstanceId.delete(instanceId);
2170
2140
  }
2171
2141
  }
2172
2142
  /**
@@ -2179,10 +2149,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
2179
2149
  if (!entry.session.isRunning()) continue;
2180
2150
  this.ctx.logger.info("camera grid: the layout changed - restarting the composition", {
2181
2151
  tags: { deviceId: this.deviceIdByInstanceId.get(entry.instanceId) ?? 0 },
2182
- meta: {
2183
- instanceId: entry.instanceId,
2184
- profile: entry.profile
2185
- }
2152
+ meta: { instanceId: entry.instanceId }
2186
2153
  });
2187
2154
  entry.session.onChildEnded("layout-changed");
2188
2155
  }
@@ -2217,24 +2184,15 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
2217
2184
  descriptorsFor(instanceId) {
2218
2185
  const instance = this.instanceById(instanceId);
2219
2186
  const relay = this.relay;
2220
- const offered = this.offeredProfiles(instanceId);
2221
- if (!instance || !relay || offered === null) return null;
2222
- return offered.map((profile) => {
2223
- const canvas = gridCanvasFor({
2224
- width: instance.width,
2225
- height: instance.height
2226
- }, profile);
2227
- return gridStreamDescriptor({
2228
- profile,
2229
- width: canvas.width,
2230
- height: canvas.height,
2231
- fps: instance.fps,
2232
- url: relay.streamUrl({
2233
- instanceId,
2234
- profile
2235
- })
2236
- });
2237
- });
2187
+ const plan = this.planFor(instanceId);
2188
+ if (!instance || !relay || plan === null) return null;
2189
+ if (!plan.offered) return [];
2190
+ return [gridStreamDescriptor({
2191
+ width: instance.width,
2192
+ height: instance.height,
2193
+ fps: instance.fps,
2194
+ url: relay.streamUrl({ instanceId })
2195
+ })];
2238
2196
  }
2239
2197
  instanceIdForDevice(deviceId) {
2240
2198
  for (const [instanceId, id] of this.deviceIdByInstanceId) if (id === deviceId) return instanceId;
@@ -2259,11 +2217,15 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
2259
2217
  source: cell.source,
2260
2218
  cell: cell.cell
2261
2219
  })),
2262
- profiles: (this.offersByInstanceId.get(instance.id) ?? []).map((offer) => ({
2263
- profile: offer.profile,
2264
- offered: offer.offered,
2265
- missingSources: offer.offered ? [] : [...offer.missingSources]
2266
- }))
2220
+ profiles: (() => {
2221
+ const plan = this.planByInstanceId.get(instance.id);
2222
+ if (plan === void 0) return [];
2223
+ return [{
2224
+ profile: GRID_OUTPUT_PROFILE,
2225
+ offered: plan.offered,
2226
+ missingSources: [...plan.missingSources]
2227
+ }];
2228
+ })()
2267
2229
  };
2268
2230
  }
2269
2231
  async saveGridLayout(input) {
@@ -2289,7 +2251,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
2289
2251
  ...row,
2290
2252
  ...updated
2291
2253
  } : row) });
2292
- await this.refreshProfileOffers(this.instances());
2254
+ await this.refreshSourcePlans(this.instances());
2293
2255
  this.ensureSessions(instance.id, input.deviceId);
2294
2256
  const view = this.gridViewFor(input.deviceId);
2295
2257
  if (!view) throw new Error("camera-grid: the grid vanished while it was being saved");
@@ -2357,7 +2319,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
2357
2319
  //#endregion
2358
2320
  exports.CameraGridAddon = CameraGridAddon;
2359
2321
  exports.GRID_CAM_STREAM_ID = GRID_CAM_STREAM_ID;
2360
- exports.GRID_PROFILES = GRID_PROFILES;
2322
+ exports.GRID_OUTPUT_PROFILE = GRID_OUTPUT_PROFILE;
2361
2323
  exports.GRID_SILENCED_CAP_NAMES = GRID_SILENCED_CAP_NAMES;
2362
2324
  exports.GridCameraDevice = GridCameraDevice;
2363
2325
  exports.GridFrameSampler = GridFrameSampler;
@@ -2368,18 +2330,17 @@ exports.GridStreamSession = GridStreamSession;
2368
2330
  exports.buildGridCameraDeclarations = buildGridCameraDeclarations;
2369
2331
  exports.buildGridFilterGraph = buildGridFilterGraph;
2370
2332
  exports.default = CameraGridAddon;
2371
- exports.gridCamStreamId = gridCamStreamId;
2372
2333
  exports.gridCameraReconciliationIndex = gridCameraReconciliationIndex;
2373
- exports.gridCanvasFor = gridCanvasFor;
2374
2334
  exports.gridPlanFor = gridPlanFor;
2375
2335
  exports.gridSnapshotAnswerFor = gridSnapshotAnswerFor;
2376
2336
  exports.gridSourceDial = gridSourceDial;
2337
+ exports.gridSourceProfiles = gridSourceProfiles;
2377
2338
  exports.gridStreamDescriptor = gridStreamDescriptor;
2378
2339
  exports.gridStreamInvocation = gridStreamInvocation;
2379
2340
  exports.installGridCameraRuntime = installGridCameraRuntime;
2341
+ exports.lowestServedProfile = lowestServedProfile;
2380
2342
  exports.newGridCameraStableId = newGridCameraStableId;
2381
2343
  exports.normalizeGridRows = normalizeGridRows;
2382
- exports.offeredGridProfiles = offeredGridProfiles;
2383
2344
  exports.parseGridStreamPath = parseGridStreamPath;
2384
2345
  exports.readGridInstances = readGridInstances;
2385
2346
  exports.silenceAnalysisFor = silenceAnalysisFor;