@camstack/addon-pipeline-orchestrator 0.1.22 → 0.1.24

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