@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.
- package/dist/builtins/camera-grid/addon.d.ts +18 -13
- package/dist/builtins/camera-grid/grid-profiles.d.ts +28 -33
- package/dist/builtins/camera-grid/grid-snapshot.d.ts +4 -4
- package/dist/builtins/camera-grid/grid-stream-descriptor.d.ts +4 -6
- package/dist/builtins/camera-grid/grid-stream-relay.d.ts +7 -11
- package/dist/builtins/camera-grid/index.d.ts +2 -2
- package/dist/builtins/camera-grid/index.js +177 -216
- package/dist/builtins/camera-grid/index.mjs +175 -213
- package/package.json +1 -1
|
@@ -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
|
|
598
|
+
* One id, for every grid, forever.
|
|
599
599
|
*
|
|
600
|
-
* D519's reason
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
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
|
|
624
|
-
*
|
|
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
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
/**
|
|
633
|
-
function
|
|
634
|
-
const
|
|
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
|
-
|
|
637
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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}
|
|
852
|
+
return `${this.baseUrl}${PATH_PREFIX}${instance}${PATH_SUFFIX}`;
|
|
866
853
|
}
|
|
867
854
|
async serve(request, res) {
|
|
868
|
-
const { instanceId
|
|
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
|
-
*
|
|
971
|
-
*
|
|
972
|
-
*
|
|
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:
|
|
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:
|
|
986
|
-
profileHint:
|
|
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
|
|
1770
|
-
*
|
|
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,
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
2004
|
-
this.
|
|
2005
|
-
this.
|
|
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
|
|
1987
|
+
* Say WHICH stream each cell is read at, and who could not be read - once per
|
|
1988
|
+
* change.
|
|
2010
1989
|
*
|
|
2011
|
-
* A
|
|
2012
|
-
* broker that swept it
|
|
2013
|
-
*
|
|
2014
|
-
*
|
|
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
|
-
|
|
2017
|
-
|
|
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
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
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:
|
|
2018
|
+
offered: plan.offered,
|
|
2019
|
+
readAt: Object.fromEntries([...plan.profileBySource].map(([id, p]) => [String(id), p]))
|
|
2036
2020
|
}
|
|
2037
2021
|
});
|
|
2038
2022
|
}
|
|
2039
|
-
|
|
2040
|
-
|
|
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
|
|
2046
|
-
|
|
2047
|
-
const
|
|
2048
|
-
if (
|
|
2049
|
-
|
|
2050
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
2110
|
+
return this.sessions.get(request.instanceId)?.session ?? null;
|
|
2134
2111
|
}
|
|
2135
|
-
/** This grid's
|
|
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
|
|
2140
|
-
|
|
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.
|
|
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
|
|
2216
|
-
if (!instance || !relay ||
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
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: (
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
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.
|
|
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,
|
|
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 };
|