Frame Injection API (Output Write Path)
App frame injection front end (PushFrame P0-P2).
FramePublisher composes the two halves of the output write path that the daemon exposes: a daemon-side DSP buffer pool (geometry-pinned dma-bufs, NV12 or ARGB32) and the CameraControl PushFrame RPCs (metadata only — the buffer crosses as a DSP-registry id, never as a raw fd number).
- class neoruntime_ipc_sdk.injection.FramePublisher(camera, dsp, stream_id='sub', pool_depth=4, *, mode='replace', fmt='nv12', inset=None, dest=(0, 0), session_id='', lease_timeout_s=5.0)[source]
Bases:
objectPublish frames into a stream’s encoder feed (REPLACE / OVERLAY).
The publisher resolves the target stream’s geometry from
GetStreamStatusat construction and rejects wrong-shaped input client-side, so the daemon never sees a doomed request.mode="replace"(P0): the pool matches the stream’s encode resolution and the daemon composites each pushed frame over the whole encode output. NV12 only — the RGB escape hatch ispublish_rgb().mode="overlay"(P2): the pool matchesinset(default: one quarter of the stream, even-rounded) and each frame is composed atdestinside the live picture.fmt="nv12"insets paste opaquely;fmt="argb"insets alpha-blend on the CPU (wire byte order [A, R, G, B]). Useneoruntime_ipc_sdk.camera.pip_dest()to place a corner-anchored picture-in-picture window.Frames round-robin across a small daemon-side pool: each
publish()writes one slot and pushes that slot’s registry id. On daemons that report the write-lease set (Fix-1) a slot is rewritten only after the daemon acknowledges it is no longer reading it — every PushFrame response carries the ids still queued or mid-bake, and with all slots busypublish()waits for the bake frontier (up tolease_timeout_s) and raises instead of silently tearing pixels. On older daemons the publisher falls back to the legacy blind rotation (a warning is logged once); give such deploymentspool_depth >= 4and publish at stream rate. The daemon-side queue is cap-3 drop-oldest, so publishing never backpressures the encoder; overflow shows up inCameraClient.injection_status()asframes_dropped.To show the injected stream in a browser, no server is needed:
neoruntime_ipc_sdk.web.platform_stream_url()composes the gateway URL of the (now app-affected) platform stream.Usage:
with FramePublisher(cam, dsp, stream_id="sub") as pub: pub.publish(frame) # uint8 (h*3//2, w) ndarray or tight NV12 bytes ... pub.publish_eos() # flush; ISP path restores at the next IDR x, y = pip_dest(1920, 1080, 480, 270, "bottom-right", 32) with FramePublisher(cam, dsp, stream_id="main", mode="overlay", fmt="argb", inset=(480, 270), dest=(x, y)) as pub: pub.publish(badge_rgba) # (270, 480, 4) uint8, [A, R, G, B]
- __init__(camera, dsp, stream_id='sub', pool_depth=4, *, mode='replace', fmt='nv12', inset=None, dest=(0, 0), session_id='', lease_timeout_s=5.0)[source]
- property width: int
Pool width (stream encode width for REPLACE, inset width for OVERLAY).
- property height: int
Pool height (stream encode height for REPLACE, inset height for OVERLAY).
- property stream_id: str
- property session_id: str
Lifecycle tag stamped on every push (”” = untagged, P2-13).
- property mode: str
- property lease_mode: bool
True when the daemon reports the write-lease set (slot-aware publishing); False on legacy daemons (blind rotation).
- publish(frame, pts_ns=0)[source]
Write one frame to the next pool slot and push its id.
NV12 pools take a uint8 ndarray shaped
(height*3//2, width)or tight NV12 bytes; ARGB32 pools take a uint8 ndarray shaped(height, width, 4)(wire byte order [A, R, G, B]) or tight bytes ofwidth * height * 4. Returns the daemon’sInjectionResult; raises on RPC failure (the pool slot was still written — the daemon-side drop-oldest queue absorbs it, seeframes_dropped).pts_nspaces the bake (device CLOCK_MONOTONIC, the same clock as frame timestamps): 0 = due immediately, a future value holds the frame until the encoder frontier passes it.Lease mode (daemon reporting
in_flight_buffer_ids): the slot is chosen among ids the daemon has released and the response’s lease snapshot replaces the local in-flight set, so a slot is never rewritten while its pixels may still be queued or mid-bake.
- property pool: DspBufferPool
The publisher’s daemon-side pool — the slots PushFrame accepts.
Exposed for device-side chains: a DSP job can write this pool directly (
dsp.blend_hw(..., out=pub.pool, dst_slot=slot)) and the frame is publishable without a single client-side pixel. The pool dies with the publisher —close()releases it — so hold the publisher open for as long as any writer targets it.
- acquire_slot()[source]
Reserve one pool slot for a device-side write.
The lease-aware half of the zero-copy chain: on daemons reporting the write-lease set the returned slot’s buffer id is absent from the queued+mid-bake in-flight set — the same rule
publish()applies before writing — so a DSP job may safely target it. Legacy daemons get the blind rotation; the caller owns the pacing there. The reservation is client-side bookkeeping only: the daemon learns the slot is in flight from the subsequentpush_slot()response, so push the slot promptly — an acquired-but-never-pushed slot is invisible to the lease set and may be handed out again.
- push_slot(slot, pts_ns=0)[source]
Push one already-written pool slot by id (no client write).
The zero-copy publish tail: the slot’s pixels were produced on the DSP straight into
pool(blend_hw(..., out=self.pool, dst_slot=slot)— or any*_hwcall taking it asdst_pool), so publishing is only the PushFrame RPC. The request is byte-identical topublish()’s; only the write is skipped, and the lease snapshot updates the same way.
- publish_stream(frames, pts_ns=None, end_with_eos=False, timeout_s=None)[source]
Push a run of frames over one client-streaming RPC.
framesis an iterable of the same payloadspublish()accepts;pts_nsis an optional parallel iterable of due timestamps (missing values default to 0). All requests ride a single PushFrameStream call: the daemon applies per-request validation and the first rejection ends the stream — raised here asRuntimeErrorwith the accepted-frame count in the text. Withend_with_eos=Truea final EOS request closes the session before the RPC returns; otherwise the stream ends in a clean half-close that leaves the session open (close it later viapublish_eos()).Slots recycle through the same pool as
publish(): the generator writes each frame into the next slot as gRPC pulls it. Client-streaming has no per-frame response, so in lease mode the generator blocks on_acquire_slot(), whose freshness comes frominjection_status()polling — the single-RPC efficiency is kept, only the write pace adapts. On legacy daemons passpool_depth >= 4and push at stream rate: a shallower pool rewrites a slot whose frame may still be queued (the queue then bakes the newer pixels).
- publish_rgb(rgb, pts_ns=0)[source]
Convert an RGB frame to NV12 and REPLACE the stream with it.
The daemon’s REPLACE contract is NV12-only, so the RGB path is composed here on the SDK side: one DMA-side
rgb24staging buffer, a hardwareCONVERT(DspClient.convert_hw()) into a one-deep NV12 staging pool, then a push of that pool’s buffer id.rgbis a uint8(height, width, 3)ndarray (RGB order — swap BGR beforehand) or tight bytes. If the DSP refuses or is unavailable the convert falls back to CPU and the result is copied into the staging pool, so the push is always well-defined (dsp.last_used_hwrecords the path taken).The staging slot is reused every call: lease mode blocks until the daemon has released the staging NV12 (the pushed id; the rgb staging is only ever read by the convert itself), legacy mode relies on the caller pacing at stream rate — faster publishing lets a still-queued frame bake with the next frame’s pixels.
- publish_eos()[source]
Flush the injection session (
buffer_id0 + end_of_stream).The daemon drops the session and its queue; the pure ISP path restores at the next IDR. Sent automatically — best effort — by the context-manager exit when frames were published and no EOS went out; failure there is a warning, not an error.
- close()[source]
Release the buffer pools (idempotent; sends no EOS itself).
The context-manager exit sends a best-effort EOS first when frames were published without one; a bare
close()never does — callers needing explicit control callpublish_eos().
FramePublisher
- class neoruntime_ipc_sdk.FramePublisher(camera, dsp, stream_id='sub', pool_depth=4, *, mode='replace', fmt='nv12', inset=None, dest=(0, 0), session_id='', lease_timeout_s=5.0)[source]
Bases:
objectPublish frames into a stream’s encoder feed (REPLACE / OVERLAY).
The publisher resolves the target stream’s geometry from
GetStreamStatusat construction and rejects wrong-shaped input client-side, so the daemon never sees a doomed request.mode="replace"(P0): the pool matches the stream’s encode resolution and the daemon composites each pushed frame over the whole encode output. NV12 only — the RGB escape hatch ispublish_rgb().mode="overlay"(P2): the pool matchesinset(default: one quarter of the stream, even-rounded) and each frame is composed atdestinside the live picture.fmt="nv12"insets paste opaquely;fmt="argb"insets alpha-blend on the CPU (wire byte order [A, R, G, B]). Useneoruntime_ipc_sdk.camera.pip_dest()to place a corner-anchored picture-in-picture window.Frames round-robin across a small daemon-side pool: each
publish()writes one slot and pushes that slot’s registry id. On daemons that report the write-lease set (Fix-1) a slot is rewritten only after the daemon acknowledges it is no longer reading it — every PushFrame response carries the ids still queued or mid-bake, and with all slots busypublish()waits for the bake frontier (up tolease_timeout_s) and raises instead of silently tearing pixels. On older daemons the publisher falls back to the legacy blind rotation (a warning is logged once); give such deploymentspool_depth >= 4and publish at stream rate. The daemon-side queue is cap-3 drop-oldest, so publishing never backpressures the encoder; overflow shows up inCameraClient.injection_status()asframes_dropped.To show the injected stream in a browser, no server is needed:
neoruntime_ipc_sdk.web.platform_stream_url()composes the gateway URL of the (now app-affected) platform stream.Usage:
with FramePublisher(cam, dsp, stream_id="sub") as pub: pub.publish(frame) # uint8 (h*3//2, w) ndarray or tight NV12 bytes ... pub.publish_eos() # flush; ISP path restores at the next IDR x, y = pip_dest(1920, 1080, 480, 270, "bottom-right", 32) with FramePublisher(cam, dsp, stream_id="main", mode="overlay", fmt="argb", inset=(480, 270), dest=(x, y)) as pub: pub.publish(badge_rgba) # (270, 480, 4) uint8, [A, R, G, B]
- __init__(camera, dsp, stream_id='sub', pool_depth=4, *, mode='replace', fmt='nv12', inset=None, dest=(0, 0), session_id='', lease_timeout_s=5.0)[source]
- property width: int
Pool width (stream encode width for REPLACE, inset width for OVERLAY).
- property height: int
Pool height (stream encode height for REPLACE, inset height for OVERLAY).
- property stream_id: str
- property session_id: str
Lifecycle tag stamped on every push (”” = untagged, P2-13).
- property mode: str
- property lease_mode: bool
True when the daemon reports the write-lease set (slot-aware publishing); False on legacy daemons (blind rotation).
- publish(frame, pts_ns=0)[source]
Write one frame to the next pool slot and push its id.
NV12 pools take a uint8 ndarray shaped
(height*3//2, width)or tight NV12 bytes; ARGB32 pools take a uint8 ndarray shaped(height, width, 4)(wire byte order [A, R, G, B]) or tight bytes ofwidth * height * 4. Returns the daemon’sInjectionResult; raises on RPC failure (the pool slot was still written — the daemon-side drop-oldest queue absorbs it, seeframes_dropped).pts_nspaces the bake (device CLOCK_MONOTONIC, the same clock as frame timestamps): 0 = due immediately, a future value holds the frame until the encoder frontier passes it.Lease mode (daemon reporting
in_flight_buffer_ids): the slot is chosen among ids the daemon has released and the response’s lease snapshot replaces the local in-flight set, so a slot is never rewritten while its pixels may still be queued or mid-bake.
- property pool: DspBufferPool
The publisher’s daemon-side pool — the slots PushFrame accepts.
Exposed for device-side chains: a DSP job can write this pool directly (
dsp.blend_hw(..., out=pub.pool, dst_slot=slot)) and the frame is publishable without a single client-side pixel. The pool dies with the publisher —close()releases it — so hold the publisher open for as long as any writer targets it.
- acquire_slot()[source]
Reserve one pool slot for a device-side write.
The lease-aware half of the zero-copy chain: on daemons reporting the write-lease set the returned slot’s buffer id is absent from the queued+mid-bake in-flight set — the same rule
publish()applies before writing — so a DSP job may safely target it. Legacy daemons get the blind rotation; the caller owns the pacing there. The reservation is client-side bookkeeping only: the daemon learns the slot is in flight from the subsequentpush_slot()response, so push the slot promptly — an acquired-but-never-pushed slot is invisible to the lease set and may be handed out again.
- push_slot(slot, pts_ns=0)[source]
Push one already-written pool slot by id (no client write).
The zero-copy publish tail: the slot’s pixels were produced on the DSP straight into
pool(blend_hw(..., out=self.pool, dst_slot=slot)— or any*_hwcall taking it asdst_pool), so publishing is only the PushFrame RPC. The request is byte-identical topublish()’s; only the write is skipped, and the lease snapshot updates the same way.
- publish_stream(frames, pts_ns=None, end_with_eos=False, timeout_s=None)[source]
Push a run of frames over one client-streaming RPC.
framesis an iterable of the same payloadspublish()accepts;pts_nsis an optional parallel iterable of due timestamps (missing values default to 0). All requests ride a single PushFrameStream call: the daemon applies per-request validation and the first rejection ends the stream — raised here asRuntimeErrorwith the accepted-frame count in the text. Withend_with_eos=Truea final EOS request closes the session before the RPC returns; otherwise the stream ends in a clean half-close that leaves the session open (close it later viapublish_eos()).Slots recycle through the same pool as
publish(): the generator writes each frame into the next slot as gRPC pulls it. Client-streaming has no per-frame response, so in lease mode the generator blocks on_acquire_slot(), whose freshness comes frominjection_status()polling — the single-RPC efficiency is kept, only the write pace adapts. On legacy daemons passpool_depth >= 4and push at stream rate: a shallower pool rewrites a slot whose frame may still be queued (the queue then bakes the newer pixels).
- publish_rgb(rgb, pts_ns=0)[source]
Convert an RGB frame to NV12 and REPLACE the stream with it.
The daemon’s REPLACE contract is NV12-only, so the RGB path is composed here on the SDK side: one DMA-side
rgb24staging buffer, a hardwareCONVERT(DspClient.convert_hw()) into a one-deep NV12 staging pool, then a push of that pool’s buffer id.rgbis a uint8(height, width, 3)ndarray (RGB order — swap BGR beforehand) or tight bytes. If the DSP refuses or is unavailable the convert falls back to CPU and the result is copied into the staging pool, so the push is always well-defined (dsp.last_used_hwrecords the path taken).The staging slot is reused every call: lease mode blocks until the daemon has released the staging NV12 (the pushed id; the rgb staging is only ever read by the convert itself), legacy mode relies on the caller pacing at stream rate — faster publishing lets a still-queued frame bake with the next frame’s pixels.
- publish_eos()[source]
Flush the injection session (
buffer_id0 + end_of_stream).The daemon drops the session and its queue; the pure ISP path restores at the next IDR. Sent automatically — best effort — by the context-manager exit when frames were published and no EOS went out; failure there is a warning, not an error.
- close()[source]
Release the buffer pools (idempotent; sends no EOS itself).
The context-manager exit sends a best-effort EOS first when frames were published without one; a bare
close()never does — callers needing explicit control callpublish_eos().
InjectionResult
- class neoruntime_ipc_sdk.InjectionResult(success, message, error_code=0, injected_frame_id=0, accepted_frame_count=0, session_id='', in_flight_buffer_ids=())[source]
Result of one CameraClient.push_frame call (PushFrameResponse).
- __init__(success, message, error_code=0, injected_frame_id=0, accepted_frame_count=0, session_id='', in_flight_buffer_ids=())
InjectionStatus
- class neoruntime_ipc_sdk.InjectionStatus(success, message, active=False, mode='replace', frames_injected=0, frames_dropped=0, queue_depth=0, session_id='', in_flight_buffer_ids=(), reports_in_flight_buffers=False)[source]
Snapshot of the daemon-side injection session (GetInjectionStatus).
- __init__(success, message, active=False, mode='replace', frames_injected=0, frames_dropped=0, queue_depth=0, session_id='', in_flight_buffer_ids=(), reports_in_flight_buffers=False)
CameraClient injection RPCs
- class neoruntime_ipc_sdk.CameraClient(endpoint=None)[source]
Camera pipeline control client.
Usage:
cam = CameraClient() # ISP cam.set_isp(brightness=60, contrast=50) isp = cam.get_isp() # Encoder cam.set_encoder("main", bitrate_bps=8_000_000) # RTSP cam.set_rtsp_enabled(True) # Streams streams = cam.get_stream_status() cam.add_stream("third", 1920, 1080, 30, "h264", 4_000_000, 30) # Profiles cam.switch_profile("night") cam.backup_profile() # Capabilities caps = cam.get_capabilities()
- set_encoder(stream_name='main', bitrate_bps=0, framerate=0, gop=0)[source]
Dynamic encoder config (no restart).
- reconfigure_encoder(stream_name, width=0, height=0, codec='', bitrate_bps=0, fps=0, gop=0)[source]
Full encoder reconfiguration (brief restart, ~100ms).
- set_imaging_mode(mode)[source]
Switch day/night imaging mode.
- Parameters:
mode (str) – “day”, “infrared” or “auto” (light-sensor driven).
Returns: the resulting InfraredStatus.
- set_infrared_settings(auto_follow=None, near_pwm=None, far_pwm=None, night_enter=None, day_enter=None)[source]
Update IR light settings; omitted fields (None) are left unchanged.
- Parameters:
auto_follow (bool | None) – tie IR output to optical zoom ratio.
far_pwm (near_pwm /) – manual IR intensities (0-100).
day_enter (night_enter /) – light-sensor thresholds for auto mode.
- save_ir_preset(name, zoom_ratio, near_pwm, far_pwm)[source]
Save (or overwrite) an IR preset and return the new list.
- set_privacy_mask(settings=None, *, color=None, blur_radius=None, enabled=None, regions=None, dpm_enabled=None, dpm_labels=None, dpm_mode=None, dpm_color=None)[source]
Update privacy-mask configuration with merge semantics.
Only the fields given as arguments are changed; everything else (including existing regions) is read back from the device and preserved, so partial updates never wipe masks.
- Parameters:
settings (PrivacyMaskSettings | None) – full PrivacyMaskSettings to apply wholesale (optional).
color (int | None) – 0x00RRGGBB fill color (used when blur_radius=0).
blur_radius (int | None) – pixelation block size 2-64; 0 = solid color.
enabled (bool | None) – global static-mask on/off.
regions (list[dict] | None) – list of {id, name, enabled, points_x, points_y} with normalized polygon coordinates (up to 8 points).
dpm_* – dynamic (AI) privacy-mask options.
- get_osd()[source]
Read back OSD overlay config, shaped like set_osd() input.
Returns: list of {stream_name, text_overlays, datetime_overlays, image_overlays}; each overlay is a plain dict of its proto fields, so the result can be edited and fed straight back into set_osd().
- get_config_field(field_path)[source]
Read one camera-daemon config field by dotted path.
- Parameters:
field_path (str) – e.g. “frontend.hailort.use-hailort-service”.
Returns: the current value encoded as a string.
- set_config_field(field_path, value)[source]
Write one camera-daemon config field by dotted path.
- Parameters:
field_path (str) – dotted config path.
value – value encoded as a string (e.g. “true”, “42”, “1.5”); non-str values are converted with str().
- get_stream_status(timeout_s=None)[source]
List live streams with encoder state;
timeout_sbounds the RPC (Nonewaits indefinitely, gRPC’s default).
- push_frame(buffer_id, width, height, stride, mode='replace', pts_ns=0, dest_x=0, dest_y=0, stream_id='', end_of_stream=False, session_id='', timeout_s=None)[source]
Push one frame into the daemon’s encoder feed (PushFrame).
buffer_idmust be a DSP-registry buffer id (seeDspBufferPool.buffer_id/DspClient.import_frame()) — a raw fd number never crosses gRPC. The registered buffer’s geometry must equalwidth/height.Modes (P2):
"replace"— full-frame NV12 content copy over one stream’s encode output.stream_idempty keeps the P0 semantics (any stream whose encode dims match); an explicitstream_idtargets that stream and requires dims equal to its encode resolution."overlay"— compose on top of the live stream. Requiresstream_id.dest_x/dest_y(even, NV12 chroma alignment) position the inset. NV12 insets paste opaquely; ARGB32 insets alpha-blend on the CPU.
pts_nspaces the bake (device CLOCK_MONOTONIC domain, same clock as frame timestamps): 0 = due immediately; a future value holds the frame until the encoder frontier passes it, and a newer due frame supersedes (drops) older due frames for the same target.end_of_stream=True(buffer_id0) flushes the session and restores the ISP path at the next IDR.session_idtags the injection session (P2-13 lifecycle): correlation and observability only — the daemon anchors the session on the buffer owner’s UDS connection and closes it on that client’s disconnect regardless of the tag.
- push_frame_stream(frames, timeout_s=None)[source]
Push a run of frames over one client-streaming RPC (PushFrameStream).
framesis an iterable (or generator) of keyword dicts, each accepted bypush_frame()— e.g.{"buffer_id": bid, "width": w, "height": h, "stride": s, "mode": "overlay", "stream_id": "main", "dest_x": 64, "dest_y": 64}. A dict withend_of_stream=Truecloses the injection session server-side before the RPC returns; omitting any EOS dict ends the stream with a clean half-close that LEAVES the session open (close later viastop_injection()).Per-request semantics are identical to unary
push_frame(); the first server-side rejection ends the stream and is re-raised here asRuntimeError(the accepted-frame count at that point rides the exception text). Asession_idkey in any request dict tags the session (P2-13) — the daemon records the latest non-empty tag and echoes it on the response.Returns an
InjectionResultwhoseaccepted_frame_countreports how many frames the daemon accepted before the stream ended.
- injection_status(timeout_s=None)[source]
Injection session snapshot: active, mode, counters, queue depth, the live session’s lifecycle tag (empty when untagged/closed), and the write-lease set (
in_flight_buffer_ids+reports_in_flight_buffers; Fix-1).
Examples
Publishing processed NV12 frames to the sub-stream encoder
from neoruntime_ipc_sdk import CameraClient, DspClient, FramePublisher
cam = CameraClient()
dsp = DspClient()
# The constructor resolves the sub-stream encode resolution from
# GetStreamStatus and pins the geometry; afterwards publish() only
# accepts tight NV12 at that resolution (ndarray or bytes)
with FramePublisher(cam, dsp, stream_id="sub") as pub:
for frame in processed_frames: # uint8 (h*3//2, w)
pub.publish(frame)
pub.publish_eos() # end the session; pure ISP restores at next IDR
Semantics
Two modes:
mode="replace"(P0) — an injected frame wholly replaces the encoder input;mode="overlay"(P2) — composites a small picture into the live image (fmt="nv12"pastes opaquely,fmt="argb"alpha-blends on the CPU; geometry comes frominset/dest).Frames cross processes as DSP-registry ids, never raw fds — the same buffer namespace shared by frame capture (FrameHandle) and zero-copy inference (
infer(frame)).Daemon-side queue caps at 3, drop-oldest on overflow: publish never backpressures the encoder; overflow surfaces as
injection_status().frames_dropped.Write lease (pool slots are never recycled early): the daemon reports the buffer ids it may still read (queued + mid-compose) on PushFrame responses and in injection status; the publish family writes only slots confirmed free, waits when the pool is exhausted, and raises an explicit error after
lease_timeout_s(default 5s) instead of silently overwriting live pixels. Defaultpool_depth=4. Against daemons that do not report the set it falls back to the legacy depth-paced rotation with a one-time warning (see the read-onlylease_modeproperty).End = restore: after
publish_eos()(orstop_injection()) the pure ISP path restores at the next IDR — no stream restart.Lifecycle tag (P2-13):
session_id=tags the injection session — the first tagged frame opens it, and the tag is observable atinjection_status().session_id. Reclaim is keyed on the connection, not the tag: when the injecting client disconnects (including a SIGKILLed process), camera-daemon closes the session and frees every buffer that client owned, whatever the tag says.close()only releases the buffer pool; it does not send EOS — ending the session requires an explicitpublish_eos().