Skip to main content

Send Video

Use VideoSender when a Graph should send video to an external receiver. VideoSender returns a reusable Graph fragment, so add it with Graph::add(...).

VideoSender sends H.264 or H.265 over RTP/UDP. Raw input is encoded as H.264; encoded H.264 and H.265 input is forwarded without re-encoding. H.264 uses RTP payload type 96 by default, while H.265 uses 98. The default UDP port rule is video_port_base + channel, with video_port_base = 9000. If the receiver runs behind container port remapping, pass the mapped host and a matching video_port_base from the app.

Raw Frames

Use the raw path when the pipeline input to VideoSender is raw video frames. Neat selects the safe encoder ingress automatically:

NV12 with a proven compatible boundary:
H264EncodeSima -> H264Parse -> H264Packetize -> UdpOutput

Other or unknown raw formats:
VideoConvert -> H264EncodeSima -> H264Parse -> H264Packetize -> UdpOutput

The automatic selection does not add an application option or change the H264RtpUdpFromRaw(...) API. Proven NV12 in system or SiMaAI memory can feed the H.264 encoder directly when the installed encoder advertises input-layout-aware=true. RGB, BGR, grayscale, I420, unknown memory/layouts, and inputs without a reliable format contract retain one conversion to NV12.

Raw frame geometry and layout

width and height are the visible image dimensions. They do not need to be multiples of 8, 16, or 32. For the NV12 and I420 4:2:0 formats, both dimensions must be positive and even; the active codec, profile, level, and hardware define the remaining minimum and maximum limits. For example, 680x382, 672x384, and 642x480 are valid shapes when the installed encoder accepts them.

Hardware storage alignment is separate from visible geometry. Neat preserves the requested dimensions in caps and allocates or stages into encoder surfaces with the pitch and storage height required by the hardware. A raw buffer with a custom physical layout must carry GstVideoMeta with authoritative plane offsets and strides. Without that metadata, the negotiated GStreamer layout is used; property-driven file input must contain exactly one tightly packed frame per buffer. Invalid, truncated, or unsupported layouts fail synchronously instead of being partially copied.

simaai::neat::Graph graph;
const int channel = 0;

auto opt = simaai::neat::nodes::groups::VideoSenderOptions::H264RtpUdpFromRaw(
width, height, fps);
opt.host = "127.0.0.1";
opt.channel = channel;
opt.video_port_base = 9000;
opt.encoder.bitrate_kbps = 2500;

graph.add(simaai::neat::nodes::groups::VideoSender(opt));

Python:

channel = 0

opt = pyneat.VideoSenderOptions.h264_rtp_udp_from_raw(
width=1920,
height=1080,
fps=30,
)
opt.host = "127.0.0.1"
opt.channel = channel
opt.video_port_base = 9000
opt.encoder.bitrate_kbps = 2500

graph = pyneat.Graph()
graph.add(pyneat.groups.video_sender(opt))

Encoded H.264 or H.265

For encoded input, pass the stream codec to the passthrough factory. Neat parses, packetizes, and sends the stream without re-encoding.

CodecC++ factoryPython factoryDefault RTP payload type
H.264Passthrough(RtspCodec::H264)passthrough(pyneat.RtspCodec.H264)96
H.265Passthrough(RtspCodec::H265)passthrough(pyneat.RtspCodec.H265)98

MJPEG passthrough is rejected: the sender has no RTP/JPEG packetizer.

H.265 example:

auto opt = simaai::neat::nodes::groups::VideoSenderOptions::Passthrough(
simaai::neat::nodes::groups::RtspCodec::H265);
opt.host = "127.0.0.1";
opt.channel = 0;
graph.add(simaai::neat::nodes::groups::VideoSender(opt));
opt = pyneat.VideoSenderOptions.passthrough(pyneat.RtspCodec.H265)
opt.host = "127.0.0.1"
opt.channel = 0
graph.add(pyneat.groups.video_sender(opt))

Fan out encoded RTSP to inference and preview

When one encoded RTSP source feeds both decoding/inference and VideoSender, connect the source directly to the sender. For a live preview such as Insight, set the encoded sender edge to RealtimeLatestByStream:

simaai::neat::GraphLinkOptions video_link;
video_link.policy = simaai::neat::GraphLinkPolicy::RealtimeLatestByStream;

graph.connect(encoded_source, decoder);
graph.connect(decoder, detector, detector_link);
graph.connect(encoded_source, video_sender, video_link);
video_link = pyneat.GraphLinkOptions()
video_link.policy = pyneat.GraphLinkPolicy.RealtimeLatestByStream

graph.connect(encoded_source, decoder)
graph.connect(decoder, detector, detector_link)
graph.connect(encoded_source, video_sender, video_link)

The sender branch stays before SimaDecode, so it does not re-encode video or copy decoded frames to CPU. With RealtimeLatestByStream, the fused sender branch keeps at most one pending encoded access unit and replaces stale data if UDP egress slows. The default edge policy remains lossless and can backpressure the shared encoded source, including its decoder branch. Use the default only when preserving every access unit is more important than keeping live inference fresh.