Skip to main content

Send JSON Metadata

Use MetadataSender when an external viewer, recorder, or service accepts UTF-8 JSON metadata over UDP. Insight is one receiver that understands this wire contract.

Wire Contract

  • Default host: 127.0.0.1
  • Default metadata port base: 9100
  • Channel port rule: metadata_port_base + channel
  • Default send mode: nonblocking (MSG_DONTWAIT)
  • Payload encoding: UTF-8 JSON text
  • Required top-level fields: type, data
  • Maximum logical payload: 65,507 bytes

MetadataSender keeps each UDP payload at or below 1200 bytes. JSON payloads up to 1200 bytes remain one unchanged datagram. Larger payloads are split into chunks with this 12-byte binary header:

ByteSizeValue
01Magic byte 0x4e
11Protocol version 0x01
28Message ID as an unsigned 64-bit big-endian integer
101Zero-based chunk index
111Total chunk count

Each chunk carries up to 1188 JSON bytes. A receiver reassembles chunks with the same sender address and message ID in chunk-index order before parsing the JSON. UDP delivery remains best effort: the sender does not retry a failed chunk, and send_raw_json(...) or send_metadata(...) returns false after the first local send failure.

Receivers should accept both unchanged JSON datagrams and versioned chunks. Update Insight to a release with chunk reassembly before or together with this Neat Library version. Older Insight versions continue to receive payloads up to 1200 bytes, but cannot decode larger chunked payloads.

For Insight, pair metadata channel N with the video UDP stream on 9000 + N. If Insight or another receiver runs behind container port remapping, pass the mapped host and port explicitly from the app.

Tracking, tracklets, and other custom metadata can be sent as generic JSON. Viewer overlay support is receiver-specific; Insight tracking visualization is tracked separately in sima-neat/insight#8.

C++

simaai::neat::MetadataSenderOptions opt;
opt.host = "127.0.0.1";
opt.channel = 0;
opt.metadata_port_base = 9100;

std::string err;
simaai::neat::MetadataSender sender(opt, &err);

sender.send_metadata(
"tracking",
R"({"tracks":[{"id":"trk-1","bbox":[10,20,30,40]}]})",
12345,
"frame-7",
&err);

send_metadata(...) validates data_json and builds this envelope:

{
"type": "tracking",
"timestamp": 12345,
"frame_id": "frame-7",
"data": {
"tracks": [
{
"id": "trk-1",
"bbox": [10, 20, 30, 40]
}
]
}
}

Use send_raw_json(...) only when the caller already built the full top-level payload:

sender.send_raw_json(
R"({"type":"object-detection","data":{"objects":[{"id":"obj_1","label":"car","confidence":0.92,"bbox":[120,80,96,64]}]}})",
&err);

Real-Time Dispatch Is Nonblocking by Default

MetadataSender applies MSG_DONTWAIT to each datagram by default so a locally congested send buffer cannot delay a thread that also dispatches video or inference work. When the kernel cannot accept a datagram immediately, the send returns false instead of waiting. Treat that metadata packet as dropped and continue real-time work; UDP delivery is not guaranteed.

The default constructor and the default send options are equivalent:

simaai::neat::MetadataSenderSendOptions send_opt;
simaai::neat::MetadataSender sender(opt, send_opt, &err);

Callers that explicitly prefer blocking delivery attempts can opt in:

simaai::neat::MetadataSenderSendOptions send_opt;
send_opt.nonblocking = false;
simaai::neat::MetadataSender sender(opt, send_opt, &err);

Use stats() to distinguish congestion from other failures and, in explicit blocking mode, to detect slow calls:

const auto stats = sender.stats();
std::cerr << "sent=" << stats.datagrams_sent
<< " would_block=" << stats.would_block
<< " enobufs=" << stats.no_buffer_space
<< " max_send_ns=" << stats.max_send_duration_ns << '\n';

stats() is safe to read while sends are in progress. Treat the result as a concurrent diagnostic snapshot, not a transactional snapshot of one instant.

Python

import json
import pyneat

opt = pyneat.MetadataSenderOptions()
opt.host = "127.0.0.1"
opt.channel = 0
opt.metadata_port_base = 9100

sender = pyneat.MetadataSender(opt)

sender.send_metadata(
"object-detection",
json.dumps(
{
"objects": [
{
"id": "obj_1",
"label": "car",
"confidence": 0.92,
"bbox": [120, 80, 96, 64],
}
]
}
),
12345,
"frame-7",
)

stats = sender.stats()
print(stats.datagrams_sent, stats.would_block, stats.max_send_duration_ns)

As in C++, explicitly set send_opt.nonblocking = False and pass it as the second constructor argument only when blocking behavior is required.