@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
|
@@ -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
|
}
|
|
@@ -1587,16 +1568,58 @@ var PIXEL_FORMAT = "yuv420p";
|
|
|
1587
1568
|
*/
|
|
1588
1569
|
var GOP_SECONDS = 1;
|
|
1589
1570
|
/**
|
|
1590
|
-
* `-analyzeduration` / `-probesize`,
|
|
1571
|
+
* `-analyzeduration` / `-probesize`, as small as ffmpeg allows.
|
|
1591
1572
|
*
|
|
1592
|
-
*
|
|
1593
|
-
*
|
|
1594
|
-
*
|
|
1595
|
-
*
|
|
1596
|
-
*
|
|
1573
|
+
* ## The probes are SEQUENTIAL, and this is what the tile skew was
|
|
1574
|
+
*
|
|
1575
|
+
* The sentence that used to be here said the probes are "PARALLEL inputs of one
|
|
1576
|
+
* child". They are not. ffmpeg opens its inputs one after another and each open
|
|
1577
|
+
* BLOCKS in `avformat_find_stream_info` until its budget is spent — so input 0
|
|
1578
|
+
* is already connected and receiving while ffmpeg is still opening input 1, its
|
|
1579
|
+
* frames pile up, and `overlay` (which pairs inputs BY PTS) marries input 0's
|
|
1580
|
+
* oldest frame to input 1's newest. The lag is fixed for the life of the child:
|
|
1581
|
+
* both tiles then advance one frame per output frame, so nothing ever closes it.
|
|
1582
|
+
* The FIRST cell is the most behind; the last is live.
|
|
1583
|
+
*
|
|
1584
|
+
* Reproduced in one command on 2026-09-18 — two sources hstacked, one frame,
|
|
1585
|
+
* reading the cameras' own burnt-in clocks out of the composed picture:
|
|
1586
|
+
*
|
|
1587
|
+
* ```
|
|
1588
|
+
* 13:11:21 | 13:11:23 two seconds, and the earlier input is the late one
|
|
1589
|
+
* ```
|
|
1590
|
+
*
|
|
1591
|
+
* ## Why the old numbers cost seconds
|
|
1592
|
+
*
|
|
1593
|
+
* `-probesize` is a BYTE budget, and 1 MB of a 640x360 sub-stream is tens of
|
|
1594
|
+
* seconds of video — so `-analyzeduration 1s` never got the chance to stop it
|
|
1595
|
+
* and each open ran until the byte budget filled. Zero and 32 make the probe
|
|
1596
|
+
* return from the SDP, which already declares the codec, without reading media.
|
|
1597
|
+
*
|
|
1598
|
+
* Measured on this hub, the two sources of the live grid, three runs each,
|
|
1599
|
+
* production input args otherwise identical — time to the first composed frame,
|
|
1600
|
+
* and the skew read off the two burnt-in clocks in that frame:
|
|
1601
|
+
*
|
|
1602
|
+
* ```
|
|
1603
|
+
* 1 s / 1 MB 8986, 9728, 9712 ms skew 5 s, 5 s, 4 s
|
|
1604
|
+
* 0 / 32 2281, 2270, 2304 ms skew 1 s, 1 s, 0 s
|
|
1605
|
+
* ```
|
|
1606
|
+
*
|
|
1607
|
+
* The same numbers are the ~9 s cold start the operator had accepted: it was a
|
|
1608
|
+
* SUM of the opens, never the wait for the slowest source.
|
|
1609
|
+
*
|
|
1610
|
+
* Checked before shipping, because a probe that reads nothing has to survive
|
|
1611
|
+
* what it is not told: h264, h265 and a derived (`mid-to-low`) source all open
|
|
1612
|
+
* with `rc=0`, a 20 s run produces 200 frames of 200 either way (so the pacing
|
|
1613
|
+
* this file already lost once is untouched), and the skew is still 0 at 25 s
|
|
1614
|
+
* into a run — it is set at start-up and does not drift.
|
|
1615
|
+
*
|
|
1616
|
+
* What remains is the spread in when each source's first key frame arrives, and
|
|
1617
|
+
* a `/muted` dial waits for the next IDR. That spread is small here; if it ever
|
|
1618
|
+
* is not, the answer is to start the inputs from a join point we hold, NOT to
|
|
1619
|
+
* re-time the inputs (see `useWallclockTimestamps` below).
|
|
1597
1620
|
*/
|
|
1598
|
-
var ANALYZE_DURATION_US =
|
|
1599
|
-
var PROBE_SIZE_BYTES =
|
|
1621
|
+
var ANALYZE_DURATION_US = 0;
|
|
1622
|
+
var PROBE_SIZE_BYTES = 32;
|
|
1600
1623
|
function inputPlanFor(source) {
|
|
1601
1624
|
return {
|
|
1602
1625
|
url: source.url,
|
|
@@ -1724,21 +1747,18 @@ var DEFAULTS = {
|
|
|
1724
1747
|
cameraGrids: [],
|
|
1725
1748
|
cameraGridTombstones: []
|
|
1726
1749
|
};
|
|
1727
|
-
/** One session per (grid, profile). A grid composes each profile separately. */
|
|
1728
|
-
function sessionKey(instanceId, profile) {
|
|
1729
|
-
return `${instanceId}::${profile}`;
|
|
1730
|
-
}
|
|
1731
1750
|
/**
|
|
1732
|
-
* One grid's composition
|
|
1733
|
-
*
|
|
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.
|
|
1734
1756
|
*/
|
|
1735
1757
|
var GridSessionEntry = class {
|
|
1736
1758
|
instanceId;
|
|
1737
|
-
profile;
|
|
1738
1759
|
session;
|
|
1739
|
-
constructor(instanceId,
|
|
1760
|
+
constructor(instanceId, session) {
|
|
1740
1761
|
this.instanceId = instanceId;
|
|
1741
|
-
this.profile = profile;
|
|
1742
1762
|
this.session = session;
|
|
1743
1763
|
}
|
|
1744
1764
|
isRunning() {
|
|
@@ -1748,7 +1768,7 @@ var GridSessionEntry = class {
|
|
|
1748
1768
|
var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
1749
1769
|
relay = null;
|
|
1750
1770
|
reconcileTimer = null;
|
|
1751
|
-
/** Keyed by
|
|
1771
|
+
/** Keyed by instance id — ONE composition per grid. */
|
|
1752
1772
|
sessions = /* @__PURE__ */ new Map();
|
|
1753
1773
|
deviceIdByInstanceId = /* @__PURE__ */ new Map();
|
|
1754
1774
|
tombstones = /* @__PURE__ */ new Set();
|
|
@@ -1762,7 +1782,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
1762
1782
|
* (D224), and 60 s is the window for a fact — "does 615 have a low assigned"
|
|
1763
1783
|
* — that only changes when an operator changes it.
|
|
1764
1784
|
*/
|
|
1765
|
-
|
|
1785
|
+
planByInstanceId = /* @__PURE__ */ new Map();
|
|
1766
1786
|
snapshots = null;
|
|
1767
1787
|
frameSampler = null;
|
|
1768
1788
|
ffmpegBinaryPath = "ffmpeg";
|
|
@@ -1865,7 +1885,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
1865
1885
|
if (!this.relay) return;
|
|
1866
1886
|
const instances = this.instances();
|
|
1867
1887
|
const declarations = buildGridCameraDeclarations(instances);
|
|
1868
|
-
await this.
|
|
1888
|
+
await this.refreshSourcePlans(instances);
|
|
1869
1889
|
await this.retireWithdrawnSessions(instances);
|
|
1870
1890
|
const result = await new require_dist.DeclaredDevices({
|
|
1871
1891
|
logger: this.ctx.logger.child("declaration"),
|
|
@@ -1932,15 +1952,15 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
1932
1952
|
}
|
|
1933
1953
|
}
|
|
1934
1954
|
/**
|
|
1935
|
-
* Which
|
|
1955
|
+
* Which stream each grid reads each of its sources at, asked of the BROKER.
|
|
1936
1956
|
*
|
|
1937
1957
|
* `listAllProfileSlots` is the one authority on "does 615 have a low
|
|
1938
1958
|
* assigned", and `sourceCamStreamId === null` is its way of saying no. A read
|
|
1939
|
-
* 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
|
|
1940
1960
|
* empty catalog, and turning it into one would retract the streams of every
|
|
1941
1961
|
* grid on the hub.
|
|
1942
1962
|
*/
|
|
1943
|
-
async
|
|
1963
|
+
async refreshSourcePlans(instances) {
|
|
1944
1964
|
let slots;
|
|
1945
1965
|
try {
|
|
1946
1966
|
slots = await this.ctx.api.streamBroker.listAllProfileSlots.query();
|
|
@@ -1959,69 +1979,70 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
1959
1979
|
servedBy.set(slot.deviceId, profiles);
|
|
1960
1980
|
}
|
|
1961
1981
|
for (const instance of instances) {
|
|
1962
|
-
const
|
|
1982
|
+
const plan = gridSourceProfiles({
|
|
1963
1983
|
sourceDeviceIds: [...new Set(instance.cells.map((cell) => cell.deviceId))],
|
|
1964
1984
|
profilesServedBy: (deviceId) => servedBy.get(deviceId) ?? null
|
|
1965
1985
|
});
|
|
1966
|
-
const previous = this.
|
|
1967
|
-
this.
|
|
1968
|
-
this.
|
|
1986
|
+
const previous = this.planByInstanceId.get(instance.id);
|
|
1987
|
+
this.planByInstanceId.set(instance.id, plan);
|
|
1988
|
+
this.logSourcePlan(instance, plan, previous);
|
|
1969
1989
|
}
|
|
1970
1990
|
}
|
|
1971
1991
|
/**
|
|
1972
|
-
* Say
|
|
1992
|
+
* Say WHICH stream each cell is read at, and who could not be read - once per
|
|
1993
|
+
* change.
|
|
1994
|
+
*
|
|
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.
|
|
1973
2000
|
*
|
|
1974
|
-
*
|
|
1975
|
-
*
|
|
1976
|
-
* that can only serve `high` would otherwise write two lines a minute for
|
|
1977
|
-
* ever.
|
|
2001
|
+
* Logged on the EDGE: a grid that cannot be composed would otherwise write a
|
|
2002
|
+
* line a minute for ever.
|
|
1978
2003
|
*/
|
|
1979
|
-
|
|
1980
|
-
|
|
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;
|
|
1981
2011
|
const deviceId = this.deviceIdByInstanceId.get(instance.id) ?? 0;
|
|
1982
|
-
|
|
1983
|
-
if (offer.offered) continue;
|
|
1984
|
-
if (offer.missingSources.length === 0) continue;
|
|
1985
|
-
this.ctx.logger.info("camera grid: a profile is NOT on offer", {
|
|
1986
|
-
tags: { deviceId },
|
|
1987
|
-
meta: {
|
|
1988
|
-
instanceId: instance.id,
|
|
1989
|
-
profile: offer.profile,
|
|
1990
|
-
missingSources: [...offer.missingSources]
|
|
1991
|
-
}
|
|
1992
|
-
});
|
|
1993
|
-
}
|
|
1994
|
-
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", {
|
|
1995
2013
|
tags: { deviceId },
|
|
1996
2014
|
meta: {
|
|
1997
2015
|
instanceId: instance.id,
|
|
1998
|
-
|
|
2016
|
+
missingSources: [...plan.missingSources]
|
|
2017
|
+
}
|
|
2018
|
+
});
|
|
2019
|
+
this.ctx.logger.info("camera grid: composing", {
|
|
2020
|
+
tags: { deviceId },
|
|
2021
|
+
meta: {
|
|
2022
|
+
instanceId: instance.id,
|
|
2023
|
+
offered: plan.offered,
|
|
2024
|
+
readAt: Object.fromEntries([...plan.profileBySource].map(([id, p]) => [String(id), p]))
|
|
1999
2025
|
}
|
|
2000
2026
|
});
|
|
2001
2027
|
}
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
if (offers === void 0) return null;
|
|
2005
|
-
return offers.filter((offer) => offer.offered).map((offer) => offer.profile);
|
|
2028
|
+
planFor(instanceId) {
|
|
2029
|
+
return this.planByInstanceId.get(instanceId) ?? null;
|
|
2006
2030
|
}
|
|
2007
2031
|
ensureSessions(instanceId, deviceId) {
|
|
2008
|
-
const
|
|
2009
|
-
|
|
2010
|
-
const
|
|
2011
|
-
if (
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
this.sessions.delete(key);
|
|
2015
|
-
stale.session.dispose();
|
|
2016
|
-
}
|
|
2017
|
-
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();
|
|
2018
2038
|
}
|
|
2019
|
-
|
|
2020
|
-
const session = this.makeSession(instanceId, deviceId, profile);
|
|
2021
|
-
this.sessions.set(key, new GridSessionEntry(instanceId, profile, session));
|
|
2039
|
+
return;
|
|
2022
2040
|
}
|
|
2041
|
+
if (this.sessions.has(instanceId)) return;
|
|
2042
|
+
const session = this.makeSession(instanceId, deviceId);
|
|
2043
|
+
this.sessions.set(instanceId, new GridSessionEntry(instanceId, session));
|
|
2023
2044
|
}
|
|
2024
|
-
makeSession(instanceId, deviceId
|
|
2045
|
+
makeSession(instanceId, deviceId) {
|
|
2025
2046
|
const childContext = {
|
|
2026
2047
|
binaryPath: this.ffmpegBinaryPath,
|
|
2027
2048
|
logger: this.ctx.logger,
|
|
@@ -2032,6 +2053,8 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2032
2053
|
deps: {
|
|
2033
2054
|
logger: this.ctx.logger,
|
|
2034
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)}`);
|
|
2035
2058
|
const dial = gridSourceDial(await acquireWithRetry(() => this.ctx.api.streamBroker.getStreamWithCodec.mutate({
|
|
2036
2059
|
deviceId: sourceDeviceId,
|
|
2037
2060
|
video: "copy",
|
|
@@ -2081,11 +2104,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2081
2104
|
if (!instance) return null;
|
|
2082
2105
|
return gridPlanFor(instance, {
|
|
2083
2106
|
encoder: "libx264",
|
|
2084
|
-
decodeHwAccel: null
|
|
2085
|
-
canvas: gridCanvasFor({
|
|
2086
|
-
width: instance.width,
|
|
2087
|
-
height: instance.height
|
|
2088
|
-
}, profile)
|
|
2107
|
+
decodeHwAccel: null
|
|
2089
2108
|
});
|
|
2090
2109
|
}
|
|
2091
2110
|
});
|
|
@@ -2093,18 +2112,14 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2093
2112
|
}
|
|
2094
2113
|
/** The session a dial is asking for, or `null` - which the relay answers 404 to. */
|
|
2095
2114
|
sessionFor(request) {
|
|
2096
|
-
return this.sessions.get(
|
|
2115
|
+
return this.sessions.get(request.instanceId)?.session ?? null;
|
|
2097
2116
|
}
|
|
2098
|
-
/** This grid's
|
|
2117
|
+
/** This grid's composition, as the list the snapshot source expects. */
|
|
2099
2118
|
sessionsForDevice(deviceId) {
|
|
2100
2119
|
const instanceId = this.instanceIdForDevice(deviceId);
|
|
2101
2120
|
if (instanceId === null) return [];
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2104
|
-
const entry = this.sessions.get(sessionKey(instanceId, profile));
|
|
2105
|
-
if (entry) entries.push(entry);
|
|
2106
|
-
}
|
|
2107
|
-
return entries;
|
|
2121
|
+
const entry = this.sessions.get(instanceId);
|
|
2122
|
+
return entry ? [entry] : [];
|
|
2108
2123
|
}
|
|
2109
2124
|
/** A grid that is gone stops immediately - it must not keep N sources dialled. */
|
|
2110
2125
|
async retireWithdrawnSessions(instances) {
|
|
@@ -2113,10 +2128,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2113
2128
|
if (live.has(entry.instanceId)) continue;
|
|
2114
2129
|
this.ctx.logger.info("camera grid: retiring a withdrawn grid", {
|
|
2115
2130
|
tags: { deviceId: this.deviceIdByInstanceId.get(entry.instanceId) ?? 0 },
|
|
2116
|
-
meta: {
|
|
2117
|
-
instanceId: entry.instanceId,
|
|
2118
|
-
profile: entry.profile
|
|
2119
|
-
}
|
|
2131
|
+
meta: { instanceId: entry.instanceId }
|
|
2120
2132
|
});
|
|
2121
2133
|
this.sessions.delete(key);
|
|
2122
2134
|
await entry.session.dispose();
|
|
@@ -2124,7 +2136,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2124
2136
|
for (const instanceId of [...this.deviceIdByInstanceId.keys()]) {
|
|
2125
2137
|
if (live.has(instanceId)) continue;
|
|
2126
2138
|
this.deviceIdByInstanceId.delete(instanceId);
|
|
2127
|
-
this.
|
|
2139
|
+
this.planByInstanceId.delete(instanceId);
|
|
2128
2140
|
}
|
|
2129
2141
|
}
|
|
2130
2142
|
/**
|
|
@@ -2137,10 +2149,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2137
2149
|
if (!entry.session.isRunning()) continue;
|
|
2138
2150
|
this.ctx.logger.info("camera grid: the layout changed - restarting the composition", {
|
|
2139
2151
|
tags: { deviceId: this.deviceIdByInstanceId.get(entry.instanceId) ?? 0 },
|
|
2140
|
-
meta: {
|
|
2141
|
-
instanceId: entry.instanceId,
|
|
2142
|
-
profile: entry.profile
|
|
2143
|
-
}
|
|
2152
|
+
meta: { instanceId: entry.instanceId }
|
|
2144
2153
|
});
|
|
2145
2154
|
entry.session.onChildEnded("layout-changed");
|
|
2146
2155
|
}
|
|
@@ -2175,24 +2184,15 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2175
2184
|
descriptorsFor(instanceId) {
|
|
2176
2185
|
const instance = this.instanceById(instanceId);
|
|
2177
2186
|
const relay = this.relay;
|
|
2178
|
-
const
|
|
2179
|
-
if (!instance || !relay ||
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
width: canvas.width,
|
|
2188
|
-
height: canvas.height,
|
|
2189
|
-
fps: instance.fps,
|
|
2190
|
-
url: relay.streamUrl({
|
|
2191
|
-
instanceId,
|
|
2192
|
-
profile
|
|
2193
|
-
})
|
|
2194
|
-
});
|
|
2195
|
-
});
|
|
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
|
+
})];
|
|
2196
2196
|
}
|
|
2197
2197
|
instanceIdForDevice(deviceId) {
|
|
2198
2198
|
for (const [instanceId, id] of this.deviceIdByInstanceId) if (id === deviceId) return instanceId;
|
|
@@ -2217,11 +2217,15 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2217
2217
|
source: cell.source,
|
|
2218
2218
|
cell: cell.cell
|
|
2219
2219
|
})),
|
|
2220
|
-
profiles: (
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
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
|
+
})()
|
|
2225
2229
|
};
|
|
2226
2230
|
}
|
|
2227
2231
|
async saveGridLayout(input) {
|
|
@@ -2247,7 +2251,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2247
2251
|
...row,
|
|
2248
2252
|
...updated
|
|
2249
2253
|
} : row) });
|
|
2250
|
-
await this.
|
|
2254
|
+
await this.refreshSourcePlans(this.instances());
|
|
2251
2255
|
this.ensureSessions(instance.id, input.deviceId);
|
|
2252
2256
|
const view = this.gridViewFor(input.deviceId);
|
|
2253
2257
|
if (!view) throw new Error("camera-grid: the grid vanished while it was being saved");
|
|
@@ -2315,7 +2319,7 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2315
2319
|
//#endregion
|
|
2316
2320
|
exports.CameraGridAddon = CameraGridAddon;
|
|
2317
2321
|
exports.GRID_CAM_STREAM_ID = GRID_CAM_STREAM_ID;
|
|
2318
|
-
exports.
|
|
2322
|
+
exports.GRID_OUTPUT_PROFILE = GRID_OUTPUT_PROFILE;
|
|
2319
2323
|
exports.GRID_SILENCED_CAP_NAMES = GRID_SILENCED_CAP_NAMES;
|
|
2320
2324
|
exports.GridCameraDevice = GridCameraDevice;
|
|
2321
2325
|
exports.GridFrameSampler = GridFrameSampler;
|
|
@@ -2326,18 +2330,17 @@ exports.GridStreamSession = GridStreamSession;
|
|
|
2326
2330
|
exports.buildGridCameraDeclarations = buildGridCameraDeclarations;
|
|
2327
2331
|
exports.buildGridFilterGraph = buildGridFilterGraph;
|
|
2328
2332
|
exports.default = CameraGridAddon;
|
|
2329
|
-
exports.gridCamStreamId = gridCamStreamId;
|
|
2330
2333
|
exports.gridCameraReconciliationIndex = gridCameraReconciliationIndex;
|
|
2331
|
-
exports.gridCanvasFor = gridCanvasFor;
|
|
2332
2334
|
exports.gridPlanFor = gridPlanFor;
|
|
2333
2335
|
exports.gridSnapshotAnswerFor = gridSnapshotAnswerFor;
|
|
2334
2336
|
exports.gridSourceDial = gridSourceDial;
|
|
2337
|
+
exports.gridSourceProfiles = gridSourceProfiles;
|
|
2335
2338
|
exports.gridStreamDescriptor = gridStreamDescriptor;
|
|
2336
2339
|
exports.gridStreamInvocation = gridStreamInvocation;
|
|
2337
2340
|
exports.installGridCameraRuntime = installGridCameraRuntime;
|
|
2341
|
+
exports.lowestServedProfile = lowestServedProfile;
|
|
2338
2342
|
exports.newGridCameraStableId = newGridCameraStableId;
|
|
2339
2343
|
exports.normalizeGridRows = normalizeGridRows;
|
|
2340
|
-
exports.offeredGridProfiles = offeredGridProfiles;
|
|
2341
2344
|
exports.parseGridStreamPath = parseGridStreamPath;
|
|
2342
2345
|
exports.readGridInstances = readGridInstances;
|
|
2343
2346
|
exports.silenceAnalysisFor = silenceAnalysisFor;
|