@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
|
-
|
|
25
|
-
|
|
40
|
+
readonly canvas: {
|
|
41
|
+
readonly width: number;
|
|
42
|
+
readonly height: number;
|
|
43
|
+
};
|
|
44
|
+
readonly cells: readonly GridSourceCell[];
|
|
26
45
|
/**
|
|
27
|
-
* Which
|
|
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
|
|
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
|
|
1004
|
+
for (const [deviceId, need] of neededBySource) {
|
|
964
1005
|
const served = input.profilesServedBy(deviceId);
|
|
965
|
-
const chosen = served === null ? null :
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
|
999
|
+
for (const [deviceId, need] of neededBySource) {
|
|
959
1000
|
const served = input.profilesServedBy(deviceId);
|
|
960
|
-
const chosen = served === null ? null :
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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);
|