本项目为解决Unity与树莓派的远程通信以及低时延视频流传输提供了一种基于UDP协议的解决方案
-
Unity端可以向树莓派发送控制信息
-
Unity端可以接收树莓派端发送的实时视频流
最近在做一个远程小车的项目, 控制端采用Unity制作
这个项目可以先为此打下技术基础, 并在此基础上进行后续开发
-
物联网远程控制与实时监测
-
远程视频监控
-
遥操作设备
......
简单来说就是利用UDP协议无连接, 低时延的特点将数据进行传输
然后为了实现远程控制, 需要让Unity和树莓派都作为frp客户端, 同时在阿里云ECS服务器上部署frp服务器端
Unity端发送的UDP数据会先发送给frp服务器, 然后通过服务器再转发给部署有frpc的树莓派端
树莓派端的视频流数据发送给Unity也是同理
- 硬件
- 从学长那白嫖的树莓派4b
- 随便拿的一根网线 (用于连接树莓派和电脑)
- 拼夕夕上买的树莓派摄像头 (应该不是官方的)
- 软件
- Unity版本: 2022.3.62f2c1
- 树莓派系统: Debian GNU/Linux 12 (bookworm)
- 使用的相关Python库: picamera2
- FRP内网穿透小工具: 0.65.0版本
- 其它
- 三个月试用的阿里云ECS服务器
- 聪明的AI助手
- FRP服务器端的配置
-
首先下载FRP, 不过我已经放在Unity工程的文件夹下了
-
配置frps.toml
bindPort = XXXX # 服务器与客户端的通信端口, 默认为7000 auth.token = "secret_token" # 你的神奇小密码, 客户端需要一致 # Dashboard配置 (可选, 如果需要的话) # 日志配置 (可选, 如果需要的话) -
frps, 启动!
注意: 安全组需要开放上述使用的端口(frp 的服务器端与客户端之间的初始连接使用 TCP 协议)
配置frps为系统服务 (开机自启)
-
创建服务文件
nano /etc/systemd/system/frps.service -
添加内容:
[Unit] Description=Frp Server Service After=network.target [Service] Type=simple Restart=on-failure RestartSec=5s ExecStart=/usr/local/frp/frps -c /usr/local/frp/frps.toml LimitNOFILE=1048576 [Install] WantedBy=multi-user.target -
加载并启动服务:
systemctl daemon-reload systemctl enable frps systemctl start frps -
检查状态: 输入
systemctl status frps应显示active (running)
-
- 树莓派FRP客户端的配置
-
依旧要先下载FRP, 注意必须要和服务器端版本相同, 都是0.65.0
-
配置frpc.toml
serverAddr = "你的ECS公网ip" serverPort = XXXX # 和服务器上的bindPort一致 auth.token = "secret_token" # 你的神奇小密码, 和frps.toml上的一致 [[proxies]] name = "随便什么名字都行" # 我取的是test_udp type = "udp" localIP = "127.0.0.1" localPort = XXXXX # 本地端口, 需要跟之后的python代码中监听的端口一致 remotePort = XXXXX # 云端暴露的公网端口(需要ECS安全组开放) -
frpc, 启动!
配置frpc为系统服务 (开机自启)
-
创建服务文件
nano /etc/systemd/system/frpc.service -
添加内容:
[Unit] Description=Frp Client Service After=network.target Wants=network.target [Service] Type=simple Restart=on-failure RestartSec=5s ExecStart=/usr/local/frp/frpc -c /usr/local/frp/frpc.toml [Install] WantedBy=multi-user.target -
加载并启动服务:
systemctl daemon-reload systemctl enable frpc systemctl start frpc -
检查状态: 输入
systemctl status frpc应显示active (running)
-
-
不用那么麻烦的办法
为了让Unity端能够接收到树莓派发送过来的视频流数据, 因此也需要在运行着Unity的设备上部署frp客户端 但是我已经写了一个批处理文件
frpc_setup.bat在windows上部署frpc了, 不过有些地方需要注意:
- 安装frp的版本和安装位置, 安装在Unity可执行文件的同目录下
set "FRP_VERSION=0.65.0" set "FRP_DIR=%~dp0frp_%FRP_VERSION%"
- frpc.toml配置文件信息的写入
echo serverAddr = "你的公网ip" >> "%INI_FILE%" echo serverPort = XXXX >> "%INI_FILE%" # 和服务器上的bindPort一致 echo auth.token = "神奇小密码" >> "%INI_FILE%" echo. >> "%INI_FILE%" echo [[proxies]] >> "%INI_FILE%" echo name = "udp-receive" >> "%INI_FILE%" echo type = "udp" >> "%INI_FILE%" echo localIP = "127.0.0.1" >> "%INI_FILE%" echo localPort = XXXXX >> "%INI_FILE%" # 本地端口, 需要跟之后的C#代码中监听的端口一致 echo remotePort = XXXXX >> "%INI_FILE%" # 云端暴露的公网端口(需要ECS安全组开放)
- 双击
frpc_setup.bat即可在电脑上自动配置frpc了
-
Unity端的C#代码
- 在
UDPCtrl.cs脚本中需要配置frp服务器的IP和本地消息发送的端口// ...existing code... [Header("FRP服务器IP")] public string serverIP; // 修改为服务器公网ip [Header("UDP消息发送")] public int MSG_SEND_PORT; // unity向frp服务器发送消息的端口, 和树莓派frpc.toml中的remotePort一致 private UdpClient udpSendClient; // ...existing code...
- 在
ReceiveCamData.cs脚本中需要配置Unity监听的端口// ...existing code... [Header("视频流接收")] public int MSG_RECEIVE_PORT; // Unity监听frp服务器端口, 和本机frpc.toml中的localPort一致 public RawImage display; private Texture2D texture; // ...existing code...
- 以上属性均为
public, 所以要在Unity Editor的inspector中修改
- 在
-
树莓派端的Python代码
- 在
UDP_Test.py中需要配置树莓派端的监听端口, 我这里默认12345# ...existing code... ser = serial.Serial('/dev/tty50', baudrate=115200, timeout=0.01) UDP_IP = '0.0.0.0' UDP_PORT = 12345 # 树莓派端监听frp服务器端口, 和树莓派frpc.toml中的localPort一致 # ...existing code...
- 在
video_stream.py中配置frp服务器的IP和本地消息发送的端口# ...existing code... UDP_IP = "服务器公网ip" UDP_PORT = 13300 # 树莓派端向frp服务器发送消息的端口, 和Unity端frpc.toml中的remotePort一致 # ...existing code...
- 在
- 树莓派端发送视频流
- 将一帧视频数据分成两部分, 帧头和视频帧数据
图4: 视频帧
其中帧头包括帧头标识(自定义)和预测帧长, 各占4字节
- 将一整个帧数据分成小的帧分片, 每个分片1024字节
- Unity端接收视频流
- 在
ReceiveUDPdata子线程中先将接收到的数据放入dataQueue中, 等待主线程处理 - 主线程处理
dataQueue中的数据, 分为以下步骤
-
帧头检测
为实现Unity接收和树莓派发送的帧同步, 需要根据帧头标识判断当前接收到的是否为帧头.
如果接收到的不是帧头, 而是1024字节的帧分片的话, 则丢弃这个数据, 直到接收到的是8字节的帧头.
-
接收帧分片并拼接帧数据
当接收到帧头后, 记录预测帧长. 开始接收当前帧数据, 将接收到的帧分片数据拼接并放入帧处理缓存中.
-
渲染当前帧
当缓存中的帧长达到预测帧长时候, 则表示这一帧接收完成, 开始渲染当前帧.
-
遇到的小问题
最初进行视频流传输测试的时候, 发现视频画面中的红色和蓝色反了, 并且整个画面是上下左右都颠倒的
于是我怀疑是树莓派端发送的字节流和Unity端接收的字节流顺序是反的. 也就是说本来是第一个像素点的RGB数据在Unity端解码后变成了最后一个像素点的BGR数据了. 为了解决这个问题, 我修改了树莓派上的
video_stream.py的代码:# ...existing code... array = picam.capture_array() frame_data = array.tobytes()[::-1] # 将帧字节数组取反再发送 # ...existing code...
同时我在Unity Editor中用于显示视频流的RawImage组件的x轴缩放修改为-1
这样一来就解决了颜色反相和画面颠倒的问题了.
- 清晰的画质 受限于树莓派和算法的处理能力, 目前只能实现128*64分辨率的实时视频流传输
- 更高的帧率 目前视频流的传输能够稳定在30帧
- 更好的编码 当前并未对视频流做任何的编码处理, 传输的是原始视频
- 更低的时延 在校园网情况下, 当前的传输时延大约在80ms到150ms之间. 这基本能满足远程小车的实时控制, 但是对于更高要求的使用场景来说需要进一步优化




