@camstack/system 1.2.261 → 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 +227 -224
- package/dist/builtins/camera-grid/index.mjs +225 -221
- 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
|
}
|
|
@@ -1582,16 +1563,58 @@ var PIXEL_FORMAT = "yuv420p";
|
|
|
1582
1563
|
*/
|
|
1583
1564
|
var GOP_SECONDS = 1;
|
|
1584
1565
|
/**
|
|
1585
|
-
* `-analyzeduration` / `-probesize`,
|
|
1566
|
+
* `-analyzeduration` / `-probesize`, as small as ffmpeg allows.
|
|
1586
1567
|
*
|
|
1587
|
-
*
|
|
1588
|
-
*
|
|
1589
|
-
*
|
|
1590
|
-
*
|
|
1591
|
-
*
|
|
1568
|
+
* ## The probes are SEQUENTIAL, and this is what the tile skew was
|
|
1569
|
+
*
|
|
1570
|
+
* The sentence that used to be here said the probes are "PARALLEL inputs of one
|
|
1571
|
+
* child". They are not. ffmpeg opens its inputs one after another and each open
|
|
1572
|
+
* BLOCKS in `avformat_find_stream_info` until its budget is spent — so input 0
|
|
1573
|
+
* is already connected and receiving while ffmpeg is still opening input 1, its
|
|
1574
|
+
* frames pile up, and `overlay` (which pairs inputs BY PTS) marries input 0's
|
|
1575
|
+
* oldest frame to input 1's newest. The lag is fixed for the life of the child:
|
|
1576
|
+
* both tiles then advance one frame per output frame, so nothing ever closes it.
|
|
1577
|
+
* The FIRST cell is the most behind; the last is live.
|
|
1578
|
+
*
|
|
1579
|
+
* Reproduced in one command on 2026-09-18 — two sources hstacked, one frame,
|
|
1580
|
+
* reading the cameras' own burnt-in clocks out of the composed picture:
|
|
1581
|
+
*
|
|
1582
|
+
* ```
|
|
1583
|
+
* 13:11:21 | 13:11:23 two seconds, and the earlier input is the late one
|
|
1584
|
+
* ```
|
|
1585
|
+
*
|
|
1586
|
+
* ## Why the old numbers cost seconds
|
|
1587
|
+
*
|
|
1588
|
+
* `-probesize` is a BYTE budget, and 1 MB of a 640x360 sub-stream is tens of
|
|
1589
|
+
* seconds of video — so `-analyzeduration 1s` never got the chance to stop it
|
|
1590
|
+
* and each open ran until the byte budget filled. Zero and 32 make the probe
|
|
1591
|
+
* return from the SDP, which already declares the codec, without reading media.
|
|
1592
|
+
*
|
|
1593
|
+
* Measured on this hub, the two sources of the live grid, three runs each,
|
|
1594
|
+
* production input args otherwise identical — time to the first composed frame,
|
|
1595
|
+
* and the skew read off the two burnt-in clocks in that frame:
|
|
1596
|
+
*
|
|
1597
|
+
* ```
|
|
1598
|
+
* 1 s / 1 MB 8986, 9728, 9712 ms skew 5 s, 5 s, 4 s
|
|
1599
|
+
* 0 / 32 2281, 2270, 2304 ms skew 1 s, 1 s, 0 s
|
|
1600
|
+
* ```
|
|
1601
|
+
*
|
|
1602
|
+
* The same numbers are the ~9 s cold start the operator had accepted: it was a
|
|
1603
|
+
* SUM of the opens, never the wait for the slowest source.
|
|
1604
|
+
*
|
|
1605
|
+
* Checked before shipping, because a probe that reads nothing has to survive
|
|
1606
|
+
* what it is not told: h264, h265 and a derived (`mid-to-low`) source all open
|
|
1607
|
+
* with `rc=0`, a 20 s run produces 200 frames of 200 either way (so the pacing
|
|
1608
|
+
* this file already lost once is untouched), and the skew is still 0 at 25 s
|
|
1609
|
+
* into a run — it is set at start-up and does not drift.
|
|
1610
|
+
*
|
|
1611
|
+
* What remains is the spread in when each source's first key frame arrives, and
|
|
1612
|
+
* a `/muted` dial waits for the next IDR. That spread is small here; if it ever
|
|
1613
|
+
* is not, the answer is to start the inputs from a join point we hold, NOT to
|
|
1614
|
+
* re-time the inputs (see `useWallclockTimestamps` below).
|
|
1592
1615
|
*/
|
|
1593
|
-
var ANALYZE_DURATION_US =
|
|
1594
|
-
var PROBE_SIZE_BYTES =
|
|
1616
|
+
var ANALYZE_DURATION_US = 0;
|
|
1617
|
+
var PROBE_SIZE_BYTES = 32;
|
|
1595
1618
|
function inputPlanFor(source) {
|
|
1596
1619
|
return {
|
|
1597
1620
|
url: source.url,
|
|
@@ -1719,21 +1742,18 @@ var DEFAULTS = {
|
|
|
1719
1742
|
cameraGrids: [],
|
|
1720
1743
|
cameraGridTombstones: []
|
|
1721
1744
|
};
|
|
1722
|
-
/** One session per (grid, profile). A grid composes each profile separately. */
|
|
1723
|
-
function sessionKey(instanceId, profile) {
|
|
1724
|
-
return `${instanceId}::${profile}`;
|
|
1725
|
-
}
|
|
1726
1745
|
/**
|
|
1727
|
-
* One grid's composition
|
|
1728
|
-
*
|
|
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.
|
|
1729
1751
|
*/
|
|
1730
1752
|
var GridSessionEntry = class {
|
|
1731
1753
|
instanceId;
|
|
1732
|
-
profile;
|
|
1733
1754
|
session;
|
|
1734
|
-
constructor(instanceId,
|
|
1755
|
+
constructor(instanceId, session) {
|
|
1735
1756
|
this.instanceId = instanceId;
|
|
1736
|
-
this.profile = profile;
|
|
1737
1757
|
this.session = session;
|
|
1738
1758
|
}
|
|
1739
1759
|
isRunning() {
|
|
@@ -1743,7 +1763,7 @@ var GridSessionEntry = class {
|
|
|
1743
1763
|
var CameraGridAddon = class extends BaseAddon {
|
|
1744
1764
|
relay = null;
|
|
1745
1765
|
reconcileTimer = null;
|
|
1746
|
-
/** Keyed by
|
|
1766
|
+
/** Keyed by instance id — ONE composition per grid. */
|
|
1747
1767
|
sessions = /* @__PURE__ */ new Map();
|
|
1748
1768
|
deviceIdByInstanceId = /* @__PURE__ */ new Map();
|
|
1749
1769
|
tombstones = /* @__PURE__ */ new Set();
|
|
@@ -1757,7 +1777,7 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
1757
1777
|
* (D224), and 60 s is the window for a fact — "does 615 have a low assigned"
|
|
1758
1778
|
* — that only changes when an operator changes it.
|
|
1759
1779
|
*/
|
|
1760
|
-
|
|
1780
|
+
planByInstanceId = /* @__PURE__ */ new Map();
|
|
1761
1781
|
snapshots = null;
|
|
1762
1782
|
frameSampler = null;
|
|
1763
1783
|
ffmpegBinaryPath = "ffmpeg";
|
|
@@ -1860,7 +1880,7 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
1860
1880
|
if (!this.relay) return;
|
|
1861
1881
|
const instances = this.instances();
|
|
1862
1882
|
const declarations = buildGridCameraDeclarations(instances);
|
|
1863
|
-
await this.
|
|
1883
|
+
await this.refreshSourcePlans(instances);
|
|
1864
1884
|
await this.retireWithdrawnSessions(instances);
|
|
1865
1885
|
const result = await new DeclaredDevices({
|
|
1866
1886
|
logger: this.ctx.logger.child("declaration"),
|
|
@@ -1927,15 +1947,15 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
1927
1947
|
}
|
|
1928
1948
|
}
|
|
1929
1949
|
/**
|
|
1930
|
-
* Which
|
|
1950
|
+
* Which stream each grid reads each of its sources at, asked of the BROKER.
|
|
1931
1951
|
*
|
|
1932
1952
|
* `listAllProfileSlots` is the one authority on "does 615 have a low
|
|
1933
1953
|
* assigned", and `sourceCamStreamId === null` is its way of saying no. A read
|
|
1934
|
-
* 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
|
|
1935
1955
|
* empty catalog, and turning it into one would retract the streams of every
|
|
1936
1956
|
* grid on the hub.
|
|
1937
1957
|
*/
|
|
1938
|
-
async
|
|
1958
|
+
async refreshSourcePlans(instances) {
|
|
1939
1959
|
let slots;
|
|
1940
1960
|
try {
|
|
1941
1961
|
slots = await this.ctx.api.streamBroker.listAllProfileSlots.query();
|
|
@@ -1954,69 +1974,70 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
1954
1974
|
servedBy.set(slot.deviceId, profiles);
|
|
1955
1975
|
}
|
|
1956
1976
|
for (const instance of instances) {
|
|
1957
|
-
const
|
|
1977
|
+
const plan = gridSourceProfiles({
|
|
1958
1978
|
sourceDeviceIds: [...new Set(instance.cells.map((cell) => cell.deviceId))],
|
|
1959
1979
|
profilesServedBy: (deviceId) => servedBy.get(deviceId) ?? null
|
|
1960
1980
|
});
|
|
1961
|
-
const previous = this.
|
|
1962
|
-
this.
|
|
1963
|
-
this.
|
|
1981
|
+
const previous = this.planByInstanceId.get(instance.id);
|
|
1982
|
+
this.planByInstanceId.set(instance.id, plan);
|
|
1983
|
+
this.logSourcePlan(instance, plan, previous);
|
|
1964
1984
|
}
|
|
1965
1985
|
}
|
|
1966
1986
|
/**
|
|
1967
|
-
* Say
|
|
1987
|
+
* Say WHICH stream each cell is read at, and who could not be read - once per
|
|
1988
|
+
* change.
|
|
1989
|
+
*
|
|
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.
|
|
1968
1995
|
*
|
|
1969
|
-
*
|
|
1970
|
-
*
|
|
1971
|
-
* that can only serve `high` would otherwise write two lines a minute for
|
|
1972
|
-
* ever.
|
|
1996
|
+
* Logged on the EDGE: a grid that cannot be composed would otherwise write a
|
|
1997
|
+
* line a minute for ever.
|
|
1973
1998
|
*/
|
|
1974
|
-
|
|
1975
|
-
|
|
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;
|
|
1976
2006
|
const deviceId = this.deviceIdByInstanceId.get(instance.id) ?? 0;
|
|
1977
|
-
|
|
1978
|
-
if (offer.offered) continue;
|
|
1979
|
-
if (offer.missingSources.length === 0) continue;
|
|
1980
|
-
this.ctx.logger.info("camera grid: a profile is NOT on offer", {
|
|
1981
|
-
tags: { deviceId },
|
|
1982
|
-
meta: {
|
|
1983
|
-
instanceId: instance.id,
|
|
1984
|
-
profile: offer.profile,
|
|
1985
|
-
missingSources: [...offer.missingSources]
|
|
1986
|
-
}
|
|
1987
|
-
});
|
|
1988
|
-
}
|
|
1989
|
-
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", {
|
|
1990
2008
|
tags: { deviceId },
|
|
1991
2009
|
meta: {
|
|
1992
2010
|
instanceId: instance.id,
|
|
1993
|
-
|
|
2011
|
+
missingSources: [...plan.missingSources]
|
|
2012
|
+
}
|
|
2013
|
+
});
|
|
2014
|
+
this.ctx.logger.info("camera grid: composing", {
|
|
2015
|
+
tags: { deviceId },
|
|
2016
|
+
meta: {
|
|
2017
|
+
instanceId: instance.id,
|
|
2018
|
+
offered: plan.offered,
|
|
2019
|
+
readAt: Object.fromEntries([...plan.profileBySource].map(([id, p]) => [String(id), p]))
|
|
1994
2020
|
}
|
|
1995
2021
|
});
|
|
1996
2022
|
}
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
if (offers === void 0) return null;
|
|
2000
|
-
return offers.filter((offer) => offer.offered).map((offer) => offer.profile);
|
|
2023
|
+
planFor(instanceId) {
|
|
2024
|
+
return this.planByInstanceId.get(instanceId) ?? null;
|
|
2001
2025
|
}
|
|
2002
2026
|
ensureSessions(instanceId, deviceId) {
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2005
|
-
const
|
|
2006
|
-
if (
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
this.sessions.delete(key);
|
|
2010
|
-
stale.session.dispose();
|
|
2011
|
-
}
|
|
2012
|
-
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();
|
|
2013
2033
|
}
|
|
2014
|
-
|
|
2015
|
-
const session = this.makeSession(instanceId, deviceId, profile);
|
|
2016
|
-
this.sessions.set(key, new GridSessionEntry(instanceId, profile, session));
|
|
2034
|
+
return;
|
|
2017
2035
|
}
|
|
2036
|
+
if (this.sessions.has(instanceId)) return;
|
|
2037
|
+
const session = this.makeSession(instanceId, deviceId);
|
|
2038
|
+
this.sessions.set(instanceId, new GridSessionEntry(instanceId, session));
|
|
2018
2039
|
}
|
|
2019
|
-
makeSession(instanceId, deviceId
|
|
2040
|
+
makeSession(instanceId, deviceId) {
|
|
2020
2041
|
const childContext = {
|
|
2021
2042
|
binaryPath: this.ffmpegBinaryPath,
|
|
2022
2043
|
logger: this.ctx.logger,
|
|
@@ -2027,6 +2048,8 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2027
2048
|
deps: {
|
|
2028
2049
|
logger: this.ctx.logger,
|
|
2029
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)}`);
|
|
2030
2053
|
const dial = gridSourceDial(await acquireWithRetry(() => this.ctx.api.streamBroker.getStreamWithCodec.mutate({
|
|
2031
2054
|
deviceId: sourceDeviceId,
|
|
2032
2055
|
video: "copy",
|
|
@@ -2076,11 +2099,7 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2076
2099
|
if (!instance) return null;
|
|
2077
2100
|
return gridPlanFor(instance, {
|
|
2078
2101
|
encoder: "libx264",
|
|
2079
|
-
decodeHwAccel: null
|
|
2080
|
-
canvas: gridCanvasFor({
|
|
2081
|
-
width: instance.width,
|
|
2082
|
-
height: instance.height
|
|
2083
|
-
}, profile)
|
|
2102
|
+
decodeHwAccel: null
|
|
2084
2103
|
});
|
|
2085
2104
|
}
|
|
2086
2105
|
});
|
|
@@ -2088,18 +2107,14 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2088
2107
|
}
|
|
2089
2108
|
/** The session a dial is asking for, or `null` - which the relay answers 404 to. */
|
|
2090
2109
|
sessionFor(request) {
|
|
2091
|
-
return this.sessions.get(
|
|
2110
|
+
return this.sessions.get(request.instanceId)?.session ?? null;
|
|
2092
2111
|
}
|
|
2093
|
-
/** This grid's
|
|
2112
|
+
/** This grid's composition, as the list the snapshot source expects. */
|
|
2094
2113
|
sessionsForDevice(deviceId) {
|
|
2095
2114
|
const instanceId = this.instanceIdForDevice(deviceId);
|
|
2096
2115
|
if (instanceId === null) return [];
|
|
2097
|
-
const
|
|
2098
|
-
|
|
2099
|
-
const entry = this.sessions.get(sessionKey(instanceId, profile));
|
|
2100
|
-
if (entry) entries.push(entry);
|
|
2101
|
-
}
|
|
2102
|
-
return entries;
|
|
2116
|
+
const entry = this.sessions.get(instanceId);
|
|
2117
|
+
return entry ? [entry] : [];
|
|
2103
2118
|
}
|
|
2104
2119
|
/** A grid that is gone stops immediately - it must not keep N sources dialled. */
|
|
2105
2120
|
async retireWithdrawnSessions(instances) {
|
|
@@ -2108,10 +2123,7 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2108
2123
|
if (live.has(entry.instanceId)) continue;
|
|
2109
2124
|
this.ctx.logger.info("camera grid: retiring a withdrawn grid", {
|
|
2110
2125
|
tags: { deviceId: this.deviceIdByInstanceId.get(entry.instanceId) ?? 0 },
|
|
2111
|
-
meta: {
|
|
2112
|
-
instanceId: entry.instanceId,
|
|
2113
|
-
profile: entry.profile
|
|
2114
|
-
}
|
|
2126
|
+
meta: { instanceId: entry.instanceId }
|
|
2115
2127
|
});
|
|
2116
2128
|
this.sessions.delete(key);
|
|
2117
2129
|
await entry.session.dispose();
|
|
@@ -2119,7 +2131,7 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2119
2131
|
for (const instanceId of [...this.deviceIdByInstanceId.keys()]) {
|
|
2120
2132
|
if (live.has(instanceId)) continue;
|
|
2121
2133
|
this.deviceIdByInstanceId.delete(instanceId);
|
|
2122
|
-
this.
|
|
2134
|
+
this.planByInstanceId.delete(instanceId);
|
|
2123
2135
|
}
|
|
2124
2136
|
}
|
|
2125
2137
|
/**
|
|
@@ -2132,10 +2144,7 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2132
2144
|
if (!entry.session.isRunning()) continue;
|
|
2133
2145
|
this.ctx.logger.info("camera grid: the layout changed - restarting the composition", {
|
|
2134
2146
|
tags: { deviceId: this.deviceIdByInstanceId.get(entry.instanceId) ?? 0 },
|
|
2135
|
-
meta: {
|
|
2136
|
-
instanceId: entry.instanceId,
|
|
2137
|
-
profile: entry.profile
|
|
2138
|
-
}
|
|
2147
|
+
meta: { instanceId: entry.instanceId }
|
|
2139
2148
|
});
|
|
2140
2149
|
entry.session.onChildEnded("layout-changed");
|
|
2141
2150
|
}
|
|
@@ -2170,24 +2179,15 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2170
2179
|
descriptorsFor(instanceId) {
|
|
2171
2180
|
const instance = this.instanceById(instanceId);
|
|
2172
2181
|
const relay = this.relay;
|
|
2173
|
-
const
|
|
2174
|
-
if (!instance || !relay ||
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
width: canvas.width,
|
|
2183
|
-
height: canvas.height,
|
|
2184
|
-
fps: instance.fps,
|
|
2185
|
-
url: relay.streamUrl({
|
|
2186
|
-
instanceId,
|
|
2187
|
-
profile
|
|
2188
|
-
})
|
|
2189
|
-
});
|
|
2190
|
-
});
|
|
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
|
+
})];
|
|
2191
2191
|
}
|
|
2192
2192
|
instanceIdForDevice(deviceId) {
|
|
2193
2193
|
for (const [instanceId, id] of this.deviceIdByInstanceId) if (id === deviceId) return instanceId;
|
|
@@ -2212,11 +2212,15 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2212
2212
|
source: cell.source,
|
|
2213
2213
|
cell: cell.cell
|
|
2214
2214
|
})),
|
|
2215
|
-
profiles: (
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
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
|
+
})()
|
|
2220
2224
|
};
|
|
2221
2225
|
}
|
|
2222
2226
|
async saveGridLayout(input) {
|
|
@@ -2242,7 +2246,7 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2242
2246
|
...row,
|
|
2243
2247
|
...updated
|
|
2244
2248
|
} : row) });
|
|
2245
|
-
await this.
|
|
2249
|
+
await this.refreshSourcePlans(this.instances());
|
|
2246
2250
|
this.ensureSessions(instance.id, input.deviceId);
|
|
2247
2251
|
const view = this.gridViewFor(input.deviceId);
|
|
2248
2252
|
if (!view) throw new Error("camera-grid: the grid vanished while it was being saved");
|
|
@@ -2308,4 +2312,4 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2308
2312
|
}
|
|
2309
2313
|
};
|
|
2310
2314
|
//#endregion
|
|
2311
|
-
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 };
|