@camstack/system 1.2.265 → 1.2.267
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CamProfile } from '@camstack/types';
|
|
2
|
+
import { NormalizedRect } from './grid-filter-graph.js';
|
|
2
3
|
/**
|
|
3
4
|
* One id, for every grid, forever.
|
|
4
5
|
*
|
|
@@ -20,15 +21,33 @@ export declare const GRID_CAM_STREAM_ID = "grid";
|
|
|
20
21
|
export declare const GRID_OUTPUT_PROFILE: CamProfile;
|
|
21
22
|
/** The cheapest stream this source serves, or `null` when it serves none. */
|
|
22
23
|
export declare function lowestServedProfile(served: readonly CamProfile[]): CamProfile | null;
|
|
24
|
+
/** One stream a camera serves, with the size that decides whether it fits. */
|
|
25
|
+
export interface ServedProfile {
|
|
26
|
+
readonly profile: CamProfile;
|
|
27
|
+
/** `0` means the broker stated no size — see `gridSourceProfiles`. */
|
|
28
|
+
readonly width: number;
|
|
29
|
+
readonly height: number;
|
|
30
|
+
}
|
|
31
|
+
/** One cell, as the two rectangles that decide what its source must carry. */
|
|
32
|
+
export interface GridSourceCell {
|
|
33
|
+
readonly deviceId: number;
|
|
34
|
+
/** The part of the source it shows, normalized against the SOURCE. */
|
|
35
|
+
readonly source: NormalizedRect;
|
|
36
|
+
/** Where it lands, normalized against the CANVAS. */
|
|
37
|
+
readonly cell: NormalizedRect;
|
|
38
|
+
}
|
|
23
39
|
export interface GridSourceProfilesInput {
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
readonly canvas: {
|
|
41
|
+
readonly width: number;
|
|
42
|
+
readonly height: number;
|
|
43
|
+
};
|
|
44
|
+
readonly cells: readonly GridSourceCell[];
|
|
26
45
|
/**
|
|
27
|
-
* Which
|
|
46
|
+
* Which streams this source can serve, or `null` when nobody could say.
|
|
28
47
|
* `null` is not an empty list: an unanswerable question is a refusal, and
|
|
29
48
|
* saying so separately keeps "we did not ask" out of the catalog.
|
|
30
49
|
*/
|
|
31
|
-
readonly profilesServedBy: (deviceId: number) => readonly
|
|
50
|
+
readonly profilesServedBy: (deviceId: number) => readonly ServedProfile[] | null;
|
|
32
51
|
}
|
|
33
52
|
export interface GridSourcePlan {
|
|
34
53
|
/** True when every source can be read — the only case a grid is on offer. */
|
|
@@ -956,18 +956,59 @@ var CHEAPEST_FIRST = [
|
|
|
956
956
|
function lowestServedProfile(served) {
|
|
957
957
|
return CHEAPEST_FIRST.find((profile) => served.includes(profile)) ?? null;
|
|
958
958
|
}
|
|
959
|
+
/** The pixels a cell needs BEHIND it, before the crop takes its share. */
|
|
960
|
+
function requiredSourceSize(cell, canvas) {
|
|
961
|
+
const tileWidth = cell.cell.width * canvas.width;
|
|
962
|
+
const tileHeight = cell.cell.height * canvas.height;
|
|
963
|
+
const cropWidth = cell.source.width > 0 ? cell.source.width : 1;
|
|
964
|
+
const cropHeight = cell.source.height > 0 ? cell.source.height : 1;
|
|
965
|
+
return {
|
|
966
|
+
width: tileWidth / cropWidth,
|
|
967
|
+
height: tileHeight / cropHeight
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* The cheapest stream that covers `need`, or the BIGGEST when none does.
|
|
972
|
+
*
|
|
973
|
+
* Soft beats absent: a camera whose best stream cannot fill its cell still
|
|
974
|
+
* composes, at the best it has. Refusing would take the whole grid down over
|
|
975
|
+
* one cell being drawn larger than its source.
|
|
976
|
+
*
|
|
977
|
+
* A stream that states NO size cannot be compared, so it is ordered as cheapest
|
|
978
|
+
* rather than discarded — an unmeasurable stream is still a stream, and the
|
|
979
|
+
* picture is what the operator asked for.
|
|
980
|
+
*/
|
|
981
|
+
function bestFit(served, need) {
|
|
982
|
+
if (served.length === 0) return null;
|
|
983
|
+
const order = new Map(CHEAPEST_FIRST.map((profile, index) => [profile, index]));
|
|
984
|
+
const cheapestFirst = [...served].sort((a, b) => (order.get(a.profile) ?? 0) - (order.get(b.profile) ?? 0));
|
|
985
|
+
const unmeasurable = cheapestFirst.find((s) => s.width <= 0 || s.height <= 0);
|
|
986
|
+
if (unmeasurable !== void 0) return unmeasurable.profile;
|
|
987
|
+
const covering = cheapestFirst.find((s) => s.width >= need.width && s.height >= need.height);
|
|
988
|
+
if (covering !== void 0) return covering.profile;
|
|
989
|
+
return cheapestFirst.reduce((best, s) => s.width * s.height > best.width * best.height ? s : best).profile;
|
|
990
|
+
}
|
|
959
991
|
/** Which stream to read each source at, and who cannot be read at all. */
|
|
960
992
|
function gridSourceProfiles(input) {
|
|
993
|
+
const neededBySource = /* @__PURE__ */ new Map();
|
|
994
|
+
for (const cell of input.cells) {
|
|
995
|
+
const need = requiredSourceSize(cell, input.canvas);
|
|
996
|
+
const current = neededBySource.get(cell.deviceId);
|
|
997
|
+
neededBySource.set(cell.deviceId, {
|
|
998
|
+
width: Math.max(current?.width ?? 0, need.width),
|
|
999
|
+
height: Math.max(current?.height ?? 0, need.height)
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
961
1002
|
const profileBySource = /* @__PURE__ */ new Map();
|
|
962
1003
|
const missingSources = [];
|
|
963
|
-
for (const deviceId of
|
|
1004
|
+
for (const [deviceId, need] of neededBySource) {
|
|
964
1005
|
const served = input.profilesServedBy(deviceId);
|
|
965
|
-
const chosen = served === null ? null :
|
|
1006
|
+
const chosen = served === null ? null : bestFit(served, need);
|
|
966
1007
|
if (chosen === null) missingSources.push(deviceId);
|
|
967
1008
|
else profileBySource.set(deviceId, chosen);
|
|
968
1009
|
}
|
|
969
1010
|
return {
|
|
970
|
-
offered:
|
|
1011
|
+
offered: neededBySource.size > 0 && missingSources.length === 0,
|
|
971
1012
|
profileBySource,
|
|
972
1013
|
missingSources
|
|
973
1014
|
};
|
|
@@ -2243,6 +2284,25 @@ async function startGridCompositorChild(ctx, plan, sources, onEnded) {
|
|
|
2243
2284
|
};
|
|
2244
2285
|
let encoderIn = null;
|
|
2245
2286
|
let encoderOut = null;
|
|
2287
|
+
let draining = false;
|
|
2288
|
+
let tick = null;
|
|
2289
|
+
const compositor = new GridCompositor({
|
|
2290
|
+
plan: tilePlan,
|
|
2291
|
+
write: (frame) => {
|
|
2292
|
+
const sink = encoderIn;
|
|
2293
|
+
if (sink === null || draining || sink.destroyed) return false;
|
|
2294
|
+
const accepted = sink.write(frame);
|
|
2295
|
+
if (!accepted) draining = true;
|
|
2296
|
+
return accepted;
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
const startTick = () => {
|
|
2300
|
+
if (tick !== null) return;
|
|
2301
|
+
tick = setInterval(() => {
|
|
2302
|
+
compositor.tick();
|
|
2303
|
+
}, Math.max(1, Math.round(1e3 / plan.fps)));
|
|
2304
|
+
tick.unref();
|
|
2305
|
+
};
|
|
2246
2306
|
const encoder = new _camstack_types_node.FfmpegProcess({
|
|
2247
2307
|
binaryPath: ctx.binaryPath,
|
|
2248
2308
|
buildArgs: () => require_dist.buildFfmpegArgs(gridCanvasEncoderInvocation({
|
|
@@ -2266,34 +2326,26 @@ async function startGridCompositorChild(ctx, plan, sources, onEnded) {
|
|
|
2266
2326
|
onChild: (child) => {
|
|
2267
2327
|
encoderIn = child.stdin;
|
|
2268
2328
|
if (child.stdout) encoderOut = child.stdout;
|
|
2329
|
+
child.stdin?.on("drain", () => {
|
|
2330
|
+
draining = false;
|
|
2331
|
+
});
|
|
2332
|
+
startTick();
|
|
2269
2333
|
child.stderr?.on("data", () => {});
|
|
2270
2334
|
child.stdin?.on("error", () => {});
|
|
2271
2335
|
},
|
|
2272
2336
|
onExit: (exit) => end(`encoder:${exit.classification}`)
|
|
2273
2337
|
});
|
|
2274
|
-
|
|
2338
|
+
try {
|
|
2339
|
+
await encoder.start();
|
|
2340
|
+
} catch (error) {
|
|
2341
|
+
if (tick !== null) clearInterval(tick);
|
|
2342
|
+
compositor.close();
|
|
2343
|
+
throw error;
|
|
2344
|
+
}
|
|
2275
2345
|
if (encoderIn === null || encoderOut === null) {
|
|
2276
2346
|
await encoder.stop();
|
|
2277
2347
|
throw new Error("camera-grid: the canvas encoder exposed no stdin/stdout to compose into");
|
|
2278
2348
|
}
|
|
2279
|
-
let draining = false;
|
|
2280
|
-
const sink = encoderIn;
|
|
2281
|
-
sink.on("drain", () => {
|
|
2282
|
-
draining = false;
|
|
2283
|
-
});
|
|
2284
|
-
const compositor = new GridCompositor({
|
|
2285
|
-
plan: tilePlan,
|
|
2286
|
-
write: (frame) => {
|
|
2287
|
-
if (draining || sink.destroyed) return false;
|
|
2288
|
-
const accepted = sink.write(frame);
|
|
2289
|
-
if (!accepted) draining = true;
|
|
2290
|
-
return accepted;
|
|
2291
|
-
}
|
|
2292
|
-
});
|
|
2293
|
-
const tick = setInterval(() => {
|
|
2294
|
-
compositor.tick();
|
|
2295
|
-
}, Math.max(1, Math.round(1e3 / plan.fps)));
|
|
2296
|
-
tick.unref();
|
|
2297
2349
|
const urlByDeviceId = /* @__PURE__ */ new Map();
|
|
2298
2350
|
for (const [index, deviceId] of plan.sourceDeviceIds.entries()) {
|
|
2299
2351
|
const url = sources[index]?.url;
|
|
@@ -2333,7 +2385,7 @@ async function startGridCompositorChild(ctx, plan, sources, onEnded) {
|
|
|
2333
2385
|
await decoder.start();
|
|
2334
2386
|
};
|
|
2335
2387
|
const stopAll = async () => {
|
|
2336
|
-
clearInterval(tick);
|
|
2388
|
+
if (tick !== null) clearInterval(tick);
|
|
2337
2389
|
compositor.close();
|
|
2338
2390
|
await Promise.allSettled(decoders.map((d) => d.stop()));
|
|
2339
2391
|
await encoder.stop();
|
|
@@ -2677,12 +2729,24 @@ var CameraGridAddon = class extends require_dist.BaseAddon {
|
|
|
2677
2729
|
for (const slot of slots) {
|
|
2678
2730
|
if (slot.sourceCamStreamId === null) continue;
|
|
2679
2731
|
const profiles = servedBy.get(slot.deviceId) ?? [];
|
|
2680
|
-
profiles.push(
|
|
2732
|
+
profiles.push({
|
|
2733
|
+
profile: slot.profile,
|
|
2734
|
+
width: slot.resolution?.width ?? 0,
|
|
2735
|
+
height: slot.resolution?.height ?? 0
|
|
2736
|
+
});
|
|
2681
2737
|
servedBy.set(slot.deviceId, profiles);
|
|
2682
2738
|
}
|
|
2683
2739
|
for (const instance of instances) {
|
|
2684
2740
|
const plan = gridSourceProfiles({
|
|
2685
|
-
|
|
2741
|
+
canvas: {
|
|
2742
|
+
width: instance.width,
|
|
2743
|
+
height: instance.height
|
|
2744
|
+
},
|
|
2745
|
+
cells: instance.cells.map((cell) => ({
|
|
2746
|
+
deviceId: cell.deviceId,
|
|
2747
|
+
source: cell.source,
|
|
2748
|
+
cell: cell.cell
|
|
2749
|
+
})),
|
|
2686
2750
|
profilesServedBy: (deviceId) => servedBy.get(deviceId) ?? null
|
|
2687
2751
|
});
|
|
2688
2752
|
const previous = this.planByInstanceId.get(instance.id);
|
|
@@ -951,18 +951,59 @@ var CHEAPEST_FIRST = [
|
|
|
951
951
|
function lowestServedProfile(served) {
|
|
952
952
|
return CHEAPEST_FIRST.find((profile) => served.includes(profile)) ?? null;
|
|
953
953
|
}
|
|
954
|
+
/** The pixels a cell needs BEHIND it, before the crop takes its share. */
|
|
955
|
+
function requiredSourceSize(cell, canvas) {
|
|
956
|
+
const tileWidth = cell.cell.width * canvas.width;
|
|
957
|
+
const tileHeight = cell.cell.height * canvas.height;
|
|
958
|
+
const cropWidth = cell.source.width > 0 ? cell.source.width : 1;
|
|
959
|
+
const cropHeight = cell.source.height > 0 ? cell.source.height : 1;
|
|
960
|
+
return {
|
|
961
|
+
width: tileWidth / cropWidth,
|
|
962
|
+
height: tileHeight / cropHeight
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* The cheapest stream that covers `need`, or the BIGGEST when none does.
|
|
967
|
+
*
|
|
968
|
+
* Soft beats absent: a camera whose best stream cannot fill its cell still
|
|
969
|
+
* composes, at the best it has. Refusing would take the whole grid down over
|
|
970
|
+
* one cell being drawn larger than its source.
|
|
971
|
+
*
|
|
972
|
+
* A stream that states NO size cannot be compared, so it is ordered as cheapest
|
|
973
|
+
* rather than discarded — an unmeasurable stream is still a stream, and the
|
|
974
|
+
* picture is what the operator asked for.
|
|
975
|
+
*/
|
|
976
|
+
function bestFit(served, need) {
|
|
977
|
+
if (served.length === 0) return null;
|
|
978
|
+
const order = new Map(CHEAPEST_FIRST.map((profile, index) => [profile, index]));
|
|
979
|
+
const cheapestFirst = [...served].sort((a, b) => (order.get(a.profile) ?? 0) - (order.get(b.profile) ?? 0));
|
|
980
|
+
const unmeasurable = cheapestFirst.find((s) => s.width <= 0 || s.height <= 0);
|
|
981
|
+
if (unmeasurable !== void 0) return unmeasurable.profile;
|
|
982
|
+
const covering = cheapestFirst.find((s) => s.width >= need.width && s.height >= need.height);
|
|
983
|
+
if (covering !== void 0) return covering.profile;
|
|
984
|
+
return cheapestFirst.reduce((best, s) => s.width * s.height > best.width * best.height ? s : best).profile;
|
|
985
|
+
}
|
|
954
986
|
/** Which stream to read each source at, and who cannot be read at all. */
|
|
955
987
|
function gridSourceProfiles(input) {
|
|
988
|
+
const neededBySource = /* @__PURE__ */ new Map();
|
|
989
|
+
for (const cell of input.cells) {
|
|
990
|
+
const need = requiredSourceSize(cell, input.canvas);
|
|
991
|
+
const current = neededBySource.get(cell.deviceId);
|
|
992
|
+
neededBySource.set(cell.deviceId, {
|
|
993
|
+
width: Math.max(current?.width ?? 0, need.width),
|
|
994
|
+
height: Math.max(current?.height ?? 0, need.height)
|
|
995
|
+
});
|
|
996
|
+
}
|
|
956
997
|
const profileBySource = /* @__PURE__ */ new Map();
|
|
957
998
|
const missingSources = [];
|
|
958
|
-
for (const deviceId of
|
|
999
|
+
for (const [deviceId, need] of neededBySource) {
|
|
959
1000
|
const served = input.profilesServedBy(deviceId);
|
|
960
|
-
const chosen = served === null ? null :
|
|
1001
|
+
const chosen = served === null ? null : bestFit(served, need);
|
|
961
1002
|
if (chosen === null) missingSources.push(deviceId);
|
|
962
1003
|
else profileBySource.set(deviceId, chosen);
|
|
963
1004
|
}
|
|
964
1005
|
return {
|
|
965
|
-
offered:
|
|
1006
|
+
offered: neededBySource.size > 0 && missingSources.length === 0,
|
|
966
1007
|
profileBySource,
|
|
967
1008
|
missingSources
|
|
968
1009
|
};
|
|
@@ -2238,6 +2279,25 @@ async function startGridCompositorChild(ctx, plan, sources, onEnded) {
|
|
|
2238
2279
|
};
|
|
2239
2280
|
let encoderIn = null;
|
|
2240
2281
|
let encoderOut = null;
|
|
2282
|
+
let draining = false;
|
|
2283
|
+
let tick = null;
|
|
2284
|
+
const compositor = new GridCompositor({
|
|
2285
|
+
plan: tilePlan,
|
|
2286
|
+
write: (frame) => {
|
|
2287
|
+
const sink = encoderIn;
|
|
2288
|
+
if (sink === null || draining || sink.destroyed) return false;
|
|
2289
|
+
const accepted = sink.write(frame);
|
|
2290
|
+
if (!accepted) draining = true;
|
|
2291
|
+
return accepted;
|
|
2292
|
+
}
|
|
2293
|
+
});
|
|
2294
|
+
const startTick = () => {
|
|
2295
|
+
if (tick !== null) return;
|
|
2296
|
+
tick = setInterval(() => {
|
|
2297
|
+
compositor.tick();
|
|
2298
|
+
}, Math.max(1, Math.round(1e3 / plan.fps)));
|
|
2299
|
+
tick.unref();
|
|
2300
|
+
};
|
|
2241
2301
|
const encoder = new FfmpegProcess({
|
|
2242
2302
|
binaryPath: ctx.binaryPath,
|
|
2243
2303
|
buildArgs: () => buildFfmpegArgs(gridCanvasEncoderInvocation({
|
|
@@ -2261,34 +2321,26 @@ async function startGridCompositorChild(ctx, plan, sources, onEnded) {
|
|
|
2261
2321
|
onChild: (child) => {
|
|
2262
2322
|
encoderIn = child.stdin;
|
|
2263
2323
|
if (child.stdout) encoderOut = child.stdout;
|
|
2324
|
+
child.stdin?.on("drain", () => {
|
|
2325
|
+
draining = false;
|
|
2326
|
+
});
|
|
2327
|
+
startTick();
|
|
2264
2328
|
child.stderr?.on("data", () => {});
|
|
2265
2329
|
child.stdin?.on("error", () => {});
|
|
2266
2330
|
},
|
|
2267
2331
|
onExit: (exit) => end(`encoder:${exit.classification}`)
|
|
2268
2332
|
});
|
|
2269
|
-
|
|
2333
|
+
try {
|
|
2334
|
+
await encoder.start();
|
|
2335
|
+
} catch (error) {
|
|
2336
|
+
if (tick !== null) clearInterval(tick);
|
|
2337
|
+
compositor.close();
|
|
2338
|
+
throw error;
|
|
2339
|
+
}
|
|
2270
2340
|
if (encoderIn === null || encoderOut === null) {
|
|
2271
2341
|
await encoder.stop();
|
|
2272
2342
|
throw new Error("camera-grid: the canvas encoder exposed no stdin/stdout to compose into");
|
|
2273
2343
|
}
|
|
2274
|
-
let draining = false;
|
|
2275
|
-
const sink = encoderIn;
|
|
2276
|
-
sink.on("drain", () => {
|
|
2277
|
-
draining = false;
|
|
2278
|
-
});
|
|
2279
|
-
const compositor = new GridCompositor({
|
|
2280
|
-
plan: tilePlan,
|
|
2281
|
-
write: (frame) => {
|
|
2282
|
-
if (draining || sink.destroyed) return false;
|
|
2283
|
-
const accepted = sink.write(frame);
|
|
2284
|
-
if (!accepted) draining = true;
|
|
2285
|
-
return accepted;
|
|
2286
|
-
}
|
|
2287
|
-
});
|
|
2288
|
-
const tick = setInterval(() => {
|
|
2289
|
-
compositor.tick();
|
|
2290
|
-
}, Math.max(1, Math.round(1e3 / plan.fps)));
|
|
2291
|
-
tick.unref();
|
|
2292
2344
|
const urlByDeviceId = /* @__PURE__ */ new Map();
|
|
2293
2345
|
for (const [index, deviceId] of plan.sourceDeviceIds.entries()) {
|
|
2294
2346
|
const url = sources[index]?.url;
|
|
@@ -2328,7 +2380,7 @@ async function startGridCompositorChild(ctx, plan, sources, onEnded) {
|
|
|
2328
2380
|
await decoder.start();
|
|
2329
2381
|
};
|
|
2330
2382
|
const stopAll = async () => {
|
|
2331
|
-
clearInterval(tick);
|
|
2383
|
+
if (tick !== null) clearInterval(tick);
|
|
2332
2384
|
compositor.close();
|
|
2333
2385
|
await Promise.allSettled(decoders.map((d) => d.stop()));
|
|
2334
2386
|
await encoder.stop();
|
|
@@ -2672,12 +2724,24 @@ var CameraGridAddon = class extends BaseAddon {
|
|
|
2672
2724
|
for (const slot of slots) {
|
|
2673
2725
|
if (slot.sourceCamStreamId === null) continue;
|
|
2674
2726
|
const profiles = servedBy.get(slot.deviceId) ?? [];
|
|
2675
|
-
profiles.push(
|
|
2727
|
+
profiles.push({
|
|
2728
|
+
profile: slot.profile,
|
|
2729
|
+
width: slot.resolution?.width ?? 0,
|
|
2730
|
+
height: slot.resolution?.height ?? 0
|
|
2731
|
+
});
|
|
2676
2732
|
servedBy.set(slot.deviceId, profiles);
|
|
2677
2733
|
}
|
|
2678
2734
|
for (const instance of instances) {
|
|
2679
2735
|
const plan = gridSourceProfiles({
|
|
2680
|
-
|
|
2736
|
+
canvas: {
|
|
2737
|
+
width: instance.width,
|
|
2738
|
+
height: instance.height
|
|
2739
|
+
},
|
|
2740
|
+
cells: instance.cells.map((cell) => ({
|
|
2741
|
+
deviceId: cell.deviceId,
|
|
2742
|
+
source: cell.source,
|
|
2743
|
+
cell: cell.cell
|
|
2744
|
+
})),
|
|
2681
2745
|
profilesServedBy: (deviceId) => servedBy.get(deviceId) ?? null
|
|
2682
2746
|
});
|
|
2683
2747
|
const previous = this.planByInstanceId.get(instance.id);
|