Skip to main content

Node

A Node is the smallest composable unit in SiMa.ai Neat. Nodes are the things a Graph wires together: decode stages, preprocess stages, model stages, postprocess stages, sources, sinks, and application boundaries.

Reference:

What a Node represents​

A node contributes one logical piece of work plus the metadata the builder needs to wire it safely:

  • runtime work, such as decode, convert, preprocess, inference, postprocess, or sink behavior;
  • input/output contracts, such as expected media type, tensor shape, dtype, or caps;
  • deterministic backend naming, so describe(), diagnostics, metrics, and probes stay stable.

The main rule is simple:

Graph = nodes wired together

Model is the model-aware node source. A Model can be added directly to a Graph, or can provide reusable stage fragments such as preprocess, inference, and postprocess.

Pre-built node groups​

Some common patterns are more than one low-level node. Neat exposes those as pre-built groups so users do not have to hand-wire every source, decoder, converter, queue, or sink.

Think of a group as a reusable mini-graph: a named bundle of nodes with a clear contract.

Examples:

  • VideoInputGroup(...): file/video source + decode path.
  • RtspDecodedInput(...): RTSP source + decode path.
  • model route fragments: preprocess + inference + decode/postprocess around a compiled model.
#include "neat/runtime.h"
#include "neat/node_groups.h"

simaai::neat::Graph graph;

simaai::neat::nodes::groups::VideoInputGroupOptions vopt;
vopt.path = "/data/sample.mp4";
graph.add(simaai::neat::nodes::groups::VideoInputGroup(vopt));

Use pre-built groups when the shape is standard and the interesting part of your app is not the internal media plumbing. If you need custom topology, build the equivalent Graph yourself from lower-level nodes.

Pick a node or group​

Use this map before you open the full API reference.

TaskC++ starting pointPython starting point
App pushes tensors, images, or samplesnodes::Input("name", options)pyneat.nodes.input("name", options)
App pulls a full samplenodes::Output("name", options)pyneat.nodes.output("name", options)
Read frames from a filenodes::FileInput(...) plus decode nodes, or nodes::groups::VideoInputGroup(...)pyneat.nodes.file_input(...) plus decode nodes, or pyneat.groups.video_input(...)
Read one image as a sourcenodes::StillImageInput(...)pyneat.nodes.still_image_input(...)
Read a MIPI/libcamera cameranodes::CameraInput(...)pyneat.nodes.camera_input(...)
Read a USB (UVC) MJPEG cameranodes::UsbCameraInput(...)pyneat.nodes.usb_camera_input(...)
Read an RTSP streamnodes::RTSPInput(...), nodes::groups::RtspEncodedInput(...), or nodes::groups::RtspDecodedInput(...)pyneat.nodes.rtsp_input(...), pyneat.groups.rtsp_encoded_input(...), or pyneat.groups.rtsp_decoded_input(...)
Read HTTP multipart MJPEGnodes::groups::HttpMjpegDecodedInput(...), or nodes::HttpSource(...), MultipartJpegDemux(), JpegParse() for manual framingpyneat.groups.http_mjpeg_decoded_input(...), or pyneat.nodes.http_source(...), pyneat.nodes.multipart_jpeg_demux(), pyneat.nodes.jpeg_parse()
Decode imagesnodes::ImageDecode() or nodes::JpegDecode()pyneat.nodes.image_decode() or pyneat.nodes.jpeg_decode()
Encode raw videonodes::SimaEncode(SimaEncodeOptions{...})pyneat.nodes.sima_encode(options)
Decode encoded videonodes::SimaDecode(SimaDecodeOptions{...})pyneat.nodes.sima_decode(options)
Decode MJPEG camera frames (4:2:0 or 4:2:2)usb_camera_info(...).decoded_format as the nodes::SimaDecode(...) out_format, model input format to matchpyneat.usb_camera_info(...).decoded_format as the pyneat.nodes.sima_decode(...) out_format, model input format to match
Scale or rate-limit videonodes::VideoScale(), VideoRate(), ImageFreeze()pyneat.nodes.video_scale(), video_rate(), image_freeze()
Build a reusable image input pathnodes::groups::ImageInputGroup(...)pyneat.groups.image_input(...)
Build a reusable video input pathnodes::groups::VideoInputGroup(...)pyneat.groups.video_input(...)
Send video outnodes::groups::VideoSender(...) or H.264/UDP output helperspyneat.groups.video_sender(...) or H.264/UDP output helpers
Send metadata outMetadataSenderpyneat.MetadataSender
Render detectionsnodes::SimaRender(...)pyneat.nodes.sima_render(...)
Convert logits to class indexnodes::SimaArgMax(...)pyneat.nodes.sima_argmax(...)
Voxelize a LiDAR sweep into pillarsnodes::SimaVoxelize(SimaVoxelizeOptions{...})pyneat.nodes.sima_voxelize(options)
Scatter pillar features onto a BEV canvasnodes::SimaPillarScatter(SimaPillarScatterOptions{...})pyneat.nodes.sima_pillar_scatter(options)
Pool camera features into a BEV gridnodes::SimaBEVPoolPlan(...) and nodes::SimaBEVPool(...)pyneat.nodes.sima_bev_pool_plan(options) and pyneat.nodes.sima_bev_pool(options)

Use groups when you want a tested fragment. Use individual nodes when you need explicit topology. Use Graph helpers such as graphs::Branch(...) and graphs::Combine(...) for fan-out and fan-in instead of hiding topology behind duplicated outputs.

nodes::H264Decode(...) and pyneat.nodes.h264_decode(...) are deprecated compatibility wrappers. New H.264 decode graphs should use SimaDecode with SimaDecodeType::H264.

Encoded decode paths​

Choose the API based on what your graph receives:

Input available to the graphAPIOutput
Complete, parsed encoded access unitsSimaDecode(...)Raw decoded video
An RTSP stream that should remain encodedRtspEncodedInput(...)Parsed encoded access units
An RTSP stream that should be decodedRtspDecodedInput(...)Raw decoded video

SimaDecode does not connect to RTSP, depacketize RTP, or parse an elementary stream. Use it when the upstream stage or the application already provides complete encoded samples.

Encoded inputNative decoder optionTypical upstream path
H.264/AVC access unitsSimaDecodeType::H264 or SimaDecodeType::AVCRTSP H.264 depacketize + parse, or another parsed H.264 source
H.265/HEVC access unitsSimaDecodeType::H265 or SimaDecodeType::HEVCRTSP H.265 depacketize + parse, or another parsed H.265 source
Single JPEG framesSimaDecodeType::JPEGJpegParse() or an app-provided encoded JPEG sample
MJPEG frame streamSimaDecodeType::MJPEGHTTP multipart demux + JpegParse(), or RTSP JPEG depacketize + JpegParse()

The aliases select identical decoder paths: H264 equals AVC, and H265 equals HEVC. The same aliases are available through RtspCodec and FormatTag in C++ and Python.

H.264 input must use 8-bit, 4:2:0 chroma and progressive frames. High 10, High 4:2:2, and interlaced bitstreams are not supported.

H.265 input must use the HEVC Main profile with 8-bit, 4:2:0 chroma. Direct elementary-stream input must be parsed, Annex-B byte-stream, and access-unit aligned. Its parameter sets (VPS, SPS, and PPS) must reach the decoder before frames that depend on them. RtspEncodedInput handles RTP depacketization and H.265 parsing for the RTSP path. Main 10, 10-bit, 4:2:2, and monochrome 4:0:0 bitstreams are not supported. For grayscale content, encode an 8-bit 4:2:0 stream whose Y plane contains the grayscale image and whose U and V samples use the neutral value 128.

Verify a stream before using it:

ffprobe -v error -rtsp_transport tcp -select_streams v:0 \
-show_entries stream=codec_name,profile,pix_fmt \
-of default=noprint_wrappers=1 rtsp://camera.example/live

The H.265 decoder requires codec_name=hevc, profile=Main, and pix_fmt=yuv420p.

SimaDecodeOptions::raw_output controls the decoder's immediate output:

ValueDecoder output
true (default)Decoder output buffers: native NV12 by default, or converted I420.
falseConverted video/x-raw(memory:SystemMemory) output for CPU consumers.

SimaDecode requests zero-copy decoder output for NV12. Selecting I420 uses copy mode, which converts the hardware's NV12 pixels directly into the final I420 planes. Both modes use the decoder's DMA-BUF output pool. Setting raw_output=false adds a software adapter for system-memory output. RunOptions::output_memory separately controls whether a terminal output shares its backing buffer (ZeroCopy) or copies it into framework-owned storage (Owned).

The decoder plugin owns the hardware session. Core does not reserve capacity through an admission daemon or hold a decoder lease. Startup failures are reported through the normal pipeline error path.

Use dec_width, dec_height, and dec_fps only when you need to override the upstream dimensions or frame rate; -1 leaves them upstream-defined. These options do not reserve decoder capacity. Core passes an explicit num_buffers value to the decoder, which applies its hardware pool requirements; -1 keeps the decoder policy.

Leave decoder_tuning empty to keep the plugin default. Explicit values, including default and auto, are forwarded to the plugin alongside memory_opt and buffer count overrides so the decoder can apply its tuning precedence. throughput-low-latency disables decoded-picture reordering and uses command depth one through 720p, or two for larger frames. Use it only for streams encoded without B-frames; leave tuning unset for general streams.

nodes::JpegDecode() and pyneat.nodes.jpeg_decode() are separate CPU/GStreamer JPEG decode nodes. Use them for simple CPU image pipelines. Do not use them for native MJPEG video decode before model preprocessing.

For RTSP, prefer the codec-aware groups when the source shape is standard. H.264 remains the default. Select H.265 or HEVC explicitly.

Decode MJPEG cameras​

USB cameras send MJPEG with 4:2:0 or 4:2:2 color sampling; most use 4:2:2. With zero-copy output, the decoder writes only the stream's own sampling, so the graph must know it before it is built. For a USB camera, usb_camera_info(...) reports it as decoded_format; use that as the decoder's out_format. For another MJPEG source, set out_format to NV12 for 4:2:0 or NV16 for 4:2:2. A model reads NV16 directly: set its preprocessing input format to the same value, so 4:2:2 frames reach the model with no conversion stage.

#include "neat.h"

// camera: your UsbCameraInputOptions; also pass info to UsbCameraInput(camera, info).
const simaai::neat::UsbCameraInfo info = simaai::neat::usb_camera_info(camera);
simaai::neat::SimaDecodeOptions decode;
decode.type = simaai::neat::SimaDecodeType::MJPEG;
decode.out_format = info.decoded_format; // NV12 or NV16
decode.dec_width = 1920;
decode.dec_height = 1080;
decode.dec_fps = 30;
decode.raw_output = true; // zero-copy: the decoder writes the camera's own sampling
graph.add(simaai::neat::nodes::JpegParse());
graph.add(simaai::neat::nodes::SimaDecode(decode));

simaai::neat::Model::Options model_options;
model_options.preprocess.color_convert.input_format =
decode.out_format == simaai::neat::FormatTag::NV16 ? simaai::neat::PreprocessColorFormat::NV16
: simaai::neat::PreprocessColorFormat::NV12;

HttpMjpegDecodedInput(...) and RtspDecodedInput(...) still request NV12 from the decoder, so they support 4:2:0 MJPEG streams only.

Keep RTSP input encoded​

Use RtspEncodedInput(...) to depacketize and parse the stream without decoding it:

simaai::neat::nodes::groups::RtspEncodedInputOptions encoded;
encoded.url = "rtsp://camera.example/live";
encoded.codec = simaai::neat::nodes::groups::RtspCodec::HEVC;
encoded.source_fps = 30;

auto encoded_graph =
simaai::neat::nodes::groups::RtspEncodedInput(encoded);

RtspEncodedInput(...) stops after source depacketization and parse/framing. Use it when your app needs the encoded samples, for example to split one parsed stream into decode and video-output graphs. For H.265, source_fps is written to the parsed encoded caps when the stream does not advertise a valid frame rate.

Decode RTSP input​

Use RtspDecodedInput(...) to append SimaDecode(...) and return raw decoded video:

simaai::neat::nodes::groups::RtspDecodedInputOptions decoded;
decoded.url = "rtsp://camera.example/live";
decoded.codec = simaai::neat::nodes::groups::RtspCodec::H265;
decoded.source_fps = 30;

simaai::neat::Graph graph;
graph.add(simaai::neat::nodes::groups::RtspDecodedInput(decoded));
graph.add(simaai::neat::nodes::Output("frames"));

For H.265, RtspDecodedInput(...) uses source_fps for both the parsed caps and decoder dec-fps. Set it when the RTSP caps do not carry a valid frame rate; without either value, decoder startup fails. Neat does not probe the RTSP URL for this value. It declares the source cadence and does not request rate conversion. To change the output cadence, enable use_videorate and set video_rate_fps. Rate conversion drops excess frames and preserves source timestamps. The nodes::VideoRate() and pyneat.nodes.video_rate() factories take no arguments and always use drop-only=true, including when inserted by source groups. They do not duplicate frames to increase the output rate.

For HTTP multipart MJPEG, use HttpMjpegDecodedInput(...). HTTP multipart MJPEG is not RTP, so it uses HttpSource -> MultipartJpegDemux -> JpegParse -> SimaDecode(MJPEG) rather than RTSP depacketization.

Source-owned and sink groups​

Use source-owned groups when the graph should read media by itself. Build those graphs without app input, then pull outputs or let sink nodes handle delivery. Use sink groups when the graph should send video out without your app pulling every frame; a sink graph can still be app-pushed if it also has an Input boundary.

NeedGroupKey fields to set firstCommon follow-up fields
Still image fixturenodes::groups::ImageInputGroup(...) / pyneat.groups.image_input(...)pathfps, imagefreeze_num_buffers, output_caps
Video filenodes::groups::VideoInputGroup(...) / pyneat.groups.video_input(...)pathout_format, output_caps, sync_mode
Live MIPI/libcamera cameranodes::CameraInput(...) / pyneat.nodes.camera_input(...)width, height, framerate_num, format; optional camera_nameallow_cpu_fallback, queue fields, buffer_name
Live RTSP encoded H.264, H.265, or MJPEG streamnodes::groups::RtspEncodedInput(...) / pyneat.groups.rtsp_encoded_input(...)url, codec when not H.264source_fps, latency_ms, tcp, drop_on_latency, buffer_mode, RTP payload type
Live RTSP decoded H.264, H.265, or MJPEG streamnodes::groups::RtspDecodedInput(...) / pyneat.groups.rtsp_decoded_input(...)url, codec when not H.264source_fps, latency_ms, tcp, drop_on_latency, buffer_mode, decoded width/height, out_format, output_caps
HTTP multipart MJPEG streamnodes::groups::HttpMjpegDecodedInput(...) / pyneat.groups.http_mjpeg_decoded_input(...)urlout_format, decoder_next_element, decoded width/height/FPS, output_caps
Raw or encoded H.264/H.265/MJPEG video outputnodes::groups::VideoSender(...) / pyneat.groups.video_sender(...)VideoSenderOptions::FromRaw(encode_options) or Passthrough(codec)host, channel, video_port_base, encoder/RTP options
RTP/H.264 over UDP outputnodes::groups::UdpH264OutputGroup(...) / pyneat.groups.udp_h264_output_group(...)destination host/port when the defaults are not righth264_caps, payload_type, config_interval

A source-owned graph is a different runtime shape than an app-pushed graph:

source group -> model -> output

For MIPI cameras, use CameraInput directly rather than wrapping the camera in an app-pushed Input. Validate the camera with libcamera/GStreamer first, then build the graph as CameraInput -> model -> Output. See Use a MIPI Camera for the full path.

Run it with graph.build() or graph.run() without an input argument. Do not add an Input("image") in front of a source group unless your app really has a second public input to push.

simaai::neat::nodes::groups::RtspDecodedInputOptions source_options;
source_options.url = "rtsp://camera.example/live";

simaai::neat::Graph graph("rtsp_detector");
graph.add(simaai::neat::nodes::groups::RtspDecodedInput(source_options));
graph.add(model);
graph.add(simaai::neat::nodes::Output(
"detections",
simaai::neat::OutputOptions::Latest()));

auto run = graph.build();

Start with a group when the media plumbing is standard. Drop to individual nodes only when you need a custom topology or a source format the group does not describe.

Common SiMa nodes​

Some SiMa nodes are common enough that they have focused reference or how-to pages:

  • Preproc: fused CVU image preprocessing for resize, color conversion, normalization, quantization, tessellation, and runtime ROI lists.
  • CameraInput: source-owned MIPI/libcamera frames with adaptive SiMaAI memory handoff for downstream CVU/MLA stages.
  • SimaBoxDecode: detection postprocessing that decodes model heads into bounding boxes and uses upstream preprocess metadata for coordinate mapping.

Boundary nodes: Input and Output​

Input and Output are nodes too. They are special because they describe where data enters or leaves a graph fragment.

The important mental model:

Input("name") and Output("name") are named doors.

At the outside of the final app, those doors become public runtime APIs:

  • Input("image") becomes run.push("image", ...).
  • Output("classes") becomes run.pull("classes", ...).

Inside a larger graph, those same doors are just connection points. Neat removes the internal boundary nodes while building the executable runtime path and wires the real work directly.

simaai::neat::Graph route("route");
route.add(simaai::neat::nodes::Input("image"));
route.add(model);
route.add(simaai::neat::nodes::Output("classes"));

Used as a complete app:

auto run = route.build();
run.push("image", simaai::neat::TensorList{image_tensor});
auto classes = run.pull("classes");

Used inside a larger app:

simaai::neat::Graph app("app");
app.connect(camera, route);
app.connect(route, telemetry);

Conceptually, the internal boundary nodes lower to direct wiring:

camera -> model -> telemetry

The names are still preserved for diagnostics, endpoint inspection, metrics, and visualization. They just do not create extra queues, copies, or fake runtime sinks in the middle of the app.

Labels are not endpoints​

Keep these names separate:

NameWhat it isDoes it create push(...) / pull(...)?
Graph("detector")Graph label for diagnostics, exports, and logs.No.
nodes::Input("image") / pyneat.nodes.input("image")Public input boundary when it remains on the outside of the final graph.Yes: run.push("image", ...).
nodes::Output("detections") / pyneat.nodes.output("detections")Public output boundary when it remains on the outside of the final graph.Yes: run.pull("detections", ...).
Node or element namesBackend names used in diagnostics and pipeline lowering.No, unless exposed through an Input or Output boundary.

If a runtime call says the endpoint name is unknown, inspect run.input_names() and run.output_names() instead of guessing. Names are cheap; wrong names are expensive.

App-pushed input​

Use nodes::Input when your application already has the frame or tensor and wants to push it into Neat.

Use InputOptions when the boundary needs an explicit payload, format, size, timing, or allocation contract.

NeedField
Payload familypayload_type / PayloadType
Formatformat / FormatTag
Fixed dimensionswidth, height, depth
Dynamic input limitsmax_width, max_height, max_depth, max_bytes
Timing and capsfps_n, fps_d, caps_override
Live-source behavioris_live, do_timestamp, block, stream_type
Allocationmemory_policy, pool_min_buffers, pool_max_buffers
Advanced metadatabuffer_name, preprocess_meta

use_simaai_pool is deprecated. Use memory_policy in new code:

  • If existing code used use_simaai_pool=false, use memory_policy=SystemMemory.
  • For the default SiMa allocation behavior, use memory_policy=Auto.
  • Use Ev74 or Dms0 only when the input boundary needs a specific target.
  • Existing code and older Graph JSON using use_simaai_pool are still accepted for compatibility.
  • New saved Graph JSON writes memory_policy instead of use_simaai_pool.

Leave fields unset unless the graph boundary needs the contract. Guessing caps is how you summon the swamp.

simaai::neat::Graph graph;

simaai::neat::InputOptions iopt;
iopt.payload_type = simaai::neat::PayloadType::Image;
iopt.format = simaai::neat::FormatTag::RGB;
iopt.width = 224;
iopt.height = 224;
graph.add(simaai::neat::nodes::Input("image", iopt));

Input boundary recipes​

Use these patterns when the boundary contract matters before the first push.

BoundarySetLeave unset when
Fixed-size decoded imagepayload_type, format, width, heightThe pushed tensor or sample already carries complete image metadata.
Dynamic decoded imagepayload_type, format, max_width, max_height, and optional max_bytesThe graph only accepts one fixed size.
Encoded H.264 inputpayload_type = Encoded, format = H264, and H.264 caps on the pushed SampleThe graph source owns the stream through an RTSP or file group.
Encoded H.265 inputpayload_type = Encoded, format = H265, and H.265 caps on the pushed SampleThe graph source owns and parses the stream through an RTSP or file group.
Detection coordinate mappingpreprocess_metaThe model route owns preprocessing and emits the metadata itself.

For dynamic image input, bound the stream without pretending every frame has the same size:

simaai::neat::InputOptions iopt;
iopt.payload_type = simaai::neat::PayloadType::Image;
iopt.format = simaai::neat::FormatTag::BGR;
iopt.max_width = 1920;
iopt.max_height = 1080;
iopt.max_bytes = 1920 * 1080 * 3;

graph.add(simaai::neat::nodes::Input("image", iopt));

For parsed H.265 access units that your app pushes, make the boundary encoded, select FormatTag::H265, and select the matching decoder type. The media type is resolved from the tag, so no caps string is required. Set the decoder width, height, and FPS to the elementary stream values:

simaai::neat::Graph graph;

simaai::neat::InputOptions iopt;
iopt.payload_type = simaai::neat::PayloadType::Encoded;
iopt.format = simaai::neat::FormatTag::H265;

graph.add(simaai::neat::nodes::Input("video", iopt));

simaai::neat::SimaDecodeOptions decode;
decode.type = simaai::neat::SimaDecodeType::H265;
decode.dec_width = 1280;
decode.dec_height = 720;
decode.dec_fps = 30;
graph.add(simaai::neat::nodes::SimaDecode(decode));
graph.add(simaai::neat::nodes::Output("frames"));

auto run = graph.build();

auto push_h265_access_unit = [&run](std::vector<std::uint8_t> bytes) {
auto sample = simaai::neat::make_encoded_sample(
std::move(bytes),
"video/x-h265,parsed=true,stream-format=byte-stream,alignment=au");
sample.port_name = "video";
run.push("video", sample);
};

If you already use RtspDecodedInput(...) or VideoInputGroup(...), do not add this app-pushed boundary in front of it. Source-owned groups own their input path.

Sample output​

Use nodes::Output when the consumer needs a full Sample: payload plus stream/frame/timestamp metadata.

Use OutputOptions when pull behavior matters.

GoalUse
Keep the freshest outputOutputOptions::Latest() / pyneat.OutputOptions.latest()
Pull every frameOutputOptions::EveryFrame(...) / pyneat.OutputOptions.every_frame(...)
Pace output to the graph clockOutputOptions::Clocked(...) / pyneat.OutputOptions.clocked(...)
Combine multiple producerscombine_policy = CombinePolicy::ByFrame or ByPts
simaai::neat::Graph graph;
graph.add(simaai::neat::nodes::Input("image"));
graph.add(model);
graph.add(simaai::neat::nodes::Output(
"classes",
simaai::neat::OutputOptions::EveryFrame()));

const auto run = graph.build();

OutputOptions has four knobs, but the presets cover most apps:

FieldWhat it controlsPrefer
max_buffersHow many output samples may queue before backpressure or dropping.Latest(), EveryFrame(...), or Clocked(...) presets.
dropWhether overflow drops the oldest queued sample instead of blocking.Latest() for live preview-style output.
syncWhether output syncs to the graph clock.Clocked(...) for clock-paced output.
combine_policyHow one public Output combines several upstream producers.CombinePolicy::ByFrame only with valid frame_id; ByPts only with valid pts_ns.

Use EveryFrame(...) for records you must not lose. Use Latest() when freshness beats history. Do not make a live preview behave like a courtroom transcript.

Advanced: adapt image or video output to a tensor​

For normal model or graph outputs, stop here: use nodes::Output. If the payload is tensor-only and you do not need the full Sample envelope, pull it with pull_tensors(...).

Use Graph::add_output_tensor(...) only when image or video output must be converted, scaled, or rate-adjusted into a CPU-friendly UInt8 tensor before the app pulls it. The helper inserts the adapter path and creates an unnamed output. With one output, pull it with run.pull_tensors(...).

OutputTensorOptions controls format, target_width, target_height, and target_fps. Keep dtype as UInt8; the current public path rejects other dtypes. Most model outputs do not need this helper.

Quick decision guide​

  • Source is file, camera, or RTSP: use a pre-built input group from nodes::groups.
  • Source is app-produced tensor/frame: use nodes::Input.
  • Output consumer needs stream/frame/timestamp metadata: use nodes::Output and pull Sample objects.
  • Output is a model or tensor payload: use nodes::Output and pull tensors with pull_tensors(...) when you do not need the full Sample.
  • Output is image or video that must be resized, reformatted, or rate-adjusted before pull: use the advanced add_output_tensor(...) adapter.
  • Need a reusable bundle: make a Graph fragment from nodes and add/connect it like any other building block.

Why this matters​

  • One concept covers atomic stages, boundary declarations, and pre-built groups.
  • Reusable graph fragments behave like functions: inputs in, outputs out, no accidental runtime sources/sinks in the middle.
  • Diagnostics stay friendly because node names and boundary names survive lowering.
  • Runtime stays efficient because internal boundaries do not force hidden copies or queues.

See also​

Tutorials​

UdpOutputGroupG is deprecated and retained for source compatibility. Build an explicit graph from supported nodes, using VideoSender for video and MetadataSender for metadata as needed. The legacy group requires render and sample-demux plugins that are not included in every runtime package.

Hardware encoding​

SimaEncodeOptions.type selects H.264, H.265 or MJPEG. The encoder accepts native 8-bit NV12. Its input adapter retains compatible SiMaAI NV12 buffers and prepares supported CPU raw layouts once. Output is encoded frames; add packetization only when sending RTP.

simaai::neat::SimaEncodeOptions encode;
encode.type = simaai::neat::SimaEncodeType::H265;
encode.fps = 30;
encode.bitrate_kbps = 4000;
encode.gop_length = 30;
encode.idr_interval = 90;
graph.add(simaai::neat::nodes::SimaEncode(encode));
encode = pyneat.SimaEncodeOptions()
encode.type = pyneat.SimaEncodeType.MJPEG
encode.fps = 30
encode.quality = 80
graph.add(pyneat.nodes.sima_encode(encode))

Resolution is taken from the input frames when encoding starts. The encoder does not resize them. Start a new run to encode a different resolution. Input dimensions must be positive and even. nodes::SimaEncode() and pyneat.nodes.sima_encode() use the default H.264 settings. Do not repeat input dimensions in encoder options. fps configures stream cadence and rate control; it does not pace input. H.264/H.265 support bitrate_kbps (default 4000), rate_control (vbr or cbr), profile, level (default 4.0), gop_length and separate idr_interval. H.264 defaults to baseline; H.265 supports main. The runtime can increase a requested level when the stream requires it and reports the effective setting. GOP zero or unset uses FPS; IDR zero or unset uses three times FPS. GOP one is all-intra. B-frame encoding is not exposed.

Codec-specific options start unset. MJPEG accepts quality from 1 to 100 (default 80), and rejects explicitly supplied video-only settings. Conversely, H.264/H.265 reject quality. num_buffers selects 1 to 20 encoded-output buffers; -1 preserves the native default of four. It does not size producer input storage. Native 4:2:2 and high-bit-depth encoding are not supported by this API.

H264EncodeSima and pyneat.nodes.h264_encode_sima are deprecated compatibility entry points. Their arguments, getters and native input behavior remain supported. Like H264Decode, C++ calls warn when SIMA_ENABLE_LEGACY_DEPRECATION is defined before including the headers. Python calls emit DeprecationWarning. H264EncodeSW is unchanged. This release changes the C++ ABI to version 5; rebuild C++ consumers and use the matching rebuilt Python extension.

Selecting a VideoSender codec​

For raw input, pass the same encoder options to VideoSenderOptions::FromRaw. The codec selects both encoding and RTP packetization:

auto options = simaai::neat::nodes::groups::VideoSenderOptions::FromRaw(encode);
options.host = "127.0.0.1";
options.video_port_base = 9000;
auto sender = simaai::neat::nodes::groups::VideoSender(options);
options = pyneat.VideoSenderOptions.from_raw(encode)
options.host = "127.0.0.1"
sender = pyneat.groups.video_sender(options)

For encoded frames, use VideoSenderOptions::Passthrough(RtspCodec::H265) or pyneat.VideoSenderOptions.passthrough(pyneat.RtspCodec.H265). This path parses and packetizes the selected codec without encoding. Both factories also support H.264 and MJPEG. The factory records the input mode; graph construction selects the path once. is_raw_input() and is_encoded_input() expose that choice.

RTP payload defaults are H.264 96, H.265 98, and MJPEG 26; rtp.payload_type can override them within 0..127. rtp.config_interval affects H.264/H.265 only. The UDP port is video_port_base + channel. sync=true waits for packet timestamps against the pipeline clock; async=true allows the sink to wait for its first preroll buffer during startup. Both default to false. Python uses async_, and retains the async alias. These controls do not select encoder threads or pace producer submission.

The existing H.264 raw/encoded factories remain compatibility entry points. Use FromRaw for new raw senders. Sender options own a copy of the configuration; changing the original encode object has no effect. Later changes to the legacy options.encoder bitrate/profile/level fields remain authoritative for raw H.264/H.265. Python may also replace that whole object. MJPEG rejects nondefault video-only overrides. Encoded passthrough ignores encoder settings.

RTP/JPEG requires a compatible receiver, baseline JPEG with standard Huffman tables, and dimensions divisible by eight, from 8 to 2040 in each direction. Raw sending and encoded passthrough reject unsupported dimensions during caps negotiation before RTP packetization. Standalone SimaEncode retains its native geometry range. This does not add MJPEG viewing to Insight's RTP/WebRTC viewer.