@camstack/addon-pipeline 1.1.52 → 1.1.54
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/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +191 -12
- package/dist/detection-pipeline/index.mjs +191 -12
- package/dist/{dist-B-pQhc30.js → dist-BbaoC680.js} +111 -7
- package/dist/{dist-DM6-eQRs.mjs → dist-CEcTeu1h.mjs} +111 -7
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +114 -5
- package/dist/pipeline-runner/index.mjs +114 -5
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/{remote-source-plane-DivMEw6Y.mjs → remote-source-plane-CNCqC_XG.mjs} +1 -1
- package/dist/{remote-source-plane-dSe5NlH9.js → remote-source-plane-vN45KFSm.js} +1 -1
- package/dist/session-decode/decode-worker-child.js +319 -35
- package/dist/session-decode/decode-worker-child.mjs +319 -35
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-D6UvBYwr.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-HS0tc6Sa.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-B3gTdHEh.mjs +26 -0
- package/dist/stream-broker/{_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-C9fwKMfg.mjs → _virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-B_v5r5ya.mjs} +1 -1
- package/dist/stream-broker/{hostInit-DpF_eilj.mjs → hostInit-DgohfEDA.mjs} +3 -3
- package/dist/stream-broker/index.js +2 -2
- package/dist/stream-broker/index.mjs +2 -2
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-pk7qdYXt.mjs → worker-protocol-DGIt_waM.mjs} +6 -0
- package/dist/{worker-protocol-BCfO8gUF.js → worker-protocol-DkL6GDxe.js} +6 -0
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DRCvljqU.js → MaskShapeCanvas-DI4BY7W2-CA2Hd6xx.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-DmQAEfWC.js → MotionZonesSettings-NcxxQN8r-DUN1VeC7.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-DNJ5RGGN.js → PrivacyMaskSettings-APgPLF7p-BqZIAU5s.js} +1 -1
- package/embed-dist/assets/index-CC06JBcl.css +2 -0
- package/embed-dist/assets/{index-BHKG2Pw-.js → index-CMkYypxB.js} +10 -10
- package/embed-dist/index.html +2 -2
- package/package.json +1 -1
- package/python/inference_pool.py +42 -1
- package/python/test_inference_pool_layout.py +44 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-o4tu_xuc.mjs +0 -26
- package/embed-dist/assets/index-JqUY2p33.css +0 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { $ as boolean, D as nodePin, H as createEvent, I as errMsg, K as makeSourceBrokerId, L as BaseAddon, Q as array, V as EventCategory, Z as _enum, _ as customAction, it as object, k as pipelineRunnerCapability, ot as string, rt as number, tt as lazy, y as defineCustomActions } from "../dist-
|
|
2
|
-
import { n as isRemoteRestream, r as profileForStreamId, t as RemoteSourcePlane } from "../remote-source-plane-
|
|
1
|
+
import { $ as boolean, D as nodePin, H as createEvent, I as errMsg, K as makeSourceBrokerId, L as BaseAddon, Q as array, V as EventCategory, Z as _enum, _ as customAction, it as object, k as pipelineRunnerCapability, ot as string, rt as number, tt as lazy, y as defineCustomActions } from "../dist-CEcTeu1h.mjs";
|
|
2
|
+
import { n as isRemoteRestream, r as profileForStreamId, t as RemoteSourcePlane } from "../remote-source-plane-CNCqC_XG.mjs";
|
|
3
3
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
4
|
-
import { t as isWorkerReply } from "../worker-protocol-
|
|
4
|
+
import { t as isWorkerReply } from "../worker-protocol-DGIt_waM.mjs";
|
|
5
5
|
import { DecoderFrameRingSink, FrameRingReaderCache, unlinkSegment } from "@camstack/shm-ring";
|
|
6
6
|
import { fork } from "node:child_process";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -1451,6 +1451,9 @@ var SessionDecodeSession = class {
|
|
|
1451
1451
|
frameTimeoutMs;
|
|
1452
1452
|
pendingPull = null;
|
|
1453
1453
|
pendingToBuffers = /* @__PURE__ */ new Map();
|
|
1454
|
+
/** Outstanding `nativeCrop` requests, keyed by the monotonic `requestId`. */
|
|
1455
|
+
pendingNativeCrops = /* @__PURE__ */ new Map();
|
|
1456
|
+
nextNativeCropId = 1;
|
|
1454
1457
|
exited = false;
|
|
1455
1458
|
sigkillTimer = null;
|
|
1456
1459
|
pullTimeoutTimer = null;
|
|
@@ -1515,6 +1518,30 @@ var SessionDecodeSession = class {
|
|
|
1515
1518
|
}
|
|
1516
1519
|
});
|
|
1517
1520
|
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Send `nativeCrop` for `frameId` and resolve with the crop result, or `null`
|
|
1523
|
+
* on any miss (frame aged out, worker error, or worker already exited). Never
|
|
1524
|
+
* rejects — native crops are strictly best-effort with a free fallback.
|
|
1525
|
+
*/
|
|
1526
|
+
requestNativeCrop(frameId, bbox, maxWidth) {
|
|
1527
|
+
if (this.exited) return Promise.resolve(null);
|
|
1528
|
+
const requestId = this.nextNativeCropId++;
|
|
1529
|
+
return new Promise((resolve) => {
|
|
1530
|
+
this.pendingNativeCrops.set(requestId, { resolve });
|
|
1531
|
+
try {
|
|
1532
|
+
this.child.send({
|
|
1533
|
+
kind: "nativeCrop",
|
|
1534
|
+
requestId,
|
|
1535
|
+
frameId,
|
|
1536
|
+
bbox,
|
|
1537
|
+
...maxWidth !== void 0 ? { maxWidth } : {}
|
|
1538
|
+
});
|
|
1539
|
+
} catch {
|
|
1540
|
+
this.pendingNativeCrops.delete(requestId);
|
|
1541
|
+
resolve(null);
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1518
1545
|
/** Teardown: `stop`, `kill()`, arm the SIGKILL fallback unless already exited. */
|
|
1519
1546
|
teardown() {
|
|
1520
1547
|
this.clearPullTimeout();
|
|
@@ -1547,6 +1574,16 @@ var SessionDecodeSession = class {
|
|
|
1547
1574
|
case "buffer":
|
|
1548
1575
|
this.settleToBuffer(raw.frameId, raw.bytes);
|
|
1549
1576
|
return;
|
|
1577
|
+
case "nativeCropResult":
|
|
1578
|
+
this.settleNativeCrop(raw.requestId, {
|
|
1579
|
+
bytes: raw.bytes,
|
|
1580
|
+
width: raw.width,
|
|
1581
|
+
height: raw.height
|
|
1582
|
+
});
|
|
1583
|
+
return;
|
|
1584
|
+
case "nativeCropMiss":
|
|
1585
|
+
this.settleNativeCrop(raw.requestId, null);
|
|
1586
|
+
return;
|
|
1550
1587
|
case "error":
|
|
1551
1588
|
this.settleError(raw.message, raw.frameId);
|
|
1552
1589
|
return;
|
|
@@ -1566,6 +1603,15 @@ var SessionDecodeSession = class {
|
|
|
1566
1603
|
}
|
|
1567
1604
|
for (const waiters of this.pendingToBuffers.values()) for (const waiter of waiters) waiter.reject(/* @__PURE__ */ new Error("session-decode-coordinator: worker exited before toBuffer reply"));
|
|
1568
1605
|
this.pendingToBuffers.clear();
|
|
1606
|
+
for (const waiter of this.pendingNativeCrops.values()) waiter.resolve(null);
|
|
1607
|
+
this.pendingNativeCrops.clear();
|
|
1608
|
+
}
|
|
1609
|
+
/** Settle the `nativeCrop` waiter for `requestId` (dropping unknown/stale ids). */
|
|
1610
|
+
settleNativeCrop(requestId, result) {
|
|
1611
|
+
const waiter = this.pendingNativeCrops.get(requestId);
|
|
1612
|
+
if (!waiter) return;
|
|
1613
|
+
this.pendingNativeCrops.delete(requestId);
|
|
1614
|
+
waiter.resolve(result);
|
|
1569
1615
|
}
|
|
1570
1616
|
settlePendingPull(outcome) {
|
|
1571
1617
|
const waiter = this.pendingPull;
|
|
@@ -1697,10 +1743,12 @@ function buildFrameImage(session, reply) {
|
|
|
1697
1743
|
width: reply.width,
|
|
1698
1744
|
height: reply.height,
|
|
1699
1745
|
format: reply.format,
|
|
1746
|
+
frameId: reply.frameId,
|
|
1700
1747
|
toBuffer: async (opts) => {
|
|
1701
1748
|
if (closed) throw new Error(`session-decode-coordinator: toBuffer() called on a closed frame (frameId=${reply.frameId})`);
|
|
1702
1749
|
return session.requestBuffer(reply.frameId, opts);
|
|
1703
1750
|
},
|
|
1751
|
+
nativeCrop: (bbox, maxWidth) => session.requestNativeCrop(reply.frameId, bbox, maxWidth),
|
|
1704
1752
|
close: () => {
|
|
1705
1753
|
closed = true;
|
|
1706
1754
|
}
|
|
@@ -1841,7 +1889,9 @@ function startSessionDecodePump(deps) {
|
|
|
1841
1889
|
}
|
|
1842
1890
|
try {
|
|
1843
1891
|
const decoded = await toDecodedFrame(frame, format);
|
|
1844
|
-
|
|
1892
|
+
const image = frame.image;
|
|
1893
|
+
const nativeCrop = image.nativeCrop ? (bbox, maxWidth) => image.nativeCrop(bbox, maxWidth) : void 0;
|
|
1894
|
+
deps.onDecodedFrame(decoded, nativeCrop);
|
|
1845
1895
|
} catch (err) {
|
|
1846
1896
|
deps.logger.debug("session-decode pump: skipped a frame (decode/consume error)", {
|
|
1847
1897
|
tags: { deviceId: deps.source.deviceId },
|
|
@@ -1874,6 +1924,40 @@ function startSessionDecodePump(deps) {
|
|
|
1874
1924
|
iterator.return().catch(() => {});
|
|
1875
1925
|
};
|
|
1876
1926
|
}
|
|
1927
|
+
var NativeCropRegistry = class {
|
|
1928
|
+
capacity;
|
|
1929
|
+
entries = /* @__PURE__ */ new Map();
|
|
1930
|
+
constructor(capacity = 64) {
|
|
1931
|
+
this.capacity = capacity;
|
|
1932
|
+
}
|
|
1933
|
+
/** Number of registered handles (tests / metrics). */
|
|
1934
|
+
get size() {
|
|
1935
|
+
return this.entries.size;
|
|
1936
|
+
}
|
|
1937
|
+
/** A retention frame is uniquely identified by its segment + slot + committed seq. */
|
|
1938
|
+
key(handle) {
|
|
1939
|
+
return `${handle.shmId}#${handle.slot}#${handle.seq}`;
|
|
1940
|
+
}
|
|
1941
|
+
/** Bind `request` to `handle`; evicts the oldest entry past the cap. */
|
|
1942
|
+
register(handle, request) {
|
|
1943
|
+
const k = this.key(handle);
|
|
1944
|
+
this.entries.delete(k);
|
|
1945
|
+
this.entries.set(k, request);
|
|
1946
|
+
while (this.entries.size > this.capacity) {
|
|
1947
|
+
const oldest = this.entries.keys().next().value;
|
|
1948
|
+
if (oldest === void 0) break;
|
|
1949
|
+
this.entries.delete(oldest);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
/** The requester for `handle`, or `null` if never registered / already evicted. */
|
|
1953
|
+
get(handle) {
|
|
1954
|
+
return this.entries.get(this.key(handle)) ?? null;
|
|
1955
|
+
}
|
|
1956
|
+
/** Drop every entry (node/addon shutdown). */
|
|
1957
|
+
clear() {
|
|
1958
|
+
this.entries.clear();
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1877
1961
|
//#endregion
|
|
1878
1962
|
//#region src/pipeline-runner/index.ts
|
|
1879
1963
|
var DEFAULT_CONFIG = {
|
|
@@ -2098,6 +2182,13 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2098
2182
|
* `resize()`d live in `onConfigChanged`. Null until `onInitialize`.
|
|
2099
2183
|
*/
|
|
2100
2184
|
spawnGate = null;
|
|
2185
|
+
/**
|
|
2186
|
+
* Node-level map from a retention-ring `FrameHandle` to the decode worker
|
|
2187
|
+
* still holding that frame's NATIVE surface — backs the `getNativeCrop` cap
|
|
2188
|
+
* so post-analysis can cut native-res crops (best-effort; a miss falls back
|
|
2189
|
+
* to the detection-frame crop). See {@link NativeCropRegistry}.
|
|
2190
|
+
*/
|
|
2191
|
+
nativeCropRegistry = new NativeCropRegistry();
|
|
2101
2192
|
attached = /* @__PURE__ */ new Map();
|
|
2102
2193
|
nodeId = "unknown";
|
|
2103
2194
|
metricsSnapshotTimer = null;
|
|
@@ -2667,6 +2758,23 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2667
2758
|
this.runner?.reportMotion(input.deviceId, input.detected, input.source, input.regions);
|
|
2668
2759
|
return { success: true };
|
|
2669
2760
|
}
|
|
2761
|
+
/**
|
|
2762
|
+
* Best-effort native-resolution crop for a retention-ring frame (native-crop
|
|
2763
|
+
* feature). Looks the handle up in {@link nativeCropRegistry}; a miss (never
|
|
2764
|
+
* registered, or the worker's tiny native ring already evicted the frame)
|
|
2765
|
+
* returns `null` so the caller falls back to a detection-frame crop. The
|
|
2766
|
+
* worker downloads ONLY the ROI — a full native frame never travels the wire.
|
|
2767
|
+
*/
|
|
2768
|
+
async getNativeCrop(input) {
|
|
2769
|
+
const requester = this.nativeCropRegistry.get(input.handle);
|
|
2770
|
+
if (!requester) return null;
|
|
2771
|
+
const result = await requester(input.bbox, input.maxWidth);
|
|
2772
|
+
return result ? {
|
|
2773
|
+
bytes: result.bytes,
|
|
2774
|
+
width: result.width,
|
|
2775
|
+
height: result.height
|
|
2776
|
+
} : null;
|
|
2777
|
+
}
|
|
2670
2778
|
detachInternal(deviceId) {
|
|
2671
2779
|
const attachment = this.attached.get(deviceId);
|
|
2672
2780
|
if (!attachment) return;
|
|
@@ -2986,8 +3094,9 @@ var PipelineRunnerAddon = class extends BaseAddon {
|
|
|
2986
3094
|
format: "rgb"
|
|
2987
3095
|
},
|
|
2988
3096
|
acquireSpawnSlot: this.buildDecodeSpawnAcquire(log, "detect"),
|
|
2989
|
-
onDecodedFrame: (frame) => {
|
|
3097
|
+
onDecodedFrame: (frame, nativeCrop) => {
|
|
2990
3098
|
const handle = retentionSink.write(frame) ?? void 0;
|
|
3099
|
+
if (handle && nativeCrop) this.nativeCropRegistry.register(handle, nativeCrop);
|
|
2991
3100
|
this.enqueueSharedDetectionFrame(config.deviceId, frame, handle);
|
|
2992
3101
|
}
|
|
2993
3102
|
});
|
package/dist/recorder/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const require_chunk = require("../chunk-D6vf50IK.js");
|
|
2
|
-
const require_dist = require("../dist-
|
|
2
|
+
const require_dist = require("../dist-BbaoC680.js");
|
|
3
3
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
4
4
|
const require_model_download_service_C_IHWnXx = require("../model-download-service-C-IHWnXx-BLXzxpRB.js");
|
|
5
5
|
let node_child_process = require("node:child_process");
|
package/dist/recorder/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as recordingCapability, B as DeviceType, D as nodePin, I as errMsg, L as BaseAddon, M as storageEvictableCapability, T as migrateConfigToBands, U as hydrateSchema, V as EventCategory, Y as selectAssignedProfileSlots, at as record, ot as string, s as EVENT_PAD_MS, u as RecordingConfigSchema } from "../dist-
|
|
1
|
+
import { A as recordingCapability, B as DeviceType, D as nodePin, I as errMsg, L as BaseAddon, M as storageEvictableCapability, T as migrateConfigToBands, U as hydrateSchema, V as EventCategory, Y as selectAssignedProfileSlots, at as record, ot as string, s as EVENT_PAD_MS, u as RecordingConfigSchema } from "../dist-CEcTeu1h.mjs";
|
|
2
2
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
3
3
|
import { t as createFileDataPlaneHandler } from "../model-download-service-C-IHWnXx-BPy6aoAx.mjs";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D as nodePin, I as errMsg, d as RingBuffer } from "./dist-
|
|
1
|
+
import { D as nodePin, I as errMsg, d as RingBuffer } from "./dist-CEcTeu1h.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
//#region src/stream-broker/stream-broker/decoder-session-proxy.ts
|
|
4
4
|
/**
|
|
@@ -1,11 +1,85 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("../chunk-D6vf50IK.js");
|
|
3
|
-
const require_worker_protocol = require("../worker-protocol-
|
|
3
|
+
const require_worker_protocol = require("../worker-protocol-DkL6GDxe.js");
|
|
4
4
|
let node_url = require("node:url");
|
|
5
5
|
let node_fs = require("node:fs");
|
|
6
6
|
node_fs = require_chunk.__toESM(node_fs);
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
node_path = require_chunk.__toESM(node_path);
|
|
9
|
+
//#region src/session-decode/crop-geometry.ts
|
|
10
|
+
/** Clamp to [min,max] then floor to the nearest even number (YUV420P chroma is 2x2 subsampled). */
|
|
11
|
+
function clampEven(value, min, max) {
|
|
12
|
+
const bounded = Math.min(Math.max(value, min), max);
|
|
13
|
+
return bounded - bounded % 2;
|
|
14
|
+
}
|
|
15
|
+
/** Resolve `FrameImageOptions.crop` against the decoded frame's dimensions, defaulting to the full frame. */
|
|
16
|
+
function resolveCropRegion(frameWidth, frameHeight, crop) {
|
|
17
|
+
const left = clampEven(crop?.left ?? 0, 0, Math.max(0, frameWidth - 2));
|
|
18
|
+
const top = clampEven(crop?.top ?? 0, 0, Math.max(0, frameHeight - 2));
|
|
19
|
+
return {
|
|
20
|
+
left,
|
|
21
|
+
top,
|
|
22
|
+
width: clampEven(crop?.width ?? frameWidth - left, 2, frameWidth - left),
|
|
23
|
+
height: clampEven(crop?.height ?? frameHeight - top, 2, frameHeight - top)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Resolve `FrameImageOptions.resize`, defaulting to the (already cropped) source dimensions. */
|
|
27
|
+
function resolveResizeTarget(cropWidth, cropHeight, resize) {
|
|
28
|
+
if (!resize) return {
|
|
29
|
+
width: cropWidth,
|
|
30
|
+
height: cropHeight
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
width: Math.max(1, Math.round(resize.width)),
|
|
34
|
+
height: Math.max(1, Math.round(resize.height))
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Whether `region` covers the entire `srcW x srcH` frame (i.e. no real crop). */
|
|
38
|
+
function isFullFrameRegion(region, srcW, srcH) {
|
|
39
|
+
return region.left === 0 && region.top === 0 && region.width === srcW && region.height === srcH;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a NORMALIZED [0,1] bbox against a native frame's real pixel dims into
|
|
43
|
+
* an even-aligned pixel crop region. Clamps the box inside the frame so a
|
|
44
|
+
* padded / slightly-out-of-frame bbox never reads past the plane; a degenerate
|
|
45
|
+
* (zero-area) box is widened to the 2px chroma minimum by
|
|
46
|
+
* {@link resolveCropRegion}'s own clamps.
|
|
47
|
+
*/
|
|
48
|
+
function normalizedBboxToCrop(frameWidth, frameHeight, bbox) {
|
|
49
|
+
return {
|
|
50
|
+
left: Math.round(bbox.x * frameWidth),
|
|
51
|
+
top: Math.round(bbox.y * frameHeight),
|
|
52
|
+
width: Math.max(2, Math.round(bbox.w * frameWidth)),
|
|
53
|
+
height: Math.max(2, Math.round(bbox.h * frameHeight))
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Aspect-preserving cap of a native crop region to `maxWidth`. Returns
|
|
58
|
+
* `undefined` (no resize → true native crop) when `maxWidth` is absent or the
|
|
59
|
+
* region is already within it.
|
|
60
|
+
*/
|
|
61
|
+
function resolveNativeCropTarget(region, maxWidth) {
|
|
62
|
+
if (maxWidth === void 0 || maxWidth <= 0 || region.width <= maxWidth) return void 0;
|
|
63
|
+
const width = Math.round(maxWidth);
|
|
64
|
+
return {
|
|
65
|
+
width,
|
|
66
|
+
height: Math.max(1, Math.round(width * region.height / region.width))
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Full native-crop geometry: normalized bbox → even-aligned source region +
|
|
71
|
+
* (optionally width-capped) output target. The single mapping the decode worker
|
|
72
|
+
* uses to turn a detection-res bbox into a native-res crop request.
|
|
73
|
+
*/
|
|
74
|
+
function resolveNativeCropGeometry(frameWidth, frameHeight, bbox, maxWidth) {
|
|
75
|
+
const region = resolveCropRegion(frameWidth, frameHeight, normalizedBboxToCrop(frameWidth, frameHeight, bbox));
|
|
76
|
+
const resize = resolveNativeCropTarget(region, maxWidth);
|
|
77
|
+
return {
|
|
78
|
+
region,
|
|
79
|
+
target: resolveResizeTarget(region.width, region.height, resize)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
9
83
|
//#region src/session-decode/frame-slot.ts
|
|
10
84
|
function toReply(held) {
|
|
11
85
|
return {
|
|
@@ -20,6 +94,10 @@ function toReply(held) {
|
|
|
20
94
|
* pending-pull flag. One instance per decode-worker child session.
|
|
21
95
|
*/
|
|
22
96
|
var FrameSlot = class {
|
|
97
|
+
retainSuperseded;
|
|
98
|
+
constructor(options) {
|
|
99
|
+
this.retainSuperseded = options?.retainSuperseded;
|
|
100
|
+
}
|
|
23
101
|
nextFrameId = 1;
|
|
24
102
|
slot = null;
|
|
25
103
|
/**
|
|
@@ -104,11 +182,61 @@ var FrameSlot = class {
|
|
|
104
182
|
const previousReserved = this.reserved;
|
|
105
183
|
this.reserved = held;
|
|
106
184
|
this.slot = null;
|
|
107
|
-
previousReserved
|
|
185
|
+
if (previousReserved) {
|
|
186
|
+
if (!(this.retainSuperseded?.(previousReserved.frameId, previousReserved.frame) ?? false)) previousReserved.frame.free();
|
|
187
|
+
}
|
|
108
188
|
return toReply(held);
|
|
109
189
|
}
|
|
110
190
|
};
|
|
111
191
|
//#endregion
|
|
192
|
+
//#region src/session-decode/native-frame-ring.ts
|
|
193
|
+
/**
|
|
194
|
+
* A hard-capped, insertion-ordered map from worker frameId → retained frame.
|
|
195
|
+
* `capacity <= 0` disables retention entirely (every `retain` frees immediately
|
|
196
|
+
* and returns false) — the safe "keep the ring off" knob.
|
|
197
|
+
*/
|
|
198
|
+
var NativeFrameRing = class {
|
|
199
|
+
capacity;
|
|
200
|
+
/** Insertion-ordered (Map preserves insertion order) frameId → frame. */
|
|
201
|
+
frames = /* @__PURE__ */ new Map();
|
|
202
|
+
constructor(capacity) {
|
|
203
|
+
this.capacity = capacity;
|
|
204
|
+
}
|
|
205
|
+
/** Number of frames currently retained (for tests / metrics). */
|
|
206
|
+
get size() {
|
|
207
|
+
return this.frames.size;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Take ownership of `frame` under `frameId`. Frees the oldest retained frame
|
|
211
|
+
* when the cap is exceeded (FIFO by frameId insertion). Returns `true` when
|
|
212
|
+
* the ring took ownership (caller MUST NOT free), `false` when retention is
|
|
213
|
+
* disabled (`capacity <= 0`) and the caller still owns/should free the frame.
|
|
214
|
+
*/
|
|
215
|
+
retain(frameId, frame) {
|
|
216
|
+
if (this.capacity <= 0) return false;
|
|
217
|
+
const existing = this.frames.get(frameId);
|
|
218
|
+
if (existing) existing.free();
|
|
219
|
+
this.frames.set(frameId, frame);
|
|
220
|
+
while (this.frames.size > this.capacity) {
|
|
221
|
+
const oldestKey = this.frames.keys().next().value;
|
|
222
|
+
if (oldestKey === void 0) break;
|
|
223
|
+
const oldest = this.frames.get(oldestKey);
|
|
224
|
+
this.frames.delete(oldestKey);
|
|
225
|
+
oldest?.free();
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
/** The retained frame for `frameId`, or `null` if never retained / already evicted. */
|
|
230
|
+
get(frameId) {
|
|
231
|
+
return this.frames.get(frameId) ?? null;
|
|
232
|
+
}
|
|
233
|
+
/** Free and drop every retained frame. Idempotent (re-dial + teardown call it). */
|
|
234
|
+
clear() {
|
|
235
|
+
for (const frame of this.frames.values()) frame.free();
|
|
236
|
+
this.frames.clear();
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
//#endregion
|
|
112
240
|
//#region src/session-decode/decode-worker-child.ts
|
|
113
241
|
/**
|
|
114
242
|
* Decode-worker CHILD entry point — Epic C P1, Task 2.
|
|
@@ -139,6 +267,20 @@ var REDIAL_MS = 3e3;
|
|
|
139
267
|
/** How often the worker emits its `framesDecoded/framesSkipped/deliveredFps` line. */
|
|
140
268
|
var METRICS_INTERVAL_MS = 1e4;
|
|
141
269
|
/**
|
|
270
|
+
* Hard cap on the native-frame retention ring (native-res crop feature). Kept
|
|
271
|
+
* DELIBERATELY TINY — its only job is to cover the detection→crop latency
|
|
272
|
+
* (typically 1-4 frames), NOT the full shm ring depth. On the HW path every
|
|
273
|
+
* retained slot pins a GPU/VAAPI surface out of the decoder pool (the historic
|
|
274
|
+
* leak site), so the cap MUST stay small; a miss is free (caller falls back to
|
|
275
|
+
* today's detection-frame crop). Override with `CAMSTACK_SESSION_NATIVE_CROP_RING`
|
|
276
|
+
* (0 disables retention entirely); clamped to [0,4], default 2.
|
|
277
|
+
*/
|
|
278
|
+
var NATIVE_RING_CAP = (() => {
|
|
279
|
+
const raw = Number(process.env["CAMSTACK_SESSION_NATIVE_CROP_RING"]);
|
|
280
|
+
if (!Number.isFinite(raw)) return 2;
|
|
281
|
+
return Math.min(4, Math.max(0, Math.floor(raw)));
|
|
282
|
+
})();
|
|
283
|
+
/**
|
|
142
284
|
* The libav GPU scale filter for a hwaccel backend, or `null` when none is
|
|
143
285
|
* known here — those backends fall back to the software crop+scale path.
|
|
144
286
|
* Mirrors `addon-decoder-ffmpeg/src/ffmpeg-args.ts` `gpuScaleFilterForBackend`.
|
|
@@ -276,37 +418,6 @@ function backendToHwDeviceConst(backend, consts) {
|
|
|
276
418
|
default: return null;
|
|
277
419
|
}
|
|
278
420
|
}
|
|
279
|
-
/** Clamp to [min,max] then floor to the nearest even number (YUV420P chroma is 2x2 subsampled). */
|
|
280
|
-
function clampEven(value, min, max) {
|
|
281
|
-
const bounded = Math.min(Math.max(value, min), max);
|
|
282
|
-
return bounded - bounded % 2;
|
|
283
|
-
}
|
|
284
|
-
/** Resolve `FrameImageOptions.crop` against the decoded frame's dimensions, defaulting to the full frame. */
|
|
285
|
-
function resolveCropRegion(frameWidth, frameHeight, crop) {
|
|
286
|
-
const left = clampEven(crop?.left ?? 0, 0, Math.max(0, frameWidth - 2));
|
|
287
|
-
const top = clampEven(crop?.top ?? 0, 0, Math.max(0, frameHeight - 2));
|
|
288
|
-
return {
|
|
289
|
-
left,
|
|
290
|
-
top,
|
|
291
|
-
width: clampEven(crop?.width ?? frameWidth - left, 2, frameWidth - left),
|
|
292
|
-
height: clampEven(crop?.height ?? frameHeight - top, 2, frameHeight - top)
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
/** Resolve `FrameImageOptions.resize`, defaulting to the (already cropped) source dimensions. */
|
|
296
|
-
function resolveResizeTarget(cropWidth, cropHeight, resize) {
|
|
297
|
-
if (!resize) return {
|
|
298
|
-
width: cropWidth,
|
|
299
|
-
height: cropHeight
|
|
300
|
-
};
|
|
301
|
-
return {
|
|
302
|
-
width: Math.max(1, Math.round(resize.width)),
|
|
303
|
-
height: Math.max(1, Math.round(resize.height))
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
/** Whether `region` covers the entire `srcW x srcH` frame (i.e. no real crop). */
|
|
307
|
-
function isFullFrameRegion(region, srcW, srcH) {
|
|
308
|
-
return region.left === 0 && region.top === 0 && region.width === srcW && region.height === srcH;
|
|
309
|
-
}
|
|
310
421
|
/**
|
|
311
422
|
* Build the libav filtergraph description for the GPU crop+scale path.
|
|
312
423
|
*
|
|
@@ -388,6 +499,30 @@ var DecodeWorkerChild = class {
|
|
|
388
499
|
*/
|
|
389
500
|
hwFilter = null;
|
|
390
501
|
hwFilterKey = "";
|
|
502
|
+
/**
|
|
503
|
+
* DEDICATED native-crop GPU filtergraph + software scaler, kept SEPARATE from
|
|
504
|
+
* the detection {@link hwFilter}/{@link scaler} so a native-crop request never
|
|
505
|
+
* evicts (rebuilds) the hot-path detection filter cache — the crop geometry
|
|
506
|
+
* differs from the stable detection geometry, so sharing one cache would
|
|
507
|
+
* thrash it. Same dispose discipline: both are closed on every re-dial
|
|
508
|
+
* ({@link closeInput}) and on teardown. Rebuilt lazily per crop geometry.
|
|
509
|
+
*/
|
|
510
|
+
nativeCropHwFilter = null;
|
|
511
|
+
nativeCropHwFilterKey = "";
|
|
512
|
+
nativeCropScaler = null;
|
|
513
|
+
nativeCropScalerKey = "";
|
|
514
|
+
/**
|
|
515
|
+
* Bounded native-frame retention ring — the survival window for a frame's
|
|
516
|
+
* NATIVE surface between detection (on the shipped small frame) and the crop
|
|
517
|
+
* request that follows. Hard-capped at {@link NATIVE_RING_CAP}; single
|
|
518
|
+
* ownership via the {@link FrameSlot} `retainSuperseded` hook (a frame enters
|
|
519
|
+
* the ring exactly when the slot would otherwise free it). Cleared on every
|
|
520
|
+
* re-dial + teardown (see {@link closeInput}).
|
|
521
|
+
*/
|
|
522
|
+
nativeRing = new NativeFrameRing(NATIVE_RING_CAP);
|
|
523
|
+
/** Throttled native-crop hit/miss counters, emitted with the throughput line. */
|
|
524
|
+
nativeCropHits = 0;
|
|
525
|
+
nativeCropMisses = 0;
|
|
391
526
|
/** Scrypted-style throughput counters (plain integers, no per-frame allocation). */
|
|
392
527
|
framesDecoded = 0;
|
|
393
528
|
/** Child-side skips only (HW-guard drops + probe/degrade frees); slot drops add {@link FrameSlot.droppedCount}. */
|
|
@@ -420,7 +555,7 @@ var DecodeWorkerChild = class {
|
|
|
420
555
|
* the reserve-on-pull invariant (`e3c78a9b`) is unit-tested without
|
|
421
556
|
* node-av. See that file's doc comment for the full semantics.
|
|
422
557
|
*/
|
|
423
|
-
frames = new FrameSlot();
|
|
558
|
+
frames = new FrameSlot({ retainSuperseded: (frameId, frame) => this.nativeRing.retain(frameId, frame) });
|
|
424
559
|
/**
|
|
425
560
|
* Mirrors whether `this.frames` currently has a pull waiting, so
|
|
426
561
|
* `teardown` knows to resolve it with `{kind:'ended'}` — `FrameSlot`
|
|
@@ -446,6 +581,9 @@ var DecodeWorkerChild = class {
|
|
|
446
581
|
case "toBuffer":
|
|
447
582
|
this.handleToBuffer(message.frameId, message.opts);
|
|
448
583
|
return;
|
|
584
|
+
case "nativeCrop":
|
|
585
|
+
this.handleNativeCrop(message.requestId, message.frameId, message.bbox, message.maxWidth);
|
|
586
|
+
return;
|
|
449
587
|
case "stop":
|
|
450
588
|
this.handleStop();
|
|
451
589
|
return;
|
|
@@ -470,6 +608,9 @@ var DecodeWorkerChild = class {
|
|
|
470
608
|
this.scaler?.[Symbol.dispose]?.();
|
|
471
609
|
this.scaler = null;
|
|
472
610
|
this.scalerKey = "";
|
|
611
|
+
this.nativeCropScaler?.[Symbol.dispose]?.();
|
|
612
|
+
this.nativeCropScaler = null;
|
|
613
|
+
this.nativeCropScalerKey = "";
|
|
473
614
|
this.hwContext?.[Symbol.dispose]?.();
|
|
474
615
|
this.hwContext = null;
|
|
475
616
|
if (this.pendingPull) {
|
|
@@ -484,7 +625,7 @@ var DecodeWorkerChild = class {
|
|
|
484
625
|
const deliveredDelta = this.framesDelivered - this.lastDeliveredSnapshot;
|
|
485
626
|
const deliveredFps = Math.round(deliveredDelta / windowMs * 1e3 * 10) / 10;
|
|
486
627
|
const skipped = this.framesSkipped + this.frames.droppedCount;
|
|
487
|
-
this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}}${final ? " (final)" : ""}\n`);
|
|
628
|
+
this.emitStderr(`session-decode metrics {framesDecoded:${this.framesDecoded}, framesSkipped:${skipped}, deliveredFps:${deliveredFps}, nativeCropHits:${this.nativeCropHits}, nativeCropMisses:${this.nativeCropMisses}}${final ? " (final)" : ""}\n`);
|
|
488
629
|
this.lastMetricsAt = now;
|
|
489
630
|
this.lastDeliveredSnapshot = this.framesDelivered;
|
|
490
631
|
}
|
|
@@ -534,6 +675,145 @@ var DecodeWorkerChild = class {
|
|
|
534
675
|
this.sendError(`decode-worker-child: toBuffer failed — ${errMessage(err)}`, frameId);
|
|
535
676
|
}
|
|
536
677
|
}
|
|
678
|
+
/**
|
|
679
|
+
* Best-effort NATIVE-resolution crop of a still-retained frame. Resolves the
|
|
680
|
+
* frame from the reserved slot (the latest, most common case) or the
|
|
681
|
+
* retention ring (an older frame detection has since moved past). A miss
|
|
682
|
+
* (frame aged out) or ANY crop error replies `nativeCropMiss` so the caller
|
|
683
|
+
* falls back to today's detection-frame crop — it NEVER degrades the
|
|
684
|
+
* detection session (unlike {@link hwScaleToBuffer}, which flips the whole
|
|
685
|
+
* session to software on a GPU-filter error).
|
|
686
|
+
*/
|
|
687
|
+
handleNativeCrop(requestId, frameId, bbox, maxWidth) {
|
|
688
|
+
const frame = this.nativeRing.get(frameId) ?? this.frames.toBuffer(frameId);
|
|
689
|
+
if (!frame) {
|
|
690
|
+
this.nativeCropMisses++;
|
|
691
|
+
this.send({
|
|
692
|
+
kind: "nativeCropMiss",
|
|
693
|
+
requestId
|
|
694
|
+
});
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
const resolved = this.resolveNativeCrop(frame, bbox, maxWidth);
|
|
699
|
+
const bytes = frame.isHwFrame() ? this.nativeHwCropToBuffer(frame, resolved) : this.nativeScaleCropToBuffer(frame, resolved);
|
|
700
|
+
this.nativeCropHits++;
|
|
701
|
+
this.send({
|
|
702
|
+
kind: "nativeCropResult",
|
|
703
|
+
requestId,
|
|
704
|
+
bytes,
|
|
705
|
+
width: resolved.target.width,
|
|
706
|
+
height: resolved.target.height
|
|
707
|
+
});
|
|
708
|
+
} catch (err) {
|
|
709
|
+
this.nativeCropMisses++;
|
|
710
|
+
this.emitStderr(`decode-worker-child: native crop failed (requestId ${requestId}) — ${errMessage(err)}\n`);
|
|
711
|
+
this.send({
|
|
712
|
+
kind: "nativeCropMiss",
|
|
713
|
+
requestId
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/** Map a normalized bbox to an even-aligned pixel crop + (optionally capped) target. */
|
|
718
|
+
resolveNativeCrop(frame, bbox, maxWidth) {
|
|
719
|
+
return resolveNativeCropGeometry(frame.width, frame.height, bbox, maxWidth);
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Software native crop — the leak-free path: offset the retained YUV420P
|
|
723
|
+
* frame's plane pointers to the crop origin (zero-copy) and scale ONLY the
|
|
724
|
+
* ROI into a tightly-packed rgb buffer via a DEDICATED scaler (never the
|
|
725
|
+
* detection {@link scaler}). Mirrors {@link scaleRoiToBuffer}, rgb-fixed.
|
|
726
|
+
*/
|
|
727
|
+
nativeScaleCropToBuffer(frame, resolved) {
|
|
728
|
+
const nav = this.nav;
|
|
729
|
+
const C = this.consts;
|
|
730
|
+
if (!nav || !C) throw new Error("node-av not initialized");
|
|
731
|
+
const { region, target } = resolved;
|
|
732
|
+
const planes = frame.data;
|
|
733
|
+
const strides = frame.linesize;
|
|
734
|
+
const yPlane = planes?.[0];
|
|
735
|
+
const uPlane = planes?.[1];
|
|
736
|
+
const vPlane = planes?.[2];
|
|
737
|
+
if (!yPlane || !uPlane || !vPlane) throw new Error("decode-worker-child: retained frame missing planar YUV420P data");
|
|
738
|
+
const yStride = strides[0] ?? 0;
|
|
739
|
+
const cStride = strides[1] ?? 0;
|
|
740
|
+
const ySlice = yPlane.subarray(region.top * yStride + region.left);
|
|
741
|
+
const uSlice = uPlane.subarray(region.top / 2 * cStride + region.left / 2);
|
|
742
|
+
const vSlice = vPlane.subarray(region.top / 2 * cStride + region.left / 2);
|
|
743
|
+
const scaler = this.ensureNativeCropScaler(nav, C, region.width, region.height, target.width, target.height);
|
|
744
|
+
const dstStride = target.width * 3;
|
|
745
|
+
const dstBuffer = Buffer.allocUnsafe(dstStride * target.height);
|
|
746
|
+
const scaledHeight = scaler.scaleSync([
|
|
747
|
+
ySlice,
|
|
748
|
+
uSlice,
|
|
749
|
+
vSlice
|
|
750
|
+
], [
|
|
751
|
+
yStride,
|
|
752
|
+
cStride,
|
|
753
|
+
cStride
|
|
754
|
+
], 0, region.height, [dstBuffer], [dstStride]);
|
|
755
|
+
if (scaledHeight < 0) throw new Error(`native crop sws_scale failed: ${scaledHeight}`);
|
|
756
|
+
if (scaledHeight !== target.height) throw new Error(`native crop scaler produced ${scaledHeight} of ${target.height} rows`);
|
|
757
|
+
return dstBuffer;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Hardware native crop — run the retained GPU surface through a DEDICATED
|
|
761
|
+
* crop filtergraph (`hwdownload,format=nv12,crop=…,scale=…,format=rgb24`) and
|
|
762
|
+
* pack the small result. Mirrors {@link hwScaleToBuffer} but uses its OWN
|
|
763
|
+
* cached filter and, on error, throws WITHOUT degrading the session.
|
|
764
|
+
*/
|
|
765
|
+
nativeHwCropToBuffer(frame, resolved) {
|
|
766
|
+
const { region, target } = resolved;
|
|
767
|
+
const outputs = this.ensureNativeCropHwFilter(region, target).processAllSync(frame);
|
|
768
|
+
const first = outputs[0];
|
|
769
|
+
if (!first) {
|
|
770
|
+
for (const out of outputs) out.free();
|
|
771
|
+
throw new Error("native crop GPU filtergraph produced no frame");
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
const plane = first.data?.[0];
|
|
775
|
+
if (!plane) throw new Error("native crop GPU frame missing packed plane data");
|
|
776
|
+
return packSinglePlane(plane, first.linesize[0] ?? 0, target.width, target.height, 3);
|
|
777
|
+
} finally {
|
|
778
|
+
for (const out of outputs) out.free();
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
/** Build-once / reuse the dedicated native-crop software scaler (rgb out). */
|
|
782
|
+
ensureNativeCropScaler(nav, C, srcW, srcH, dstW, dstH) {
|
|
783
|
+
const key = `${srcW}x${srcH}->${dstW}x${dstH}`;
|
|
784
|
+
const cached = this.nativeCropScaler;
|
|
785
|
+
if (cached && key === this.nativeCropScalerKey) return cached;
|
|
786
|
+
this.nativeCropScaler?.[Symbol.dispose]?.();
|
|
787
|
+
this.nativeCropScaler = null;
|
|
788
|
+
this.nativeCropScalerKey = "";
|
|
789
|
+
const scaler = new nav.SoftwareScaleContext();
|
|
790
|
+
scaler.getContext(srcW, srcH, C.AV_PIX_FMT_YUV420P, dstW, dstH, C.AV_PIX_FMT_RGB24, C.SWS_FAST_BILINEAR);
|
|
791
|
+
const initRet = scaler.initContext();
|
|
792
|
+
if (initRet < 0) {
|
|
793
|
+
scaler[Symbol.dispose]?.();
|
|
794
|
+
throw new Error(`native crop sws_init_context failed: ${initRet}`);
|
|
795
|
+
}
|
|
796
|
+
this.nativeCropScaler = scaler;
|
|
797
|
+
this.nativeCropScalerKey = key;
|
|
798
|
+
return scaler;
|
|
799
|
+
}
|
|
800
|
+
/** Build-once / reuse the dedicated native-crop GPU filtergraph (rgb out). */
|
|
801
|
+
ensureNativeCropHwFilter(region, target) {
|
|
802
|
+
const nav = this.nav;
|
|
803
|
+
const scaleFilter = this.gpuScaleFilter;
|
|
804
|
+
if (!nav || !scaleFilter || !this.hwContext) throw new Error("decode-worker-child: native GPU crop requested without a HW context");
|
|
805
|
+
const key = `${region.left},${region.top},${region.width},${region.height}->${target.width}x${target.height}`;
|
|
806
|
+
const cached = this.nativeCropHwFilter;
|
|
807
|
+
if (cached && key === this.nativeCropHwFilterKey) return cached;
|
|
808
|
+
this.nativeCropHwFilter?.close();
|
|
809
|
+
this.nativeCropHwFilter = null;
|
|
810
|
+
this.nativeCropHwFilterKey = "";
|
|
811
|
+
const description = buildGpuFilterDescription(scaleFilter, region, target, false, "rgb24");
|
|
812
|
+
const filter = nav.FilterAPI.create(description, { hardware: this.hwContext });
|
|
813
|
+
this.nativeCropHwFilter = filter;
|
|
814
|
+
this.nativeCropHwFilterKey = key;
|
|
815
|
+
return filter;
|
|
816
|
+
}
|
|
537
817
|
handleStop() {
|
|
538
818
|
this.teardown();
|
|
539
819
|
process.exit(0);
|
|
@@ -887,6 +1167,10 @@ var DecodeWorkerChild = class {
|
|
|
887
1167
|
* the next dial's first `toBuffer`.
|
|
888
1168
|
*/
|
|
889
1169
|
closeInput() {
|
|
1170
|
+
this.nativeRing.clear();
|
|
1171
|
+
this.nativeCropHwFilter?.close();
|
|
1172
|
+
this.nativeCropHwFilter = null;
|
|
1173
|
+
this.nativeCropHwFilterKey = "";
|
|
890
1174
|
this.hwFilter?.close();
|
|
891
1175
|
this.hwFilter = null;
|
|
892
1176
|
this.hwFilterKey = "";
|