@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
|
@@ -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
|
|
603
|
+
* One id, for every grid, forever.
|
|
604
604
|
*
|
|
605
|
-
* D519's reason
|
|
606
|
-
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
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
|
|
629
|
-
*
|
|
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
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
/**
|
|
638
|
-
function
|
|
639
|
-
const
|
|
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
|
-
|
|
642
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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}
|
|
857
|
+
return `${this.baseUrl}${PATH_PREFIX}${instance}${PATH_SUFFIX}`;
|
|
871
858
|
}
|
|
872
859
|
async serve(request, res) {
|
|
873
|
-
const { instanceId
|
|
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
|
-
*
|
|
976
|
-
*
|
|
977
|
-
*
|
|
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:
|
|
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:
|
|
991
|
-
profileHint:
|
|
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
|
|
1775
|
-
*
|
|
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,
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
2009
|
-
this.
|
|
2010
|
-
this.
|
|
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
|
|
1992
|
+
* Say WHICH stream each cell is read at, and who could not be read - once per
|
|
1993
|
+
* change.
|
|
2015
1994
|
*
|
|
2016
|
-
* A
|
|
2017
|
-
* broker that swept it
|
|
2018
|
-
*
|
|
2019
|
-
*
|
|
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
|
-
|
|
2022
|
-
|
|
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
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
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:
|
|
2023
|
+
offered: plan.offered,
|
|
2024
|
+
readAt: Object.fromEntries([...plan.profileBySource].map(([id, p]) => [String(id), p]))
|
|
2041
2025
|
}
|
|
2042
2026
|
});
|
|
2043
2027
|
}
|
|
2044
|
-
|
|
2045
|
-
|
|
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
|
|
2051
|
-
|
|
2052
|
-
const
|
|
2053
|
-
if (
|
|
2054
|
-
|
|
2055
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
2115
|
+
return this.sessions.get(request.instanceId)?.session ?? null;
|
|
2139
2116
|
}
|
|
2140
|
-
/** This grid's
|
|
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
|
|
2145
|
-
|
|
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.
|
|
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
|
|
2221
|
-
if (!instance || !relay ||
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
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: (
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
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.
|
|
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.
|
|
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;
|