@camstack/system 1.2.266 → 1.2.267

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.
@@ -1,4 +1,5 @@
1
1
  import { CamProfile } from '@camstack/types';
2
+ import { NormalizedRect } from './grid-filter-graph.js';
2
3
  /**
3
4
  * One id, for every grid, forever.
4
5
  *
@@ -20,15 +21,33 @@ export declare const GRID_CAM_STREAM_ID = "grid";
20
21
  export declare const GRID_OUTPUT_PROFILE: CamProfile;
21
22
  /** The cheapest stream this source serves, or `null` when it serves none. */
22
23
  export declare function lowestServedProfile(served: readonly CamProfile[]): CamProfile | null;
24
+ /** One stream a camera serves, with the size that decides whether it fits. */
25
+ export interface ServedProfile {
26
+ readonly profile: CamProfile;
27
+ /** `0` means the broker stated no size — see `gridSourceProfiles`. */
28
+ readonly width: number;
29
+ readonly height: number;
30
+ }
31
+ /** One cell, as the two rectangles that decide what its source must carry. */
32
+ export interface GridSourceCell {
33
+ readonly deviceId: number;
34
+ /** The part of the source it shows, normalized against the SOURCE. */
35
+ readonly source: NormalizedRect;
36
+ /** Where it lands, normalized against the CANVAS. */
37
+ readonly cell: NormalizedRect;
38
+ }
23
39
  export interface GridSourceProfilesInput {
24
- /** Every camera the composition takes a cell from, deduplicated. */
25
- readonly sourceDeviceIds: readonly number[];
40
+ readonly canvas: {
41
+ readonly width: number;
42
+ readonly height: number;
43
+ };
44
+ readonly cells: readonly GridSourceCell[];
26
45
  /**
27
- * Which profiles this source can serve, or `null` when nobody could say.
46
+ * Which streams this source can serve, or `null` when nobody could say.
28
47
  * `null` is not an empty list: an unanswerable question is a refusal, and
29
48
  * saying so separately keeps "we did not ask" out of the catalog.
30
49
  */
31
- readonly profilesServedBy: (deviceId: number) => readonly CamProfile[] | null;
50
+ readonly profilesServedBy: (deviceId: number) => readonly ServedProfile[] | null;
32
51
  }
33
52
  export interface GridSourcePlan {
34
53
  /** True when every source can be read — the only case a grid is on offer. */
@@ -956,18 +956,59 @@ var CHEAPEST_FIRST = [
956
956
  function lowestServedProfile(served) {
957
957
  return CHEAPEST_FIRST.find((profile) => served.includes(profile)) ?? null;
958
958
  }
959
+ /** The pixels a cell needs BEHIND it, before the crop takes its share. */
960
+ function requiredSourceSize(cell, canvas) {
961
+ const tileWidth = cell.cell.width * canvas.width;
962
+ const tileHeight = cell.cell.height * canvas.height;
963
+ const cropWidth = cell.source.width > 0 ? cell.source.width : 1;
964
+ const cropHeight = cell.source.height > 0 ? cell.source.height : 1;
965
+ return {
966
+ width: tileWidth / cropWidth,
967
+ height: tileHeight / cropHeight
968
+ };
969
+ }
970
+ /**
971
+ * The cheapest stream that covers `need`, or the BIGGEST when none does.
972
+ *
973
+ * Soft beats absent: a camera whose best stream cannot fill its cell still
974
+ * composes, at the best it has. Refusing would take the whole grid down over
975
+ * one cell being drawn larger than its source.
976
+ *
977
+ * A stream that states NO size cannot be compared, so it is ordered as cheapest
978
+ * rather than discarded — an unmeasurable stream is still a stream, and the
979
+ * picture is what the operator asked for.
980
+ */
981
+ function bestFit(served, need) {
982
+ if (served.length === 0) return null;
983
+ const order = new Map(CHEAPEST_FIRST.map((profile, index) => [profile, index]));
984
+ const cheapestFirst = [...served].sort((a, b) => (order.get(a.profile) ?? 0) - (order.get(b.profile) ?? 0));
985
+ const unmeasurable = cheapestFirst.find((s) => s.width <= 0 || s.height <= 0);
986
+ if (unmeasurable !== void 0) return unmeasurable.profile;
987
+ const covering = cheapestFirst.find((s) => s.width >= need.width && s.height >= need.height);
988
+ if (covering !== void 0) return covering.profile;
989
+ return cheapestFirst.reduce((best, s) => s.width * s.height > best.width * best.height ? s : best).profile;
990
+ }
959
991
  /** Which stream to read each source at, and who cannot be read at all. */
960
992
  function gridSourceProfiles(input) {
993
+ const neededBySource = /* @__PURE__ */ new Map();
994
+ for (const cell of input.cells) {
995
+ const need = requiredSourceSize(cell, input.canvas);
996
+ const current = neededBySource.get(cell.deviceId);
997
+ neededBySource.set(cell.deviceId, {
998
+ width: Math.max(current?.width ?? 0, need.width),
999
+ height: Math.max(current?.height ?? 0, need.height)
1000
+ });
1001
+ }
961
1002
  const profileBySource = /* @__PURE__ */ new Map();
962
1003
  const missingSources = [];
963
- for (const deviceId of input.sourceDeviceIds) {
1004
+ for (const [deviceId, need] of neededBySource) {
964
1005
  const served = input.profilesServedBy(deviceId);
965
- const chosen = served === null ? null : lowestServedProfile(served);
1006
+ const chosen = served === null ? null : bestFit(served, need);
966
1007
  if (chosen === null) missingSources.push(deviceId);
967
1008
  else profileBySource.set(deviceId, chosen);
968
1009
  }
969
1010
  return {
970
- offered: input.sourceDeviceIds.length > 0 && missingSources.length === 0,
1011
+ offered: neededBySource.size > 0 && missingSources.length === 0,
971
1012
  profileBySource,
972
1013
  missingSources
973
1014
  };
@@ -2688,12 +2729,24 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
2688
2729
  for (const slot of slots) {
2689
2730
  if (slot.sourceCamStreamId === null) continue;
2690
2731
  const profiles = servedBy.get(slot.deviceId) ?? [];
2691
- profiles.push(slot.profile);
2732
+ profiles.push({
2733
+ profile: slot.profile,
2734
+ width: slot.resolution?.width ?? 0,
2735
+ height: slot.resolution?.height ?? 0
2736
+ });
2692
2737
  servedBy.set(slot.deviceId, profiles);
2693
2738
  }
2694
2739
  for (const instance of instances) {
2695
2740
  const plan = gridSourceProfiles({
2696
- sourceDeviceIds: [...new Set(instance.cells.map((cell) => cell.deviceId))],
2741
+ canvas: {
2742
+ width: instance.width,
2743
+ height: instance.height
2744
+ },
2745
+ cells: instance.cells.map((cell) => ({
2746
+ deviceId: cell.deviceId,
2747
+ source: cell.source,
2748
+ cell: cell.cell
2749
+ })),
2697
2750
  profilesServedBy: (deviceId) => servedBy.get(deviceId) ?? null
2698
2751
  });
2699
2752
  const previous = this.planByInstanceId.get(instance.id);
@@ -951,18 +951,59 @@ var CHEAPEST_FIRST = [
951
951
  function lowestServedProfile(served) {
952
952
  return CHEAPEST_FIRST.find((profile) => served.includes(profile)) ?? null;
953
953
  }
954
+ /** The pixels a cell needs BEHIND it, before the crop takes its share. */
955
+ function requiredSourceSize(cell, canvas) {
956
+ const tileWidth = cell.cell.width * canvas.width;
957
+ const tileHeight = cell.cell.height * canvas.height;
958
+ const cropWidth = cell.source.width > 0 ? cell.source.width : 1;
959
+ const cropHeight = cell.source.height > 0 ? cell.source.height : 1;
960
+ return {
961
+ width: tileWidth / cropWidth,
962
+ height: tileHeight / cropHeight
963
+ };
964
+ }
965
+ /**
966
+ * The cheapest stream that covers `need`, or the BIGGEST when none does.
967
+ *
968
+ * Soft beats absent: a camera whose best stream cannot fill its cell still
969
+ * composes, at the best it has. Refusing would take the whole grid down over
970
+ * one cell being drawn larger than its source.
971
+ *
972
+ * A stream that states NO size cannot be compared, so it is ordered as cheapest
973
+ * rather than discarded — an unmeasurable stream is still a stream, and the
974
+ * picture is what the operator asked for.
975
+ */
976
+ function bestFit(served, need) {
977
+ if (served.length === 0) return null;
978
+ const order = new Map(CHEAPEST_FIRST.map((profile, index) => [profile, index]));
979
+ const cheapestFirst = [...served].sort((a, b) => (order.get(a.profile) ?? 0) - (order.get(b.profile) ?? 0));
980
+ const unmeasurable = cheapestFirst.find((s) => s.width <= 0 || s.height <= 0);
981
+ if (unmeasurable !== void 0) return unmeasurable.profile;
982
+ const covering = cheapestFirst.find((s) => s.width >= need.width && s.height >= need.height);
983
+ if (covering !== void 0) return covering.profile;
984
+ return cheapestFirst.reduce((best, s) => s.width * s.height > best.width * best.height ? s : best).profile;
985
+ }
954
986
  /** Which stream to read each source at, and who cannot be read at all. */
955
987
  function gridSourceProfiles(input) {
988
+ const neededBySource = /* @__PURE__ */ new Map();
989
+ for (const cell of input.cells) {
990
+ const need = requiredSourceSize(cell, input.canvas);
991
+ const current = neededBySource.get(cell.deviceId);
992
+ neededBySource.set(cell.deviceId, {
993
+ width: Math.max(current?.width ?? 0, need.width),
994
+ height: Math.max(current?.height ?? 0, need.height)
995
+ });
996
+ }
956
997
  const profileBySource = /* @__PURE__ */ new Map();
957
998
  const missingSources = [];
958
- for (const deviceId of input.sourceDeviceIds) {
999
+ for (const [deviceId, need] of neededBySource) {
959
1000
  const served = input.profilesServedBy(deviceId);
960
- const chosen = served === null ? null : lowestServedProfile(served);
1001
+ const chosen = served === null ? null : bestFit(served, need);
961
1002
  if (chosen === null) missingSources.push(deviceId);
962
1003
  else profileBySource.set(deviceId, chosen);
963
1004
  }
964
1005
  return {
965
- offered: input.sourceDeviceIds.length > 0 && missingSources.length === 0,
1006
+ offered: neededBySource.size > 0 && missingSources.length === 0,
966
1007
  profileBySource,
967
1008
  missingSources
968
1009
  };
@@ -2683,12 +2724,24 @@ var CameraGridAddon = class extends BaseAddon {
2683
2724
  for (const slot of slots) {
2684
2725
  if (slot.sourceCamStreamId === null) continue;
2685
2726
  const profiles = servedBy.get(slot.deviceId) ?? [];
2686
- profiles.push(slot.profile);
2727
+ profiles.push({
2728
+ profile: slot.profile,
2729
+ width: slot.resolution?.width ?? 0,
2730
+ height: slot.resolution?.height ?? 0
2731
+ });
2687
2732
  servedBy.set(slot.deviceId, profiles);
2688
2733
  }
2689
2734
  for (const instance of instances) {
2690
2735
  const plan = gridSourceProfiles({
2691
- sourceDeviceIds: [...new Set(instance.cells.map((cell) => cell.deviceId))],
2736
+ canvas: {
2737
+ width: instance.width,
2738
+ height: instance.height
2739
+ },
2740
+ cells: instance.cells.map((cell) => ({
2741
+ deviceId: cell.deviceId,
2742
+ source: cell.source,
2743
+ cell: cell.cell
2744
+ })),
2692
2745
  profilesServedBy: (deviceId) => servedBy.get(deviceId) ?? null
2693
2746
  });
2694
2747
  const previous = this.planByInstanceId.get(instance.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.266",
3
+ "version": "1.2.267",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",