Introduction

Task

最近在玩宇树机器狗时,需要解决接入局域网内的设备都能够访问到机器狗的相机画面,机器狗扩展坞(外部加装)使用的是jetson orin NX作为开发板,机器狗自带了两个相机(机器狗前方的摄像头和扩展坞上的intel D435i RGB相机)。其他的结构配置如下图。dog_system

Camera Enable && Code

搭载的jetson Orin NX预装了ubuntu20.04版本,想要实现视频推流需要装ffmpeg包

1
2
sudo apt-get upgrade
sudo apt-get install ffmpeg

Camera01

其中狗自带的摄像头与高性能CPU相连,底层CPU模块通过交换机与orin NX通信,指定网口为eth0,相机分辨率为1280*720,帧率为15 FPS,下面的代码为将摄像头数据推送到本地或局域网内指定ip(示例IP为:192.168.0.199)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
//RTMP方式推流,使用前置摄像头
#include <iostream>
#include <csignal>
#include <opencv4/opencv2/opencv.hpp>
#include <sstream>
#include <thread>
#include <chrono>

bool is_running = true;

void OnSignal(int) {
is_running = false;
}

// 检查RTMP服务器连接状态
bool checkRTMPServer(const std::string& server_ip, int port) {
std::stringstream test_cmd;
test_cmd << "timeout 5 nc -z " << server_ip << " " << port << " > /dev/null 2>&1";
int result = system(test_cmd.str().c_str());
return result == 0;
}

int main() {
// 触发下面的信号就退出
signal(SIGINT, OnSignal);
signal(SIGQUIT, OnSignal);
signal(SIGTERM, OnSignal);

// 创建 GStreamer 视频流
cv::VideoCapture cap("udpsrc address=230.1.1.1 port=1720 multicast-iface=eth0 ! application/x-rtp, media=video, encoding-name=H264 ! rtph264depay ! h264parse ! avdec_h264 ! videoconvert ! video/x-raw,width=1280,height=720,format=BGR ! appsink drop=1", cv::CAP_GSTREAMER);

if (!cap.isOpened()) {
std::cerr << "Failed to open camera." << std::endl;
return EXIT_FAILURE;
}

// 获取视频分辨率
cap.set(cv::CAP_PROP_FRAME_WIDTH, 1280);
cap.set(cv::CAP_PROP_FRAME_HEIGHT, 720);

// RTMP服务器配置
std::string rtmp_server_ip = "192.168.0.199";
int rtmp_port = 1935;
std::string rtmp_output_url = "rtmp://" + rtmp_server_ip + ":" + std::to_string(rtmp_port) + "/live/livestream01";

// 检查RTMP服务器连接
std::cout << "检查RTMP服务器连接状态..." << std::endl;
if (!checkRTMPServer(rtmp_server_ip, rtmp_port)) {
std::cerr << "错误:无法连接到RTMP服务器 " << rtmp_server_ip << ":" << rtmp_port << std::endl;
std::cerr << "请检查:" << std::endl;
std::cerr << "1. RTMP服务器是否正在运行" << std::endl;
std::cerr << "2. 防火墙是否阻止了连接" << std::endl;
std::cerr << "3. 网络连接是否正常" << std::endl;
std::cerr << "4. 服务器地址和端口是否正确" << std::endl;
cap.release();
return EXIT_FAILURE;
}
std::cout << "RTMP服务器连接正常" << std::endl;

std::stringstream command;
command << "ffmpeg ";

// infile options
command << "-y " // overwrite output files
<< "-loglevel error " // 设置日志级别为错误,减少输出
<< "-an " // disable audio
<< "-f rawvideo " // force format to rawvideo
<< "-vcodec rawvideo " // force video rawvideo ('copy' to copy stream)
<< "-pix_fmt bgr24 " // set pixel format to bgr24
<< "-s 1280x720 " // set frame size (WxH or abbreviation)
<< "-r 15 " // set frame rate (Hz value, fraction or abbreviation)
<< "-re "; // read input at its native frame rate

command << "-i - "; //

// outfile options - 使用RTMP推流
command << "-c:v h264 " // Hyper fast Audio and Video encoder
<< "-pix_fmt yuv420p " // set pixel format to yuv420p
<< "-preset ultrafast " // set the libx264 encoding preset to ultrafast
<< "-tune zerolatency " // tune for zero latency
<< "-maxrate 50M " // 设置最大比特率
<< "-bufsize 1k " // 设置缓冲区大小
<< "-max_delay 100 " // 设置缓冲区大小
<< "-g 3 " // 设置关键帧间隔
<< "-f flv " // 使用 FLV 格式适合 RTMP 传输
<< "-rtmp_live 1 " // 启用RTMP实时模式
<< "-rtmp_conn \"S:publish\" " // 设置RTMP连接参数
<< rtmp_output_url;

cv::Mat frame;
FILE *fp = nullptr;
int retry_count = 0;
const int max_retries = 3;

std::cout << "尝试连接到RTMP服务器: " << rtmp_output_url << std::endl;

// 重试机制
while (retry_count < max_retries && is_running) {
// 再次检查服务器连接状态
if (!checkRTMPServer(rtmp_server_ip, rtmp_port)) {
std::cerr << "RTMP服务器连接丢失,正在重试..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
retry_count++;
continue;
}

// 在子进程中调用 ffmpeg 进行推流
fp = popen(command.str().c_str(), "w");

if (fp != nullptr) {
std::cout << "成功连接到RTMP服务器,开始推流..." << std::endl;
break;
} else {
retry_count++;
std::cerr << "连接失败,第 " << retry_count << " 次重试..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
}
}

// 将 cv 读到的每一帧传入子进程
if (fp != nullptr) {
int consecutive_errors = 0;
const int max_consecutive_errors = 10;

while (is_running && consecutive_errors < max_consecutive_errors) {
cap >> frame;
if (frame.empty()) {
std::cerr << "获取到空帧,跳过..." << std::endl;
consecutive_errors++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
continue;
}

// 写入数据到ffmpeg进程
size_t expected_size = frame.total() * frame.elemSize();
size_t written = fwrite(frame.data, sizeof(char), expected_size, fp);

if (written != expected_size) {
consecutive_errors++;
std::cerr << "写入数据失败 (" << consecutive_errors << "/" << max_consecutive_errors << "),可能连接中断" << std::endl;

// 检查管道是否仍然有效
if (ferror(fp)) {
std::cerr << "管道错误,推流连接已中断" << std::endl;
break;
}
} else {
consecutive_errors = 0; // 重置错误计数器
}

// 添加小延迟以避免过载
std::this_thread::sleep_for(std::chrono::milliseconds(33)); // ~30fps
}

pclose(fp);
cap.release();

if (consecutive_errors >= max_consecutive_errors) {
std::cout << "推流因连续错误而结束" << std::endl;
} else {
std::cout << "推流正常结束" << std::endl;
}
return EXIT_SUCCESS;
} else {
std::cerr << "无法连接到RTMP服务器,请检查:" << std::endl;
std::cerr << "1. RTMP服务器是否正在运行 (如: nginx-rtmp, SRS等)" << std::endl;
std::cerr << "2. 服务器配置是否允许发布流" << std::endl;
std::cerr << "3. 防火墙设置是否阻止连接" << std::endl;
std::cerr << "4. FFmpeg是否正确安装并支持RTMP" << std::endl;
std::cerr << "5. 网络延迟或带宽是否足够" << std::endl;
cap.release();
return EXIT_FAILURE;
}
}

Camera02

第二个相机是intel D435i,该相机一共有四个设备,包括两个红外相机、一个红外发射器和一个RGB相机。我们主要使用RGB相机,最高支持1920*1080分辨率,帧率支持15 FPS、30 FPS、60 FPS。查看相机设备时发现有六个(不清楚原因):

/dev/video0、/dev/video1、/dev/video2、/dev/video3、/dev/video4、/dev/video5

RGB相机是/dev/video4,可以使用下面命令直接检查相机画面:

1
ffplay /dev/video4

code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
//RTMP方式推流  使用intel D435i
#include <iostream>
#include <csignal>
#include <opencv4/opencv2/opencv.hpp>
#include <sstream>
#include <thread>
#include <chrono>

bool is_running = true;

void OnSignal(int) {
is_running = false;
}

// 检查RTMP服务器连接状态
bool checkRTMPServer(const std::string& server_ip, int port) {
std::stringstream test_cmd;
test_cmd << "timeout 5 nc -z " << server_ip << " " << port << " > /dev/null 2>&1";
int result = system(test_cmd.str().c_str());
return result == 0;
}

int main() {
// 触发下面的信号就退出
signal(SIGINT, OnSignal);
signal(SIGQUIT, OnSignal);
signal(SIGTERM, OnSignal);

// 创建摄像头对象,使用指定的video4设备
cv::VideoCapture cap(4);

if (!cap.isOpened()) {
std::cerr << "Failed to open camera device video4." << std::endl;
std::cerr << "无法打开video4设备" << std::endl;
return EXIT_FAILURE;
} else {
std::cout << "成功打开video4摄像头设备" << std::endl;
}

// 设置视频分辨率为1920x1080
cap.set(cv::CAP_PROP_FRAME_WIDTH, 1920);
cap.set(cv::CAP_PROP_FRAME_HEIGHT, 1080);
cap.set(cv::CAP_PROP_FPS, 30); // 设置帧率

// 获取实际设置的分辨率
int actual_width = cap.get(cv::CAP_PROP_FRAME_WIDTH);
int actual_height = cap.get(cv::CAP_PROP_FRAME_HEIGHT);
double actual_fps = cap.get(cv::CAP_PROP_FPS);

std::cout << "摄像头分辨率: " << actual_width << "x" << actual_height << std::endl;
std::cout << "摄像头帧率: " << actual_fps << " fps" << std::endl;

// RTMP服务器配置
std::string rtmp_server_ip = "192.168.0.199";
int rtmp_port = 1935;
std::string rtmp_output_url = "rtmp://" + rtmp_server_ip + ":" + std::to_string(rtmp_port) + "/live/livestream02";

// 检查RTMP服务器连接
std::cout << "检查RTMP服务器连接状态..." << std::endl;
if (!checkRTMPServer(rtmp_server_ip, rtmp_port)) {
std::cerr << "错误:无法连接到RTMP服务器 " << rtmp_server_ip << ":" << rtmp_port << std::endl;
std::cerr << "请检查:" << std::endl;
std::cerr << "1. RTMP服务器是否正在运行" << std::endl;
std::cerr << "2. 防火墙是否阻止了连接" << std::endl;
std::cerr << "3. 网络连接是否正常" << std::endl;
std::cerr << "4. 服务器地址和端口是否正确" << std::endl;
cap.release();
return EXIT_FAILURE;
}
std::cout << "RTMP服务器连接正常" << std::endl;

std::stringstream command;
command << "ffmpeg ";

// infile options
command << "-y " // overwrite output files
<< "-loglevel error " // 设置日志级别为错误,减少输出
<< "-an " // disable audio
<< "-f rawvideo " // force format to rawvideo
<< "-vcodec rawvideo " // force video rawvideo ('copy' to copy stream)
<< "-pix_fmt bgr24 " // 输入像素格式为bgr24,匹配OpenCV的默认格式
<< "-s " << actual_width << "x" << actual_height << " " // 使用实际分辨率
<< "-r 30 "; // set frame rate (Hz value, fraction or abbreviation)

command << "-i - "; //

// outfile options - 针对1920x1080分辨率的RTMP推流优化参数
command << "-c:v libx264 " // 使用libx264编码器
<< "-pix_fmt yuv420p " // 输出像素格式为yuv420p
<< "-preset fast " // 设置编码预设为fast,平衡速度和质量
<< "-tune zerolatency " // 调优为零延迟
<< "-b:v 6000k " // 设置视频比特率为6Mbps,适合1080p推流
<< "-maxrate 8000k " // 设置最大比特率为8Mbps
<< "-bufsize 2000k " // 设置缓冲区大小为2MB
<< "-g 60 " // 设置GOP大小为60帧(2秒的关键帧间隔)
<< "-keyint_min 30 " // 最小关键帧间隔
<< "-sc_threshold 0 " // 禁用场景切换检测
<< "-profile:v high " // 使用H.264 High Profile
<< "-level 4.1 " // 设置H.264 level 4.1
<< "-f flv " // 使用 FLV 格式适合 RTMP 传输
<< "-rtmp_live 1 " // 启用RTMP实时模式
<< rtmp_output_url;

cv::Mat frame;
FILE *fp = nullptr;
int retry_count = 0;
const int max_retries = 3;

std::cout << "尝试连接到RTMP服务器: " << rtmp_output_url << std::endl;

// 重试机制
while (retry_count < max_retries && is_running) {
// 再次检查服务器连接状态
if (!checkRTMPServer(rtmp_server_ip, rtmp_port)) {
std::cerr << "RTMP服务器连接丢失,正在重试..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
retry_count++;
continue;
}

// 在子进程中调用 ffmpeg 进行推流
fp = popen(command.str().c_str(), "w");

if (fp != nullptr) {
std::cout << "成功连接到RTMP服务器,开始推流..." << std::endl;
break;
} else {
retry_count++;
std::cerr << "连接失败,第 " << retry_count << " 次重试..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3));
}
}

// 将 cv 读到的每一帧传入子进程
if (fp != nullptr) {
int consecutive_errors = 0;
const int max_consecutive_errors = 10;

while (is_running && consecutive_errors < max_consecutive_errors) {
cap >> frame;
if (frame.empty()) {
std::cerr << "获取到空帧,跳过..." << std::endl;
consecutive_errors++;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
continue;
}

// 写入数据到ffmpeg进程
size_t expected_size = frame.total() * frame.elemSize();
size_t written = fwrite(frame.data, sizeof(char), expected_size, fp);

if (written != expected_size) {
consecutive_errors++;
std::cerr << "写入数据失败 (" << consecutive_errors << "/" << max_consecutive_errors << "),可能连接中断" << std::endl;

// 检查管道是否仍然有效
if (ferror(fp)) {
std::cerr << "管道错误,推流连接已中断" << std::endl;
break;
}
} else {
consecutive_errors = 0; // 重置错误计数器
}

// 添加小延迟以避免过载
std::this_thread::sleep_for(std::chrono::milliseconds(33)); // ~30fps
}

pclose(fp);
cap.release();

if (consecutive_errors >= max_consecutive_errors) {
std::cout << "推流因连续错误而结束" << std::endl;
} else {
std::cout << "推流正常结束" << std::endl;
}
return EXIT_SUCCESS;
} else {
std::cerr << "无法连接到RTMP服务器,请检查:" << std::endl;
std::cerr << "1. RTMP服务器是否正在运行 (如: nginx-rtmp, SRS等)" << std::endl;
std::cerr << "2. 服务器配置是否允许发布流" << std::endl;
std::cerr << "3. 防火墙设置是否阻止连接" << std::endl;
std::cerr << "4. FFmpeg是否正确安装并支持RTMP" << std::endl;
std::cerr << "5. 网络延迟或带宽是否足够" << std::endl;
cap.release();
return EXIT_FAILURE;
}
}

SRS服务搭建与推流

SRS简介

一款开源的实时视频流媒体服务器,主要用于实时音视频流的推送、转发、直播、点播等应用场景。它支持RTMP、RTSP、HLS、HTTP-FLV、WebRTC等多种流媒体协议。Windows端安装链接:https://github.com/ossrs/srs,找到release下载最新版。

使用

安装完成后使用管理员权限打开终端,

启动SRS:

1
D:\software\SRS>.\objs\srs.exe -c .\conf\console.conf

也可以直接通过绝对路径执行,但不要进入到objs目录再使用.\srs.exe -c ..\conf\console.conf跑,会导致Not found。

浏览器打开http://192.168.0.199:8080/可以查看服务是否跑起来了,界面如下所示:

srs

在srs服务跑起来后执行Camera01和Camera02中的代码,注意Camera01中的推流链接为rtmp://192.168.0.199:1935/live/livestream01,Camera02中的推流链接为rtmp://192.168.0.199:1935/live/livestream02(这两个链接可以在代码中设置)。

其中的localhost换成跑srs设备的IP。

video