@camstack/addon-pipeline-orchestrator 0.1.21 → 0.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.
Files changed (25) hide show
  1. package/dist/@mf-types/compiled-types/widgets/index.d.ts +0 -2
  2. package/dist/@mf-types/compiled-types/widgets/index.d.ts.map +1 -1
  3. package/dist/@mf-types.zip +0 -0
  4. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-C4HmLg0z.mjs +20 -0
  5. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-DEuqbomC.mjs +34 -0
  6. package/dist/_stub.js +568 -771
  7. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-GASHflbS.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-xWchXz0h.mjs} +6 -6
  8. package/dist/{hostInit-s8NZVmrk.mjs → hostInit-Nvn9J-YF.mjs} +6 -6
  9. package/dist/{index-BP1Nti7b.mjs → index-BCEx31Mh.mjs} +3767 -3350
  10. package/dist/{index-BIlr4dIX.mjs → index-BmY66bNn.mjs} +1 -1
  11. package/dist/{index-CMke0KpS.mjs → index-BuYTzV_S.mjs} +6594 -6053
  12. package/dist/index.js +325 -91
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.mjs +325 -91
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/remoteEntry.js +1 -1
  17. package/package.json +3 -3
  18. package/dist/@mf-types/compiled-types/widgets/MotionZonesEditor.d.ts +0 -19
  19. package/dist/@mf-types/compiled-types/widgets/MotionZonesEditor.d.ts.map +0 -1
  20. package/dist/@mf-types/compiled-types/widgets/motion-zones/MotionGridCanvas.d.ts +0 -10
  21. package/dist/@mf-types/compiled-types/widgets/motion-zones/MotionGridCanvas.d.ts.map +0 -1
  22. package/dist/@mf-types/compiled-types/widgets/motion-zones/MotionZonesTab.d.ts +0 -5
  23. package/dist/@mf-types/compiled-types/widgets/motion-zones/MotionZonesTab.d.ts.map +0 -1
  24. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.mjs-UNj4rttw.mjs +0 -20
  25. package/dist/__mfe_internal__addon_pipeline_orchestrator_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.mjs-DPoup41Y.mjs +0 -34
package/dist/index.js CHANGED
@@ -5069,6 +5069,7 @@ const WELL_KNOWN_TABS = [
5069
5069
  { id: "osd", label: "OSD", icon: "type", order: 18 },
5070
5070
  { id: "stream-broker", label: "Stream Broker", icon: "radio", order: 20 },
5071
5071
  { id: "streaming", label: "Streaming", icon: "video", order: 35 },
5072
+ { id: "ptz", label: "PTZ", icon: "move", order: 40 },
5072
5073
  { id: "pipeline", label: "Detection Pipeline", icon: "cpu", order: 39 },
5073
5074
  { id: "zones", label: "Detection", icon: "shapes", order: 38 },
5074
5075
  { id: "live-stats", label: "Live Stats", icon: "activity", order: 39 },
@@ -5225,7 +5226,19 @@ const DecoderSessionConfigSchema = object({
5225
5226
  * on every line so `grep tag=broker:5/high` filters one camera
5226
5227
  * profile cleanly.
5227
5228
  */
5228
- tag: string().optional()
5229
+ tag: string().optional(),
5230
+ /**
5231
+ * Where the session delivers decoded frames (Phase 5 / D9):
5232
+ *
5233
+ * - `'callback'` (default) — the legacy pixel path: decoded frames are
5234
+ * buffered as `DecodedFrame`s and drained via `pullFrames`.
5235
+ * - `'shm'` — the shared-memory frame plane: decoded frames are written
5236
+ * into an OS shared-memory ring and drained as zero-pixel
5237
+ * `FrameHandle`s via `pullHandles`. A session is one mode or the
5238
+ * other — `pullFrames` returns nothing for an `'shm'` session and
5239
+ * `pullHandles` returns nothing for a `'callback'` session.
5240
+ */
5241
+ frameSink: _enum(["callback", "shm"]).default("callback")
5229
5242
  });
5230
5243
  function errMsg(err) {
5231
5244
  if (err instanceof Error) return err.message;
@@ -5983,6 +5996,53 @@ const DecodedFrameSchema = object({
5983
5996
  format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
5984
5997
  timestamp: number()
5985
5998
  });
5999
+ const FrameHandleSchema = object({
6000
+ shmId: string(),
6001
+ slot: number().int().nonnegative(),
6002
+ seq: number().int().nonnegative(),
6003
+ width: number().int().positive(),
6004
+ height: number().int().positive(),
6005
+ format: _enum(["jpeg", "rgb", "bgr", "yuv420", "gray"]),
6006
+ pts: number(),
6007
+ byteLength: number().int().nonnegative(),
6008
+ nodeId: string(),
6009
+ slotCount: number().int().positive()
6010
+ });
6011
+ const FrameHandleFormatSchema = _enum(["rgb", "bgr", "yuv420", "gray"]);
6012
+ const SubscribeFramesInputSchema = object({
6013
+ brokerId: string(),
6014
+ format: FrameHandleFormatSchema,
6015
+ /**
6016
+ * Optional reader-side cadence hint in frames per second. The broker does
6017
+ * NOT throttle — latest-wins ring reads drop frames implicitly for a slow
6018
+ * consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
6019
+ * the consumer can pace its own `pullFrameHandles` polling.
6020
+ */
6021
+ maxFps: number().positive().optional(),
6022
+ /** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
6023
+ tag: string().optional()
6024
+ });
6025
+ const SubscribeFramesResultSchema = object({
6026
+ /** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
6027
+ subscriptionId: string(),
6028
+ /** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
6029
+ maxFps: number().nonnegative()
6030
+ });
6031
+ const DecodedAudioChunkSchema = object({
6032
+ data: _instanceof(Uint8Array),
6033
+ sampleRate: number().int().positive(),
6034
+ channels: number().int().positive(),
6035
+ timestamp: number()
6036
+ });
6037
+ const SubscribeAudioChunksInputSchema = object({
6038
+ brokerId: string(),
6039
+ /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
6040
+ tag: string().optional()
6041
+ });
6042
+ const SubscribeAudioChunksResultSchema = object({
6043
+ /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
6044
+ subscriptionId: string()
6045
+ });
5986
6046
  const BrokerStatusSchema$1 = _enum(["idle", "connecting", "streaming", "error", "stopped"]);
5987
6047
  const BrokerStatsSchema = object({
5988
6048
  status: BrokerStatusSchema$1,
@@ -6204,9 +6264,76 @@ const RtpSourceSchema = object({
6204
6264
  object({ released: boolean(), refcount: number().int().nonnegative() }),
6205
6265
  { kind: "mutation", auth: "admin" }
6206
6266
  ),
6207
- getBroker: method(
6208
- object({ brokerId: string() }),
6209
- custom()
6267
+ /**
6268
+ * ── Decoded audio-chunk plane (Phase 5 / D9) ──────────────────────
6269
+ *
6270
+ * The serialisable replacement for the live-object `IStreamBroker.
6271
+ * onDecodedAudioChunk` callback path. Unlike the video frame plane,
6272
+ * audio chunks are tiny (a ~500ms PCM window is a few KB) so they ship
6273
+ * their bytes INLINE over tRPC — no shared-memory ring. A consumer:
6274
+ *
6275
+ * 1. `subscribeAudioChunks({ brokerId, tag })` — the broker registers
6276
+ * a per-subscription bounded FIFO queue and returns a
6277
+ * `subscriptionId`.
6278
+ * 2. polls `pullAudioChunks({ subscriptionId, maxCount })` — drains
6279
+ * `DecodedAudioChunk[]` in arrival order (FIFO, no latest-wins
6280
+ * drop: an audio gap is audible / breaks an analysis window). The
6281
+ * queue is generously sized; it only drops its oldest chunk if a
6282
+ * truly stalled consumer lets it overflow.
6283
+ * 3. `unsubscribeAudioChunks({ subscriptionId })` on teardown.
6284
+ */
6285
+ subscribeAudioChunks: method(
6286
+ SubscribeAudioChunksInputSchema,
6287
+ SubscribeAudioChunksResultSchema,
6288
+ { kind: "mutation" }
6289
+ ),
6290
+ pullAudioChunks: method(
6291
+ object({
6292
+ subscriptionId: string(),
6293
+ maxCount: number().int().positive().default(8)
6294
+ }),
6295
+ array(DecodedAudioChunkSchema).readonly()
6296
+ ),
6297
+ unsubscribeAudioChunks: method(
6298
+ object({ subscriptionId: string() }),
6299
+ object({ released: boolean() }),
6300
+ { kind: "mutation" }
6301
+ ),
6302
+ /**
6303
+ * ── Shared-memory frame plane (Phase 5 / D9) ──────────────────────
6304
+ *
6305
+ * The handle-based replacement for the live-object `IStreamBroker.
6306
+ * onDecodedFrame` callback path. A consumer:
6307
+ *
6308
+ * 1. `subscribeFrames({ brokerId, format })` — the broker spins up
6309
+ * (or reuses) a `frameSink: 'shm'` decoder session producing that
6310
+ * `format` and returns a `subscriptionId`.
6311
+ * 2. polls `pullFrameHandles({ subscriptionId, maxCount })` — drains
6312
+ * zero-pixel `FrameHandle[]`; each handle is fed to a
6313
+ * `FrameRingReader` that opens the named shm segment and reads the
6314
+ * pixels back zero-copy.
6315
+ * 3. `unsubscribeFrames({ subscriptionId })` on teardown.
6316
+ *
6317
+ * The broker keeps one shm ring per `(brokerId, format)` actually
6318
+ * requested — no broker-side `sharp` conversion. fps throttling is
6319
+ * implicit (latest-wins ring reads drop frames for a slow consumer).
6320
+ */
6321
+ subscribeFrames: method(
6322
+ SubscribeFramesInputSchema,
6323
+ SubscribeFramesResultSchema,
6324
+ { kind: "mutation" }
6325
+ ),
6326
+ pullFrameHandles: method(
6327
+ object({
6328
+ subscriptionId: string(),
6329
+ maxCount: number().int().positive().default(4)
6330
+ }),
6331
+ array(FrameHandleSchema).readonly()
6332
+ ),
6333
+ unsubscribeFrames: method(
6334
+ object({ subscriptionId: string() }),
6335
+ object({ released: boolean() }),
6336
+ { kind: "mutation" }
6210
6337
  ),
6211
6338
  setPreBufferDuration: method(
6212
6339
  object({ brokerId: string(), seconds: number().min(0).max(30) }),
@@ -7532,6 +7659,29 @@ const StreamProfilePatchSchema = object({
7532
7659
  }),
7533
7660
  _void(),
7534
7661
  { kind: "mutation", auth: "admin" }
7662
+ ),
7663
+ /**
7664
+ * Build the `ConfigUISchema` (admin-ui `ConfigFormBuilder` input
7665
+ * shape) for this camera's stream-encoder settings — one section per
7666
+ * profile (main / sub / ext) with the resolution / codec / framerate
7667
+ * / bitrate / bitrate-mode / encoder-profile / GOP controls the
7668
+ * firmware actually exposes.
7669
+ *
7670
+ * Driven by `getOptions` (camera-probed availability) + `getStatus`
7671
+ * (current per-profile config); each field's `default` is seeded
7672
+ * from the live config so the form renders the camera state in one
7673
+ * pass. Returns `null` when the camera exposes no configurable
7674
+ * stream property — the renderer then shows the unsupported message.
7675
+ *
7676
+ * Output is `z.unknown().nullable()` — the same convention every
7677
+ * other `ConfigUISchema`-returning cap method uses (`device-ops`,
7678
+ * `device-manager`); `ConfigUISchema` is a TS-only type with no
7679
+ * companion Zod schema, and a concrete object would collapse
7680
+ * unrelated AppRouter branches to `unknown` during codegen.
7681
+ */
7682
+ getConfigSchema: method(
7683
+ object({ deviceId: number() }),
7684
+ unknown().nullable()
7535
7685
  )
7536
7686
  }
7537
7687
  });
@@ -8602,7 +8752,7 @@ const OauthIntegrationDescriptorSchema = object({
8602
8752
  displayName: string(),
8603
8753
  /** Scopes baked into every token issued for this integration. */
8604
8754
  requestedScopes: array(TokenScopeSchema),
8605
- /** Allowed redirect_uri prefixes. /oauth2/authorize rejects any
8755
+ /** Allowed redirect_uri prefixes. /api/oauth2/authorize rejects any
8606
8756
  * redirect_uri that does not start with one of these. Required —
8607
8757
  * an empty list means the integration can never complete linking. */
8608
8758
  allowedRedirectPrefixes: array(string()).min(1)
@@ -8892,21 +9042,30 @@ const AddonPageDeclarationSchema = object({
8892
9042
  });
8893
9043
  const WidgetHostEnum = _enum(["device-tab", "dashboard", "integration-detail"]);
8894
9044
  const WidgetSizeEnum = _enum(["xs", "sm", "md", "lg", "xl"]);
9045
+ const WidgetRemoteSchema = object({
9046
+ remoteName: string(),
9047
+ exposedModule: string(),
9048
+ componentKey: string().optional()
9049
+ });
8895
9050
  const WidgetMetadataSchema = object({
8896
- /** Stable id within the addon — kebab-case. */
8897
- stableId: string(),
9051
+ // ── UiContribution core (kind:'remote') ──────────────────────────
9052
+ /** Primary host tab — `'dashboard'`, `'device-tab'`, or a device-detail tab id. */
9053
+ tab: string(),
9054
+ /** Optional sub-tab within `tab`. */
9055
+ subTab: string().optional(),
8898
9056
  /** Operator-facing label. */
8899
9057
  label: string(),
9058
+ /** Ordering within `(tab, subTab)`, ascending. */
9059
+ order: number().optional(),
9060
+ /** Always `'remote'` — a widget is a Module Federation remote. */
9061
+ kind: literal("remote"),
9062
+ /** MF remote descriptor. */
9063
+ remote: WidgetRemoteSchema,
9064
+ // ── Widget-only metadata ─────────────────────────────────────────
9065
+ /** Stable id within the addon — kebab-case. Equals `remote.componentKey`. */
9066
+ stableId: string(),
8900
9067
  description: string().optional(),
8901
9068
  icon: string().optional(),
8902
- /**
8903
- * Module Federation remote name — must match the `name` field on the
8904
- * widget addon's `federation()` plugin config. Used by the host's
8905
- * `<WidgetRegistryProvider>` to call `loadRemote('<remoteName>/widgets')`.
8906
- * Conventionally `addon_<addonid>_widgets` (snake_case; MF names
8907
- * cannot contain hyphens).
8908
- */
8909
- remoteName: string(),
8910
9069
  /**
8911
9070
  * Bundle filename inside the addon's `dist/` dir served at
8912
9071
  * `/api/addon-widgets/<addonId>/<bundle>`. With Module Federation
@@ -8915,9 +9074,9 @@ const WidgetMetadataSchema = object({
8915
9074
  * cache-buster URL without a separate filesystem stat.
8916
9075
  */
8917
9076
  bundle: string(),
8918
- /** Where the widget makes sense to render. */
9077
+ /** Every host the widget supports. The picker filters on this set. */
8919
9078
  hosts: array(WidgetHostEnum).readonly(),
8920
- /** Required props the host must supply. Validated at <WidgetSlot> mount. */
9079
+ /** Required props the host must supply. Validated at `<WidgetSlot>` mount. */
8921
9080
  requires: object({
8922
9081
  deviceContext: boolean().default(false),
8923
9082
  integrationContext: boolean().default(false)
@@ -8992,6 +9151,16 @@ const InvokeReplyEnvelopeSchema = object({
8992
9151
  invoke: method(InvokeRequestSchema, InvokeReplyEnvelopeSchema, { kind: "mutation" })
8993
9152
  }
8994
9153
  });
9154
+ const ShmRingStatsSchema = object({
9155
+ sessionId: string(),
9156
+ slotCount: number().int(),
9157
+ slotByteLength: number().int(),
9158
+ segmentBytes: number().int(),
9159
+ budgetMb: number().int(),
9160
+ framesWritten: number().int(),
9161
+ getFrameHits: number().int(),
9162
+ getFrameMisses: number().int()
9163
+ });
8995
9164
  ({
8996
9165
  methods: {
8997
9166
  // ── Discovery ─────────────────────────────────────────────────
@@ -9019,10 +9188,27 @@ const InvokeReplyEnvelopeSchema = object({
9019
9188
  url: string()
9020
9189
  }), _void()),
9021
9190
  // ── Output — polling-based frame retrieval ────────────────────
9191
+ // `pullFrames` drains the pixel `DecodedFrame[]` of a `frameSink:
9192
+ // 'callback'` session. `pullHandles` (Phase 5 / D9) drains the
9193
+ // zero-pixel `FrameHandle[]` of a `frameSink: 'shm'` session — the
9194
+ // broker hands each handle to a `FrameRingReader` that opens the
9195
+ // named segment and reads the pixels back zero-copy. A session is
9196
+ // one mode or the other; the unmatched method returns an empty
9197
+ // array.
9022
9198
  pullFrames: method(object({
9023
9199
  sessionId: string(),
9024
9200
  maxCount: number().default(1)
9025
9201
  }), array(DecodedFrameSchema)),
9202
+ pullHandles: method(object({
9203
+ sessionId: string(),
9204
+ maxCount: number().default(1)
9205
+ }), array(FrameHandleSchema)),
9206
+ // ── Frame fetch (Phase 5 / D9 downstream access) ──────────────
9207
+ // Read the pixels a FrameHandle refers to from this node's shm ring.
9208
+ // Returns null when the slot was already recycled (latest-wins).
9209
+ getFrame: method(object({ handle: FrameHandleSchema }), DecodedFrameSchema.nullable()),
9210
+ // shm ring usage stats for a session.
9211
+ getShmStats: method(object({ sessionId: string() }), ShmRingStatsSchema.nullable()),
9026
9212
  // ── Control ───────────────────────────────────────────────────
9027
9213
  updateConfig: method(object({
9028
9214
  sessionId: string(),
@@ -10194,8 +10380,8 @@ const DevicePersistConfigPayloadSchema = object({
10194
10380
  /**
10195
10381
  * Return the addon ids that declared a wrapper provider for `capName`.
10196
10382
  * Backs the device-bindings UI's wrapper-picker dropdown. Entries are
10197
- * sourced from `CapabilityRegistry.wrapperProviders`, populated at
10198
- * `ProviderRegistration.kind === 'wrapper'` time.
10383
+ * sourced from `CapabilityRegistry.wrapperProviders`, populated when
10384
+ * the cap definition declares `kind: 'wrapper'`.
10199
10385
  */
10200
10386
  listWrappersForCap: method(
10201
10387
  object({ capName: string() }),
@@ -11285,13 +11471,17 @@ const PtzMoveCommandSchema = object({
11285
11471
  zoom: number().optional(),
11286
11472
  speed: number().optional()
11287
11473
  });
11474
+ PtzPositionSchema.extend({ autofocus: boolean() });
11288
11475
  const PtzOptionsSchema = object({
11289
11476
  hasPan: boolean(),
11290
11477
  hasTilt: boolean(),
11291
11478
  hasZoom: boolean(),
11292
11479
  supportsPresets: boolean(),
11293
11480
  /** Max number of named presets the camera supports, when known. */
11294
- maxPresets: number().optional()
11481
+ maxPresets: number().optional(),
11482
+ /** Whether the camera exposes a controllable autofocus toggle
11483
+ * (boolean `hasX` per the getOptions availability convention). */
11484
+ hasAutofocus: boolean()
11295
11485
  });
11296
11486
  ({
11297
11487
  deviceTypes: [DeviceType.Camera],
@@ -11348,6 +11538,13 @@ const PtzOptionsSchema = object({
11348
11538
  getPosition: method(
11349
11539
  object({ deviceId: number() }),
11350
11540
  PtzPositionSchema
11541
+ ),
11542
+ /** Toggle the camera's autofocus. Only meaningful when
11543
+ * `getOptions().hasAutofocus` is true. */
11544
+ setAutofocus: method(
11545
+ object({ deviceId: number(), enabled: boolean() }),
11546
+ _void(),
11547
+ { kind: "mutation" }
11351
11548
  )
11352
11549
  }
11353
11550
  });
@@ -12716,6 +12913,19 @@ const RenameNodeResultSchema = object({
12716
12913
  record(string(), ClusterAddonStatusEntrySchema),
12717
12914
  { auth: "admin" }
12718
12915
  ),
12916
+ getCapUsageGraph: method(
12917
+ object({
12918
+ windowSeconds: number().int().positive().max(300).default(60)
12919
+ }),
12920
+ array(object({
12921
+ callerAddonId: string(),
12922
+ providerAddonId: string(),
12923
+ capName: string(),
12924
+ callsPerMin: number(),
12925
+ lastCallAtMs: number()
12926
+ })).readonly(),
12927
+ { auth: "admin" }
12928
+ ),
12719
12929
  /**
12720
12930
  * Direct per-node addon listing — calls `$agent.status` on the target
12721
12931
  * node (or returns the hub registry for `nodeId === 'hub'`) and surfaces
@@ -14521,6 +14731,60 @@ function balanceAudio(input) {
14521
14731
  const best = input.nodes.slice().sort((a, b) => a.deviceCount - b.deviceCount)[0];
14522
14732
  return { nodeId: best.nodeId, reason: "capacity" };
14523
14733
  }
14734
+ const POLL_INTERVAL_MS = 200;
14735
+ const PULL_MAX_COUNT = 8;
14736
+ async function startAudioChunkPoller(options) {
14737
+ const { api, brokerId, tag, onChunk, logger } = options;
14738
+ let subscriptionId;
14739
+ try {
14740
+ const result = await api.streamBroker.subscribeAudioChunks.mutate({
14741
+ brokerId,
14742
+ tag
14743
+ });
14744
+ subscriptionId = result.subscriptionId;
14745
+ } catch (err) {
14746
+ logger.warn("audio-chunk poller: subscribeAudioChunks failed", {
14747
+ meta: { brokerId, tag, error: errMsg(err) }
14748
+ });
14749
+ return null;
14750
+ }
14751
+ let stopped = false;
14752
+ let timer;
14753
+ const tick = async () => {
14754
+ if (stopped) return;
14755
+ try {
14756
+ const chunks = await api.streamBroker.pullAudioChunks.query({
14757
+ subscriptionId,
14758
+ maxCount: PULL_MAX_COUNT
14759
+ });
14760
+ for (const chunk of chunks) {
14761
+ if (stopped) break;
14762
+ await onChunk(chunk);
14763
+ }
14764
+ } catch (err) {
14765
+ logger.warn("audio-chunk poller: pullAudioChunks failed", {
14766
+ meta: { brokerId, subscriptionId, error: errMsg(err) }
14767
+ });
14768
+ }
14769
+ if (!stopped) {
14770
+ timer = setTimeout(() => void tick(), POLL_INTERVAL_MS);
14771
+ }
14772
+ };
14773
+ void tick();
14774
+ return () => {
14775
+ if (stopped) return;
14776
+ stopped = true;
14777
+ if (timer) {
14778
+ clearTimeout(timer);
14779
+ timer = void 0;
14780
+ }
14781
+ api.streamBroker.unsubscribeAudioChunks.mutate({ subscriptionId }).catch((err) => {
14782
+ logger.warn("audio-chunk poller: unsubscribeAudioChunks failed", {
14783
+ meta: { brokerId, subscriptionId, error: errMsg(err) }
14784
+ });
14785
+ });
14786
+ };
14787
+ }
14524
14788
  const PHASE_MODE_VALUES = /* @__PURE__ */ new Set([
14525
14789
  "disabled",
14526
14790
  "always-on",
@@ -14976,11 +15240,17 @@ class PipelineOrchestratorAddon extends BaseAddon {
14976
15240
  const widgetsProvider = {
14977
15241
  listWidgets: async () => [
14978
15242
  {
14979
- stableId: "pipeline-quick-stats",
15243
+ tab: "device-tab",
14980
15244
  label: "Pipeline Quick Stats",
15245
+ kind: "remote",
15246
+ remote: {
15247
+ remoteName: "addon_pipeline_orchestrator_widgets",
15248
+ exposedModule: "./widgets",
15249
+ componentKey: "pipeline-quick-stats"
15250
+ },
15251
+ stableId: "pipeline-quick-stats",
14981
15252
  description: "Phase / Detection FPS / Inference / Active Tracks tile row.",
14982
15253
  icon: "activity",
14983
- remoteName: "addon_pipeline_orchestrator_widgets",
14984
15254
  bundle: "remoteEntry.js",
14985
15255
  hosts: ["device-tab", "dashboard"],
14986
15256
  requires: { deviceContext: true, integrationContext: false },
@@ -14990,11 +15260,17 @@ class PipelineOrchestratorAddon extends BaseAddon {
14990
15260
  defaultRows: 1
14991
15261
  },
14992
15262
  {
14993
- stableId: "zone-editor",
15263
+ tab: "device-tab",
14994
15264
  label: "Zone Editor",
15265
+ kind: "remote",
15266
+ remote: {
15267
+ remoteName: "addon_pipeline_orchestrator_widgets",
15268
+ exposedModule: "./widgets",
15269
+ componentKey: "zone-editor"
15270
+ },
15271
+ stableId: "zone-editor",
14995
15272
  description: "Polygon / tripwire CRUD + per-stage rule editor.",
14996
15273
  icon: "shapes",
14997
- remoteName: "addon_pipeline_orchestrator_widgets",
14998
15274
  bundle: "remoteEntry.js",
14999
15275
  hosts: ["device-tab"],
15000
15276
  requires: { deviceContext: true, integrationContext: false },
@@ -15002,24 +15278,6 @@ class PipelineOrchestratorAddon extends BaseAddon {
15002
15278
  allowedSizes: ["lg", "xl"],
15003
15279
  defaultColumns: 12,
15004
15280
  defaultRows: 4
15005
- },
15006
- {
15007
- // On-camera motion-detection grid editor. The `motion-zones`
15008
- // cap is owned by the camera provider addons; the widget only
15009
- // talks to it over tRPC, so hosting the React surface here
15010
- // (one bundle) avoids duplicating it per provider.
15011
- stableId: "motion-zones-editor",
15012
- label: "Motion Zones",
15013
- description: "On-camera motion-detection grid editor (live-frame overlay).",
15014
- icon: "grid-3x3",
15015
- remoteName: "addon_pipeline_orchestrator_widgets",
15016
- bundle: "remoteEntry.js",
15017
- hosts: ["device-tab"],
15018
- requires: { deviceContext: true, integrationContext: false },
15019
- defaultSize: "lg",
15020
- allowedSizes: ["md", "lg"],
15021
- defaultColumns: 12,
15022
- defaultRows: 1
15023
15281
  }
15024
15282
  ]
15025
15283
  };
@@ -17168,53 +17426,27 @@ class PipelineOrchestratorAddon extends BaseAddon {
17168
17426
  title: "Detection Zones",
17169
17427
  tab: "zones",
17170
17428
  location: "top-tab",
17171
- // Single full-width column — the `zone-editor` renderer paints
17172
- // the polygon canvas + side panel + rules + occupancy across the
17429
+ // Single full-width column — the zone-editor widget paints the
17430
+ // polygon canvas + side panel + rules + occupancy across the
17173
17431
  // entire viewport. Default 2-column grid would clip everything to
17174
17432
  // 50% width.
17175
17433
  columns: 1,
17176
17434
  order: 0,
17177
17435
  fields: [
17178
17436
  {
17179
- type: "zone-editor",
17437
+ type: "widget",
17180
17438
  key: "zones",
17181
17439
  label: "Detection Zones",
17182
- backdrop: "snapshot",
17440
+ widgetId: "pipeline-orchestrator/zone-editor",
17183
17441
  // Span across the (single) column so even if a layout engine
17184
17442
  // were to compute multiple columns, the field still pulls full
17185
17443
  // width.
17186
- span: 1,
17187
- // Hydrated form requires a value field; the `zone-editor`
17188
- // renderer reads zones from the `dev.state.zones` slice via
17189
- // DeviceProxy and ignores this value entirely.
17190
- value: void 0
17191
- }
17192
- ]
17193
- };
17194
- const motionZonesSection = {
17195
- id: "motion-zones",
17196
- title: "Motion Zones",
17197
- tab: "motion",
17198
- location: "settings",
17199
- columns: 1,
17200
- order: 0,
17201
- fields: [
17202
- {
17203
- // Widget fields manage their own state via DeviceProxy —
17204
- // `ConfigWidgetField` carries no `value` (unlike the legacy
17205
- // `zone-editor` field type which still does). No `label` —
17206
- // the section title already reads "Motion Zones"; a field
17207
- // label here would duplicate it.
17208
- type: "widget",
17209
- key: "motion-zones",
17210
- label: "",
17211
- widgetId: "pipeline-orchestrator/motion-zones-editor",
17212
17444
  span: 1
17213
17445
  }
17214
17446
  ]
17215
17447
  };
17216
17448
  return {
17217
- sections: [...baseSections, zonesSection, motionZonesSection]
17449
+ sections: [...baseSections, zonesSection]
17218
17450
  };
17219
17451
  }
17220
17452
  // `getCameraPipelineWithFallback` was a thin wrapper over
@@ -18067,14 +18299,16 @@ class PipelineOrchestratorAddon extends BaseAddon {
18067
18299
  async handleInferenceResult(payload) {
18068
18300
  const ctx = this.ctx;
18069
18301
  if (!ctx) return;
18070
- const { deviceId, frame } = payload;
18302
+ const { deviceId, frame, frameHandle } = payload;
18071
18303
  if (frame.detections.length === 0) return;
18072
18304
  this.ctx.eventBus.emit({
18073
18305
  id: `detection-${deviceId}-${Date.now()}`,
18074
18306
  category: EventCategory.DetectionResult,
18075
18307
  source: { type: "device", id: deviceId, nodeId: "hub", addonId: "pipeline-orchestrator", deviceId },
18076
18308
  timestamp: /* @__PURE__ */ new Date(),
18077
- data: { frame, analysisResults: [] }
18309
+ // Forward the upstream shm-ring `frameHandle` so post-analysis
18310
+ // consumers (Task 8) can resolve the original frame zero-copy.
18311
+ data: { frame, analysisResults: [], frameHandle }
18078
18312
  });
18079
18313
  }
18080
18314
  /**
@@ -18165,16 +18399,6 @@ class PipelineOrchestratorAddon extends BaseAddon {
18165
18399
  }
18166
18400
  const audioStream = config2.audioStreamId ?? config2.motionStreamId;
18167
18401
  const audioBrokerId = `${deviceId}/${audioStream}`;
18168
- const broker = await api.streamBroker.getBroker.query({
18169
- brokerId: audioBrokerId
18170
- });
18171
- if (!broker) {
18172
- this.ctx.logger.warn("No broker found for audio subscription", {
18173
- tags: { deviceId },
18174
- meta: { brokerId: audioBrokerId }
18175
- });
18176
- return null;
18177
- }
18178
18402
  const settings = await api.audioAnalysis.resolveDeviceSettings.query({
18179
18403
  deviceId
18180
18404
  });
@@ -18192,8 +18416,12 @@ class PipelineOrchestratorAddon extends BaseAddon {
18192
18416
  meta: { audioNodeId, isRemote: isRemoteAudio }
18193
18417
  });
18194
18418
  let loggedAnalyzerStatus = false;
18195
- const unsub = broker.onDecodedAudioChunk(
18196
- async (chunk) => {
18419
+ const teardown = await startAudioChunkPoller({
18420
+ api,
18421
+ brokerId: audioBrokerId,
18422
+ tag: "audio-analyzer",
18423
+ logger: this.ctx.logger,
18424
+ onChunk: async (chunk) => {
18197
18425
  if (!loggedAnalyzerStatus) {
18198
18426
  loggedAnalyzerStatus = true;
18199
18427
  this.ctx.logger.info("audio status", {
@@ -18282,12 +18510,18 @@ class PipelineOrchestratorAddon extends BaseAddon {
18282
18510
  meta: { error: msg }
18283
18511
  });
18284
18512
  }
18285
- },
18286
- { tag: "audio-analyzer" }
18287
- );
18513
+ }
18514
+ });
18515
+ if (!teardown) {
18516
+ this.ctx.logger.warn("audio subscription failed — no broker", {
18517
+ tags: { deviceId },
18518
+ meta: { brokerId: audioBrokerId }
18519
+ });
18520
+ return null;
18521
+ }
18288
18522
  this.ctx.logger.info("Audio stream subscribed", { tags: { deviceId } });
18289
18523
  return () => {
18290
- unsub();
18524
+ teardown();
18291
18525
  };
18292
18526
  }
18293
18527
  }