CanMV 上推 RTSP 已经比较熟悉:摄像头采集、编码,再由 RtspServer 发出去。很多场景反过来——板子要去看别人的码流:NVR、另一块 K230、电脑上的 mediamtx,或者已经在跑的监控地址。
这次 CanMV 补上了拉流这一端。固件类型 multimedia.RtspClient 从 rtsp:// 地址接收 H.264 或 H.265,在固件里解码,再把画面交给现有的 Display。用 from multimedia import RtspClient 调用。接法和 Sensor 相同:bind_info() 给出绑定参数,由调用方自己 Display.bind_layer。
播放留在媒体链路上,检测需要的 RGB 另取。纯看画面时,不必在脚本里一帧帧搬运像素。



一
这次点亮了什么
能力 | 说明 |
拉流 | rtsp://host[:port]/path,传输为 TCP 交织 |
编码 | 由 SDP 决定,接受 H.264 和 H.265 |
解码 | 固件内 VDEC 解码,输出 NV12 给视频层 |
显示 | bind_info() + Display.bind_layer,接法和 Sensor 相同 |
给 AI 取图 | get_frame() 返回 RGB 平面,形状为 (3, out_h, out_w) |
路数 | 同时最多 4 个客户端,与 VDEC 通道数一致 |
认证 | 地址里写账号,或传入 username、password;401 时按 Digest 或 Basic 重发 |
类在 CanMV 固件的 multimedia 模块里。需要烧录包含该模块的固件后,才能 import。
固件自带两份示例:
只显示:
/sdcard/examples/02-Media/rtsp_client_display.py
拉流加人脸检测:
/sdcard/examples/21-AI-With-Others/rtsp_client_face_detect.py


二
播放为什么更顺
RtspClient 实现在固件里,脚本侧只负责建连、绑定显示和取图。几处设计和「脚本里自己收包、自己转图」不一样,播放时 Python 侧更轻:
解码在固件里完成。 构造函数连上服务器并完成 DESCRIBE,记下码流宽高;start() 之后才开始收流和解码。
显示走视频层 NV12。 视频层只接受 NV12。检测框画在一块 ARGB 的 OSD 上,用 Display.show_image 叠到视频上面。
缩小由 VDEC 完成。 构造时的 width、height 是解码器的唯一输出,get_frame() 和送给显示的 NV12 都是这个尺寸。与码流尺寸相同时不做缩放。
RGB 按需转换。 只有有人等待 get_frame() 时才做 RGB 转换。不调用 get_frame() 时,已绑定的显示仍然出画面。
收流和解码之间有缓存。 默认缓存 60 个压缩访问单元(范围 1 到 120),压缩数据总量超过 4 MB 时也视为已满。队列满时丢掉后续帧,直到下一个关键帧。解码输出默认 6 块 YUV 缓冲(范围 3 到 8)。
播放过程中连接被拆掉时,客户端保留解码器和显示绑定,由接收线程重连。间隔从 1 秒起每次加倍,最长 5 秒,最多 3 次。一次 PLAY 成功后,次数和间隔都恢复。


三
先联网,再出画
拉流前必须先连上 LAN、Wi-Fi STA 或 Wi-Fi AP。推荐使用 libs.Network。客户端不负责联网。
只显示、不取帧时,顺序和摄像头显示示例一样:先 bind_layer,再 Display.init,再 start()。循环里周期性调用 check(),重连放弃后退出。
import timefrom media.display import Displayfrom multimedia import RtspClientfrom libs.Network import connect_network_, ip = connect_network("lan")client = Nonestarted = Falsedisplay_inited = Falsetry:client = RtspClient("rtsp://192.168.1.108:8554/test",cache_count=60,buffer_count=6,width=800,height=480)info = client.bind_info(x=0, y=0, width=800, height=480)Display.bind_layer(**info, layer=Display.LAYER_VIDEO1)Display.init(Display.ST7701, width=800, height=480, to_ide=False, osd_num=1)display_inited = Trueclient.start()started = Truewhile True:time.sleep(1)client.check()except OSError as exc:print("rtsp failed:", exc)finally:if started:client.stop()if display_inited:Display.disable_layer(Display.LAYER_VIDEO1)if client is not None:client.deinit()if display_inited:Display.deinit()
上面的 800 x 480 是缩小后的解码输出,也是送进视频层的窗口大小。窗口必须和解码输出一样大,尺寸不同时这一帧不会送去显示。width、height 同时为 0 时,按码流原尺寸输出。
需要认证时,两种写法都可以:
client = RtspClient("rtsp://user:password@192.168.1.108:8554/test")client = RtspClient("rtsp://192.168.1.108:8554/test", username="user", password="password")


四
检测另取一帧,显示继续走视频层
客户端不打开屏幕,也不做人脸检测,也不会把图缩成模型输入。检测需要的 RGB 用 get_frame() 另取,letterbox 由 AI 预处理完成。
frame = client.get_frame(timeout_ms=100)if frame is not None:data = frame.to_numpy_ref() # (3, out_h, out_w)# 在这里读 dataframe = None
get_frame() 超时返回 None。返回的图像借用解码器唯一的一块 RGB 缓冲,下一次会等待的 get_frame(),以及 stop()、deinit(),都会释放这块缓冲。在循环里用完就丢掉,不要留到下一次 get_frame()。
框画到与窗口同样大小的 ARGB 图像上,用 Display.show_image(..., layer=Display.LAYER_OSD0) 叠上去。完整流程见 rtsp_client_face_detect.py。
只绑定显示、循环里不调用 get_frame() 的程序,应周期性调用 check()。重连还在进行时它直接返回;重连已放弃时抛出 OSError("rtsp connection closed")。


五
最多四路,屏幕上能绑三层
同时最多 4 个还没有 deinit() 的客户端,第 5 个构造抛出 OSError。某个客户端 deinit() 之后,名额空出来,可以再创建一个。不绑定显示同样占一个名额,这时只解码,用 get_frame() 取图。
要上屏时,可用的视频层是 Display.LAYER_VIDEO1、LAYER_VIDEO2、LAYER_VIDEO3。层不够的那几路继续 get_frame()。每个窗口的 x + width、y + height 必须落在屏幕内。
多路 1080p 占用很大。显示时把 width、height 设成窗口大小,由 VDEC 缩小。例如 800×480 屏上左右各半,第三路只取图做检测:
left = RtspClient("rtsp://192.168.1.108:8554/cam0", width=400, height=480)right = RtspClient("rtsp://192.168.1.108:8554/cam1", width=400, height=480)detect = RtspClient("rtsp://192.168.1.108:8554/cam2", width=320, height=320)left_info = left.bind_info(x=0, y=0, width=400, height=480)right_info = right.bind_info(x=400, y=0, width=400, height=480)Display.bind_layer(**left_info, layer=Display.LAYER_VIDEO1)Display.bind_layer(**right_info, layer=Display.LAYER_VIDEO2)
每路单独分配输入和输出缓冲。stop()、deinit() 按对象分别调用,绑定过的层再 Display.disable_layer。


六
上板前先对一下这几条
先确认固件里有这个模块。 from multimedia import RtspClient 失败时,换一份已包含该模块的 CanMV 固件。
输出尺寸只能缩小或保持原尺寸。 width 和 height 必须同时为 0,或同时为大于 0 的偶数。任一方向大于码流可见尺寸时,构造函数抛出 OSError。
显示窗口和解码输出保持一致。 x、y 为非负偶数。播放开始后要改位置或大小,先 stop(),再 Display.disable_layer,然后重新 bind_info。
账号不要写两遍。 地址里已经有 user:password@ 时,不要再传 username。只给 password、不给 username 会抛出 ValueError。
重连有次数。 3 次都失败后停止重连,之后 get_frame() 和 check() 抛出 OSError。初次 DESCRIBE 失败不会重连,构造函数直接抛出 OSError。PLAY 之前会话被关掉时,由接下来的 start() 重新 DESCRIBE。
当前处理视频。 音频数据会被丢掉。传输固定为 TCP 交织。
退出按这个顺序。 先 client.stop(),再 Display.disable_layer,然后 client.deinit(),最后 Display.deinit()。要换地址,在 stop() 之后调用 init(url, ...),不要先 deinit()。init()、start()、stop() 失败时抛出 OSError,成功时返回 0。正在播放时调用 init() 也会抛出 OSError。

写在最后
推流用 RtspServer,拉流用 RtspClient。一块 K230 / K230D 既可以把自己的画面发出去,也可以把网络上的 H.264 / H.265 码流解出来:直接上屏,或再交一帧给检测。
监控预览、多路拼接、拉流做人脸或目标检测,都可以从固件里的 rtsp_client_display.py 和 rtsp_client_face_detect.py 改起。
现在就烧录带 RtspClient 的固件,把一条 rtsp:// 地址点亮到屏幕上。
说明:实际观感仍取决于码流分辨率、码率、关键帧间隔、网络质量和同时打开的路数。多路 1080p 请把解码输出设成窗口大小。建议在目标板卡上确认延时和稳定性。
📚 参考
官方API文档:
https://www.kendryte.com/k230_canmv/zh/main/api/mpp/k230_canmv_rtsp_module_api_manual.html