@camstack/addon-pipeline 1.1.22 → 1.1.23

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.
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.1.17",
21
+ version: "1.1.18",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_stream_broker_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.1.17",
39
+ version: "1.1.18",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -8904,6 +8904,17 @@ var DEFAULT_HINT_FPS = 5;
8904
8904
  * broker's 2s `inputFps` window so the two stats are comparable. */
8905
8905
  var DECODE_FPS_WINDOW_MS = 2e3;
8906
8906
  /**
8907
+ * Keep-warm grace before a decoder session with no subscribers is torn down.
8908
+ *
8909
+ * Subscribers flap on ~30s cadences (occupancy recheck bursts, detection
8910
+ * motion-gating watch↔active, onboard gray gating). Destroying the
8911
+ * ffmpeg/VAAPI decode session the instant the last subscriber leaves spawns +
8912
+ * destroys a full session every cycle — wasted setup/teardown. Chosen >30s so
8913
+ * consecutive bursts and watch↔active flips reuse the warm session, yet a
8914
+ * genuinely idle session still frees its ffmpeg within ~a minute.
8915
+ */
8916
+ var SESSION_LINGER_MS = 45e3;
8917
+ /**
8907
8918
  * Owns the broker's shared-memory frame-handle subscriptions and the
8908
8919
  * per-format `frameSink: 'shm'` decoder sessions that feed them.
8909
8920
  */
@@ -9022,17 +9033,18 @@ var FrameHandlePlane = class {
9022
9033
  * underlying decoder session is destroyed (and its shm segment unlinked
9023
9034
  * by the decoder). Returns `true` when a known subscription was released.
9024
9035
  */
9025
- async unsubscribe(subscriptionId) {
9036
+ async unsubscribe(subscriptionId, immediate = false) {
9026
9037
  const subscription = this.subscriptions.get(subscriptionId);
9027
9038
  if (!subscription) return false;
9028
9039
  this.subscriptions.delete(subscriptionId);
9029
9040
  const session = this.sessions.get(subscription.format);
9030
9041
  if (session) {
9031
9042
  session.subscriberIds.delete(subscriptionId);
9032
- if (session.subscriberIds.size === 0) {
9043
+ if (session.subscriberIds.size === 0) if (immediate) {
9044
+ this.clearLinger(session);
9033
9045
  this.sessions.delete(subscription.format);
9034
9046
  await this.destroySession(session);
9035
- }
9047
+ } else this.scheduleLinger(session);
9036
9048
  }
9037
9049
  this.logger?.info("frame-handle subscription removed", { meta: {
9038
9050
  subscriptionId,
@@ -9041,6 +9053,32 @@ var FrameHandlePlane = class {
9041
9053
  return true;
9042
9054
  }
9043
9055
  /**
9056
+ * Arm the keep-warm teardown timer for a session whose last subscriber just
9057
+ * left. Clears any existing timer first (idempotent). On fire it re-checks
9058
+ * `subscriberIds.size === 0` — a subscriber may have returned in the interim,
9059
+ * in which case the timer is a no-op — and only then removes the session and
9060
+ * destroys the decoder. The timer is `unref`'d so a lingering warm session
9061
+ * never keeps the process alive.
9062
+ */
9063
+ scheduleLinger(session) {
9064
+ this.clearLinger(session);
9065
+ const timer = setTimeout(() => {
9066
+ session.lingerTimer = null;
9067
+ if (session.subscriberIds.size > 0) return;
9068
+ this.sessions.delete(session.format);
9069
+ this.destroySession(session).catch(() => {});
9070
+ }, SESSION_LINGER_MS);
9071
+ timer.unref?.();
9072
+ session.lingerTimer = timer;
9073
+ }
9074
+ /** Cancel a session's pending keep-warm teardown timer, if any. */
9075
+ clearLinger(session) {
9076
+ if (session.lingerTimer) {
9077
+ clearTimeout(session.lingerTimer);
9078
+ session.lingerTimer = null;
9079
+ }
9080
+ }
9081
+ /**
9044
9082
  * Forward a video `EncodedPacket` to every active shm decoder session.
9045
9083
  * Push-mode decoders consume this; a pull-mode decoder ignores it (it
9046
9084
  * reads its own pipe via `openStream`). Audio packets are not forwarded.
@@ -9162,7 +9200,7 @@ var FrameHandlePlane = class {
9162
9200
  */
9163
9201
  async killByTag(tag) {
9164
9202
  const victims = [...this.subscriptions.values()].filter((s) => s.tag === tag).map((s) => s.id);
9165
- for (const id of victims) await this.unsubscribe(id);
9203
+ for (const id of victims) await this.unsubscribe(id, true);
9166
9204
  return victims.length;
9167
9205
  }
9168
9206
  /** Tear down every subscription + decoder session. Idempotent. */
@@ -9190,6 +9228,7 @@ var FrameHandlePlane = class {
9190
9228
  async ensureSession(format, subscriptionId) {
9191
9229
  const existing = this.sessions.get(format);
9192
9230
  if (existing) {
9231
+ this.clearLinger(existing);
9193
9232
  existing.subscriberIds.add(subscriptionId);
9194
9233
  return;
9195
9234
  }
@@ -9206,7 +9245,8 @@ var FrameHandlePlane = class {
9206
9245
  format,
9207
9246
  proxy: null,
9208
9247
  nodeId: null,
9209
- subscriberIds
9248
+ subscriberIds,
9249
+ lingerTimer: null
9210
9250
  };
9211
9251
  if (await this.startSessionDecoder(session)) this.sessions.set(format, session);
9212
9252
  })();
@@ -9299,6 +9339,7 @@ var FrameHandlePlane = class {
9299
9339
  }
9300
9340
  /** Destroy a decoder session, swallowing teardown errors. */
9301
9341
  async destroySession(session) {
9342
+ this.clearLinger(session);
9302
9343
  const proxy = session.proxy;
9303
9344
  session.proxy = null;
9304
9345
  session.nodeId = null;
@@ -10579,12 +10620,12 @@ var StreamBroker = class StreamBroker {
10579
10620
  async subscribeFrameHandles(input) {
10580
10621
  const plane = this.ensureFrameHandlePlane();
10581
10622
  if (!plane) throw new Error("stream-broker: no decoder available for frame-handle subscription");
10582
- this.checkDemand();
10583
10623
  const { subscriptionId, maxFps } = await plane.subscribe({
10584
10624
  format: input.format,
10585
10625
  maxFps: input.maxFps,
10586
10626
  tag: input.tag
10587
10627
  });
10628
+ this.checkDemand();
10588
10629
  return {
10589
10630
  subscriptionId,
10590
10631
  maxFps
@@ -8900,6 +8900,17 @@ var DEFAULT_HINT_FPS = 5;
8900
8900
  * broker's 2s `inputFps` window so the two stats are comparable. */
8901
8901
  var DECODE_FPS_WINDOW_MS = 2e3;
8902
8902
  /**
8903
+ * Keep-warm grace before a decoder session with no subscribers is torn down.
8904
+ *
8905
+ * Subscribers flap on ~30s cadences (occupancy recheck bursts, detection
8906
+ * motion-gating watch↔active, onboard gray gating). Destroying the
8907
+ * ffmpeg/VAAPI decode session the instant the last subscriber leaves spawns +
8908
+ * destroys a full session every cycle — wasted setup/teardown. Chosen >30s so
8909
+ * consecutive bursts and watch↔active flips reuse the warm session, yet a
8910
+ * genuinely idle session still frees its ffmpeg within ~a minute.
8911
+ */
8912
+ var SESSION_LINGER_MS = 45e3;
8913
+ /**
8903
8914
  * Owns the broker's shared-memory frame-handle subscriptions and the
8904
8915
  * per-format `frameSink: 'shm'` decoder sessions that feed them.
8905
8916
  */
@@ -9018,17 +9029,18 @@ var FrameHandlePlane = class {
9018
9029
  * underlying decoder session is destroyed (and its shm segment unlinked
9019
9030
  * by the decoder). Returns `true` when a known subscription was released.
9020
9031
  */
9021
- async unsubscribe(subscriptionId) {
9032
+ async unsubscribe(subscriptionId, immediate = false) {
9022
9033
  const subscription = this.subscriptions.get(subscriptionId);
9023
9034
  if (!subscription) return false;
9024
9035
  this.subscriptions.delete(subscriptionId);
9025
9036
  const session = this.sessions.get(subscription.format);
9026
9037
  if (session) {
9027
9038
  session.subscriberIds.delete(subscriptionId);
9028
- if (session.subscriberIds.size === 0) {
9039
+ if (session.subscriberIds.size === 0) if (immediate) {
9040
+ this.clearLinger(session);
9029
9041
  this.sessions.delete(subscription.format);
9030
9042
  await this.destroySession(session);
9031
- }
9043
+ } else this.scheduleLinger(session);
9032
9044
  }
9033
9045
  this.logger?.info("frame-handle subscription removed", { meta: {
9034
9046
  subscriptionId,
@@ -9037,6 +9049,32 @@ var FrameHandlePlane = class {
9037
9049
  return true;
9038
9050
  }
9039
9051
  /**
9052
+ * Arm the keep-warm teardown timer for a session whose last subscriber just
9053
+ * left. Clears any existing timer first (idempotent). On fire it re-checks
9054
+ * `subscriberIds.size === 0` — a subscriber may have returned in the interim,
9055
+ * in which case the timer is a no-op — and only then removes the session and
9056
+ * destroys the decoder. The timer is `unref`'d so a lingering warm session
9057
+ * never keeps the process alive.
9058
+ */
9059
+ scheduleLinger(session) {
9060
+ this.clearLinger(session);
9061
+ const timer = setTimeout(() => {
9062
+ session.lingerTimer = null;
9063
+ if (session.subscriberIds.size > 0) return;
9064
+ this.sessions.delete(session.format);
9065
+ this.destroySession(session).catch(() => {});
9066
+ }, SESSION_LINGER_MS);
9067
+ timer.unref?.();
9068
+ session.lingerTimer = timer;
9069
+ }
9070
+ /** Cancel a session's pending keep-warm teardown timer, if any. */
9071
+ clearLinger(session) {
9072
+ if (session.lingerTimer) {
9073
+ clearTimeout(session.lingerTimer);
9074
+ session.lingerTimer = null;
9075
+ }
9076
+ }
9077
+ /**
9040
9078
  * Forward a video `EncodedPacket` to every active shm decoder session.
9041
9079
  * Push-mode decoders consume this; a pull-mode decoder ignores it (it
9042
9080
  * reads its own pipe via `openStream`). Audio packets are not forwarded.
@@ -9158,7 +9196,7 @@ var FrameHandlePlane = class {
9158
9196
  */
9159
9197
  async killByTag(tag) {
9160
9198
  const victims = [...this.subscriptions.values()].filter((s) => s.tag === tag).map((s) => s.id);
9161
- for (const id of victims) await this.unsubscribe(id);
9199
+ for (const id of victims) await this.unsubscribe(id, true);
9162
9200
  return victims.length;
9163
9201
  }
9164
9202
  /** Tear down every subscription + decoder session. Idempotent. */
@@ -9186,6 +9224,7 @@ var FrameHandlePlane = class {
9186
9224
  async ensureSession(format, subscriptionId) {
9187
9225
  const existing = this.sessions.get(format);
9188
9226
  if (existing) {
9227
+ this.clearLinger(existing);
9189
9228
  existing.subscriberIds.add(subscriptionId);
9190
9229
  return;
9191
9230
  }
@@ -9202,7 +9241,8 @@ var FrameHandlePlane = class {
9202
9241
  format,
9203
9242
  proxy: null,
9204
9243
  nodeId: null,
9205
- subscriberIds
9244
+ subscriberIds,
9245
+ lingerTimer: null
9206
9246
  };
9207
9247
  if (await this.startSessionDecoder(session)) this.sessions.set(format, session);
9208
9248
  })();
@@ -9295,6 +9335,7 @@ var FrameHandlePlane = class {
9295
9335
  }
9296
9336
  /** Destroy a decoder session, swallowing teardown errors. */
9297
9337
  async destroySession(session) {
9338
+ this.clearLinger(session);
9298
9339
  const proxy = session.proxy;
9299
9340
  session.proxy = null;
9300
9341
  session.nodeId = null;
@@ -10575,12 +10616,12 @@ var StreamBroker = class StreamBroker {
10575
10616
  async subscribeFrameHandles(input) {
10576
10617
  const plane = this.ensureFrameHandlePlane();
10577
10618
  if (!plane) throw new Error("stream-broker: no decoder available for frame-handle subscription");
10578
- this.checkDemand();
10579
10619
  const { subscriptionId, maxFps } = await plane.subscribe({
10580
10620
  format: input.format,
10581
10621
  maxFps: input.maxFps,
10582
10622
  tag: input.tag
10583
10623
  });
10624
+ this.checkDemand();
10584
10625
  return {
10585
10626
  subscriptionId,
10586
10627
  maxFps
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CLVitFM4.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BVFAEkIB.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.1.22",
3
+ "version": "1.1.23",
4
4
  "description": "CamStack Pipeline bundle — runner, detection, motion, decoders, audio + stream broker. Multi-entry npm package shipping 7 addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",