视频流 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]
- property retained_frames: int
Number of keep-fd frames currently held (not yet released).
- 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=1, keep_fd=False)[source]
Yield frames from
stream_id, auto-reconnecting on socket errors.skip_framesis a decimation interval: 1 (the default, and the effective value of the historicalTrue/Falseflags) yields every frame; N > 1 yields every Nth frame. Decimated keep-fd frames are released back to the daemon immediately so their dma-bufs are not retained.
- subscribe(stream_id, skip_frames=1, keep_fd=False)[source]
Yield frames; see
subscribe_raw()for the parameters.
- 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 querying the camera daemon.
Streams whose pipeline is not active are excluded. Falls back to the platform’s guaranteed streams (
main/sub) when the camera control service is unreachable, unresponsive (1s probe deadline) or reports nothing, so data-plane-only setups keep working. Results are cached for_LIST_STREAM_CACHE_Sseconds. For detailed status (resolution, fps, codec) useCameraClient.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).
数据类型
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, flags: 'int' = 0)[source]
- sequence: int
- timestamp_ns: int
- width: int
- height: int
- format: str
- handle: FrameHandle | None = None
- flags: int = 0
- property baked_overlay: bool
True when the AI-overlay bake pass is active on this stream.
- property baked_dpm: bool
True when the DPM (privacy mask) render pass is active.
- 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. Also invoked by
with frame:on exit — the context manager makes the keep-fd ownership window explicit:>>> with media.get_frame("sub", keep_fd=True) as frame: ... result = pipeline.run(frame) >>> # handle returned to the daemon here, even on exceptions
- 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. The crop geometry is recorded in the new frame’s
metadata['transform'](affine, scale 1:1, origin (-x, -y)).
- 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.The geometry is recorded in the new frame’s
metadata['transform']as a per-axis affine mappingdst = src * scale + origin(composed across chained crop/resize calls). Postprocessing maps model-input coordinates back to the source frame withsrc = (dst - origin) / scale— no hand-derived letterbox maths in the app.
- 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, flags=0)
FrameHandle
- class neoruntime_ipc_sdk.FrameHandle(fds, strides, plane_sizes, frame_id, on_release=None, width=0, height=0, format='', flags=0)[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
使用示例
获取原始视频流
from neoruntime_ipc_sdk import FdMediaClient
import cv2
media = FdMediaClient()
# 获取主码流(可用流 ID 见 list_streams(),通常为 main / sub)
for frame in media.subscribe("main"):
print(f"帧 {frame.sequence}: {frame.width}x{frame.height}")
print(f"格式: {frame.format}, 时间戳: {frame.timestamp_ns}")
# frame.image 是 numpy array (H, W, C) 或 (H*3//2, W) 对于 NV12
# 可以直接用于 OpenCV 或其他图像处理库
cv2.imshow("Camera", frame.image)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cv2.destroyAllWindows()
不跳过帧
# 获取每一帧(不跳过)
for frame in media.subscribe("main", skip_frames=False):
process_frame(frame.image)
获取单帧
# 获取单帧
frame = media.get_frame("main", timeout_ms=1000)
if frame:
print(f"帧: {frame.width}x{frame.height}")
print(f"格式: {frame.format}")
获取流信息
# FdMediaClient 没有独立的流信息接口,从首帧读取尺寸与格式
frame = media.get_frame("main", timeout_ms=5000)
if frame:
print(f"分辨率: {frame.width}x{frame.height}")
print(f"格式: {frame.format}")
print(f"可用流: {media.list_streams()}")
列出可用流
# 列出所有可用的流
streams = media.list_streams()
for stream_id in streams:
print(f"流: {stream_id}")
帧处理回调
def handle_frame(frame):
print(f"收到帧: {frame.sequence}")
# 使用回调处理帧
thread = media.on_frame("main", handle_frame)
# 保持运行
import time
while True:
time.sleep(1)
图像处理
import cv2
media = FdMediaClient()
for frame in media.subscribe("main"):
# 转换为 RGB
rgb = frame.to_rgb()
# 灰度图
gray = cv2.cvtColor(rgb, cv2.COLOR_RGB2GRAY)
# 边缘检测
edges = cv2.Canny(gray, 100, 200)
# 显示结果
cv2.imshow("Original", rgb)
cv2.imshow("Edges", edges)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
保存图像
media = FdMediaClient()
for frame in media.subscribe("main"):
# 保存为图片
frame.save("frame.jpg")
break
获取编码流
get_encoded_stream() 返回 EncodedStreamClient`(不是迭代器),产出 H.264/H.265 Annex-B 码流包,配合 ``recording` 模块可直接落盘:
from neoruntime_ipc_sdk import FdMediaClient
media = FdMediaClient()
# get_encoded_stream() 返回 EncodedStreamClient,不是迭代器
client = media.get_encoded_stream("main")
for packet in client.subscribe():
print(f"{packet.codec_name()} 包: {len(packet.data)} 字节")
if packet.is_keyframe():
print(" 关键帧")
多流处理
import threading
def process_main_stream():
media = FdMediaClient()
for frame in media.subscribe("main"):
# 处理主码流(高分辨率)
process_high_res(frame.image)
def process_sub_stream():
media = FdMediaClient()
for frame in media.subscribe("sub"):
# 处理子码流(低分辨率)
process_low_res(frame.image)
# 并行处理多个流
t1 = threading.Thread(target=process_main_stream)
t2 = threading.Thread(target=process_sub_stream)
t1.start()
t2.start()
t1.join()
t2.join()
帧率控制
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(frame.image)
last_time = current_time
保存视频
如需保存 H.264/H.265 编码流,更推荐直接复用 recording 模块的 TsWriter / HlsWriter,无需解码重编码。下面的示例演示解码后用 OpenCV 保存原始帧:
import cv2
media = FdMediaClient()
# 从首帧获取分辨率(FdMediaClient 没有独立的流信息接口)
first = media.get_frame("main", timeout_ms=5000)
if first is None:
raise RuntimeError("未收到视频帧")
# 创建视频写入器(fps 需按实际码流配置填写,这里以 30 为例)
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(
'output.mp4',
fourcc,
30.0,
(first.width, first.height)
)
frame_count = 0
max_frames = 300 # 录制 10 秒 (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"已保存 {frame_count} 帧到 output.mp4")
上下文管理器
# 使用上下文管理器自动管理资源
with FdMediaClient() as media:
for frame in media.subscribe("main"):
process_frame(frame.image)
零拷贝访问
# 默认接收路径在收到帧时拷贝像素数据(frame.image 立即可用)
# keep_fd=True 保留 dma-buf fd,实现真正的零拷贝:
# 缓冲区延迟到 frame.release() / GC / 客户端关闭时才归还 daemon
media = FdMediaClient()
for frame in media.subscribe("main", keep_fd=True):
# frame.handle 持有 dma-buf fd
# frame.to_array() 首次调用时映射一次并缓存像素数据
result = inference_engine.process(frame.to_array())
# 处理完尽早归还缓冲区(幂等,可省略)
frame.release()
错误处理
media = FdMediaClient()
try:
for frame in media.subscribe("invalid_stream"):
process_frame(frame.image)
except Exception as e:
print(f"流访问失败: {e}")
except KeyboardInterrupt:
print("用户中断")
finally:
media.close()