Video Stream API
Media clients (compatibility facade).
The implementation lives in frame (frame types and pixel-format
helpers), encoded (encoded-stream UDS client) and
fd_client (zero-copy DMA-BUF fd client). This module re-exports
the full historical surface — including the private wire-protocol names
tests and internal callers patch — so from .media import X keeps
working unchanged.
FdMediaClient
- class neoruntime_ipc_sdk.FdMediaClient(socket_path=None)[source]
Bases:
objectZero-copy media client using DMA-BUF FD passing over Unix Domain Socket.
- __init__(socket_path=None)[source]
- get_frame(stream_id, timeout_ms=5000, *, keep_fd=False)[source]
Receive one frame.
With
keep_fd=Truethe frame’s dma-buf fds are retained (zero-copy handoff; seeFrameHandle) instead of copied, and the daemon-side buffer release is deferred untilframe.release()/ GC / client close.
- subscribe_raw(stream_id, skip_frames=True, keep_fd=False)[source]
- subscribe(stream_id, skip_frames=True, keep_fd=False)[source]
- on_frame(stream_id, callback)[source]
- close()[source]
- get_encoded_stream(stream_id='main', socket_dir=None)[source]
Return an
EncodedStreamClientfor the given encoded stream.- Parameters:
- Returns:
A connected
EncodedStreamClientreading from{socket_dir}/{stream_id}.sock.- Return type:
- list_streams()[source]
List available raw stream IDs by scanning the camera socket.
Returns common stream IDs. For detailed status use
CameraClient.get_stream_status.
- get_rtsp_url(stream_id='main', host='192.0.2.72', port=8554)[source]
Return an RTSP URL for the given stream.
Note: RTSP must be enabled on the device first (via CameraClient or REST API).
Data Types
Frame
- class neoruntime_ipc_sdk.Frame(sequence: 'int', timestamp_ns: 'int', width: 'int', height: 'int', format: 'str', image: 'np.ndarray | None', metadata: 'dict[str, Any]'=<factory>, handle: 'FrameHandle | None' = None)[source]
- sequence: int
- timestamp_ns: int
- width: int
- height: int
- format: str
- handle: FrameHandle | None = None
- to_array()[source]
Return the raw pixel buffer, materializing a retained fd.
For keep-fd frames this maps the dma-buf planes once (with the DMA_BUF_IOCTL_SYNC read fences) and caches the copy in
image; later calls return the cache without re-mapping.
- release()[source]
Release a retained fd frame back to the daemon (idempotent).
No-op for frames that were copied on receive.
- to_rgb()[source]
- crop(x, y, width, height)[source]
Return a new Frame cropped to the given pixel rectangle.
NV12/NV21 require even x, y, width, height (chroma subsampling). The original Frame is left untouched.
- resize(width, height, mode='letterbox', pad_value=114)[source]
Return a new Frame resized to width x height.
- Modes:
“letterbox”: fit inside, preserve aspect ratio, pad with pad_value (NV12 pads luma with pad_value and chroma with neutral 128). Default.
“stretch”: fill exactly, aspect ratio not preserved.
“crop”: scale to cover, center-crop the overflow.
NV12/NV21 require even target dimensions. Frames received with keep_fd=True are scaled on the DSP without materializing their dma-bufs first (falling back to the CPU path when the DSP service is unavailable). The fast path respects the accel router’s policy:
SOFTWARE_ONLYskips the DSP attempt,HARDWARE_ONLYraises on a failed attempt instead of silently degrading, and the defaultPREFER_HARDWARErecords the fallback in the router’s health counters. cv2 accelerates the CPU path when available; a pure-numpy nearest-neighbour path is the fallback.
- to_jpeg_bytes(quality=85)[source]
Encode the frame as JPEG bytes, hardware-first for keep-fd frames.
Frames with a live dma-buf handle ride the accel router’s zero-copy EncodeImage leg (the daemon imports the buffer — no RGB materialization, no read-back) and degrade to the CPU cv2/Pillow encode when the daemon cannot serve them. In-memory frames stay on the CPU encode outright: the daemon encoder is N-threaded libjpeg behind an RPC, so its win is the zero-copy import, not raw speed — tight loops (e.g. MjpegStream.push_frame) must not pay pool-alloc + copy + RPC per frame (S-3 record).
- save(path)[source]
- __init__(sequence, timestamp_ns, width, height, format, image, metadata=<factory>, handle=None)
FrameHandle
- class neoruntime_ipc_sdk.FrameHandle(fds, strides, plane_sizes, frame_id, on_release=None, width=0, height=0, format='')[source]
Retained dma-buf backing store for one received frame (SDK-1).
In keep-fd mode the per-plane dma-buf fds the daemon passed with the FRAME message are kept open here and the RELEASE message — the daemon’s buffer-recycling ticket — is deferred until
close(). CPU access to the pixels must go throughFrame.to_array(), which applies the required DMA_BUF_IOCTL_SYNC read fences. The fds can also be handed to aDspClientjob as-is for a zero-copy hardware path (resize_hw(frame, ...)); the geometry carried here is what the daemon needs to import them.
StreamInfo
PixelFormat
EncodedStreamClient
- class neoruntime_ipc_sdk.EncodedStreamClient(socket_path=None, *, stream_id='main', socket_dir=None)[source]
Read encoded video frames from an EncodedPublisher UDS socket.
Connects to sockets like
/run/aipc/encoded/main.sockand yieldsEncodedFrameobjects containing H.264/H.265 NAL units.Usage:
client = EncodedStreamClient() # main stream client = EncodedStreamClient(stream_id="sub") # sub stream client = EncodedStreamClient("/run/aipc/encoded/main.sock") # explicit for frame in client.subscribe(): print(f"{frame.codec_name} {frame.width}x{frame.height} " f"keyframe={frame.is_keyframe} {len(frame.data)}B")
EncodedFrame
Usage Examples
Getting Raw Video Stream
from neoruntime_ipc_sdk import FdMediaClient
import cv2
media = FdMediaClient()
# Get main stream (available IDs come from list_streams(), usually main / sub)
for frame in media.subscribe("main"):
print(f"Frame {frame.sequence}: {frame.width}x{frame.height}")
print(f"Format: {frame.format}, Timestamp: {frame.timestamp_ns}")
# frame.image is a numpy array (H, W, C) or (H*3//2, W) for NV12
# Can be used directly with OpenCV or other image processing libraries
cv2.imshow("Camera", frame.image)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cv2.destroyAllWindows()
No Frame Skipping
# Get every frame (no skipping)
for frame in media.subscribe("main", skip_frames=False):
process_frame(frame.image)
Getting a Single Frame
# Get a single frame
frame = media.get_frame("main", timeout_ms=1000)
if frame:
print(f"Frame: {frame.width}x{frame.height}")
print(f"Format: {frame.format}")
Getting Stream Info
# FdMediaClient has no separate stream-info API;
# read dimensions and format from the first frame
frame = media.get_frame("main", timeout_ms=5000)
if frame:
print(f"Resolution: {frame.width}x{frame.height}")
print(f"Format: {frame.format}")
print(f"Available streams: {media.list_streams()}")
Listing Available Streams
# List all available streams
streams = media.list_streams()
for stream_id in streams:
print(f"Stream: {stream_id}")
Frame Callback
def handle_frame(frame):
print(f"Received frame: {frame.sequence}")
# Use callback for frame processing
thread = media.on_frame("main", handle_frame)
# Keep running
import time
while True:
time.sleep(1)
Image Processing
import cv2
media = FdMediaClient()
for frame in media.subscribe("main"):
# Convert to RGB
rgb = frame.to_rgb()
# Grayscale
gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
# Edge detection
edges = cv2.Canny(gray, 100, 200)
# Display results
cv2.imshow("Original", rgb)
cv2.imshow("Edges", edges)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
Saving Images
media = FdMediaClient()
for frame in media.subscribe("main"):
# Save as image
frame.save("frame.jpg")
break
Getting Encoded Stream
get_encoded_stream() returns an EncodedStreamClient (not an iterator) yielding H.264/H.265 Annex-B packets, which the recording module can write to disk directly:
from neoruntime_ipc_sdk import FdMediaClient
media = FdMediaClient()
# get_encoded_stream() returns an EncodedStreamClient, not an iterator
client = media.get_encoded_stream("main")
for packet in client.subscribe():
print(f"{packet.codec_name()} packet: {len(packet.data)} bytes")
if packet.is_keyframe():
print(" keyframe")
Multi-Stream Processing
import threading
def process_main_stream():
media = FdMediaClient()
for frame in media.subscribe("main"):
# Process main stream (high resolution)
process_high_res(frame.image)
def process_sub_stream():
media = FdMediaClient()
for frame in media.subscribe("sub"):
# Process sub stream (low resolution)
process_low_res(frame.image)
# Process multiple streams in parallel
t1 = threading.Thread(target=process_main_stream)
t2 = threading.Thread(target=process_sub_stream)
t1.start()
t2.start()
t1.join()
t2.join()
Frame Rate Control
import time
media = FdMediaClient()
target_fps = 10
frame_interval = 1.0 / target_fps
last_time = time.time()
for frame in media.subscribe("main"):
current_time = time.time()
elapsed = current_time - last_time
if elapsed >= frame_interval:
# Process frame
process_frame(frame.image)
last_time = current_time
Saving Video
To save the H.264/H.265 encoded stream, prefer reusing it directly with the recording module (TsWriter / HlsWriter) — no decode/re-encode needed. The example below decodes frames and saves them with OpenCV:
import cv2
media = FdMediaClient()
# Read the resolution from the first frame
# (FdMediaClient has no separate stream-info API)
first = media.get_frame("main", timeout_ms=5000)
if first is None:
raise RuntimeError("no frame received")
# Create video writer (fill in the fps of your actual stream, 30 here)
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(
'output.mp4',
fourcc,
30.0,
(first.width, first.height)
)
frame_count = 0
max_frames = 300 # Record 10 seconds (30fps)
for frame in media.subscribe("main"):
rgb = frame.to_rgb()
bgr = rgb[:, :, ::-1] # RGB to BGR
out.write(bgr)
frame_count += 1
if frame_count >= max_frames:
break
out.release()
print(f"Saved {frame_count} frames to output.mp4")
Context Manager
# Use context manager for automatic resource management
with FdMediaClient() as media:
for frame in media.subscribe("main"):
process_frame(frame.image)
Zero-Copy Access
# The default receive path copies pixel data on arrival
# (frame.image is immediately usable). keep_fd=True retains the
# dma-buf fds for true zero-copy: the buffer is only returned to
# the daemon on frame.release() / GC / client close
media = FdMediaClient()
for frame in media.subscribe("main", keep_fd=True):
# frame.handle holds the dma-buf fds
# frame.to_array() maps them once on first call and caches
result = inference_engine.process(frame.to_array())
# Return the buffer as early as possible (idempotent, optional)
frame.release()
Error Handling
media = FdMediaClient()
try:
for frame in media.subscribe("invalid_stream"):
process_frame(frame.image)
except Exception as e:
print(f"Stream access failed: {e}")
except KeyboardInterrupt:
print("User interrupted")
finally:
media.close()