若该文为原创文章,转载请注明原文出处。

一、背景与挑战

在嵌入式安防设备(报警主机、门禁控制器、智能显示终端等)中,通过 Web 浏览器无插件预览 IPC(网络摄像机)的 RTSP 实时视频流,是一个普遍而棘手的需求。

核心矛盾在于:RTSP 协议是安防监控领域的事实标准,几乎所有的 IP 摄像头都支持这一协议栈。但 RTSP 基于 UDP 的设计在公网传输时表现不佳,且浏览器原生不支持 RTSP 协议。传统方案如 VLC 浏览器插件、ActiveX 控件等,随着浏览器安全策略的持续升级(Chrome 取消 NPAPI 支持、Edge 禁用 ActiveX 等),已基本被淘汰。

WebRTC 作为 W3C 标准,提供了浏览器原生的实时音视频传输能力,但需要复杂的信令服务器和 NAT 穿透机制。如何在资源受限的嵌入式设备上,将 RTSP 流低成本地转换为浏览器可播放的格式,成为安防嵌入式开发中的经典难题。

go2rtc 的出现提供了一个相当优雅的解法:这个纯 Go 编写的流媒体网关,用一个进程支持了 20 多种流媒体协议的双向转换。其核心优势在于实现了协议无关的媒体处理——WebRTC 与 RTSP 之间的转换仅需处理 RTP 封装格式的差异,而无需重新编码媒体数据,极大降低了延迟和资源占用。

本文基于真实量产项目(SSD202D 芯片 + ARMv7 平台 + BOA Web 服务器),完整记录 go2rtc 从交叉编译到系统集成的全流程,涵盖 CGI + Socket 通信机制、IPC 列表管理、流启停控制、登录会话超时等完整功能模块。


二、整体架构

text

┌─────────────────────────────────────────────────────────────────────────┐
│                         浏览器 (Chrome/Firefox/Edge)                    │
│  ┌──────────────┐    ┌──────────────┐    ┌─────────────────────────┐  │
│  │  BOA 服务器   │    │  go2rtc      │    │  前端页面 (webrtc.html) │  │
│  │  (端口 80)    │    │  (端口 1984) │    │  + iframe 嵌入播放器   │  │
│  └──────┬───────┘    └──────┬───────┘    └───────────┬─────────────┘  │
│         │                   │                        │                 │
│         │  CGI 请求         │  WebRTC/FLV 流         │  fetch / XHR   │
└─────────┼───────────────────┼────────────────────────┼─────────────────┘
          │                   │                        │
          ▼                   ▼                        ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                        设备端 (嵌入式 Linux)                           │
│  ┌──────────────────────────────────────────────────────────────────┐  │
│  │              WebDeal 守护进程 (监听 7777 端口)                   │  │
│  │  - 接收 CGI 的 Socket 请求 (MSG_GET_IPC_LIST / MSG_START_GO2RTC /│  │
│  │    MSG_STOP_GO2RTC / MSG_LOGIN / MSG_LOGOUT)                    │  │
│  │  - 读取/写入配置 (LL_SETT 持久化存储)                            │  │
│  │  - 启动/停止 go2rtc 子进程 (含 3 秒启动保护)                     │  │
│  │  - 登录会话管理 (15 分钟超时自动登出)                            │  │
│  └──────────────────────────────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────────────────────────────┐  │
│  │           go2rtc (二进制, 监听 1984 端口)                        │  │
│  │  - 拉取 RTSP 流 (rtsp://user:pass@ip:port/path)                 │  │
│  │  - 提供 WebRTC / MSE / HLS / MJPEG 等多协议转换输出             │  │
│  │  - 自带 Web 播放器 (stream.html)                                │  │
│  └──────────────────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────┘

数据流说明:

  1. 用户访问 http://device-ip/webrtc.html,页面加载后通过 fetch('./get_ipc_list.cgi') 获取 IPC 列表。

  2. CGI 通过 Socket 连接 WebDeal(127.0.0.1:7777),发送 MSG_GET_IPC_LIST 消息。

  3. WebDeal 从 LL_SETT 读取 SETT_IPC01~10 配置,解析并返回 JSON 数组。

  4. 用户点击某个 IPC,页面调用 control_go2rtc.cgi?action=start&rtsp=<encoded_url>。

  5. CGI 解码 RTSP 地址,通过 Socket 发送 MSG_START_GO2RTC 给 WebDeal。

  6. WebDeal 执行 system() 启动 go2rtc 子进程,返回成功/失败。

  7. 页面加载 iframe,指向 go2rtc 的 stream.html,开始播放。

  8. 停止时调用 control_go2rtc.cgi?action=stop,WebDeal 执行 killall -9。


三、go2rtc 的集成方案

3.1 为什么选择 go2rtc?

在评估多种方案后,go2rtc 因以下特点被选中:

特性说明
轻量级ARMv7 二进制仅 ~10MB,内存占用 < 30MB,适合嵌入式设备
多协议输出支持 WebRTC(低延迟)、MSE(兼容性好)、HLS、MJPEG 等
零拷贝媒体处理采用内存映射技术,减少数据拷贝次数,降低 CPU 占用
UDP 优先传输局域网环境下可实现 50-100ms 端到端延迟
无需转码协议转换无需完整解码再编码,仅处理封装格式差异
自带 Web 播放器提供 stream.html 通用播放器,支持多协议自动选择

go2rtc 的设计哲学是:不让开发者在各种协议之间做选择题,而是提供一个统一的抽象层,让任意协议的数据可以平滑地流转到另一个协议。

3.2 go2rtc 的启动与停止

WebDeal 通过 system() 调用启动 go2rtc,命令格式如下:

c

snprintf(cmd, sizeof(cmd),
    "/customer/go2rtc_linux_arm -c '{\"streams\":{\"cam1\":\"%s\"}}' > /tmp/go2rtc.log 2>&1 &",
    rtsp_url);
int ret = system(cmd);

命令解析:

  • -c 参数传递 JSON 配置,定义流名称(cam1)和 RTSP 地址。

  • 输出重定向到 /tmp/go2rtc.log 便于调试和日志查看。

  • & 将进程放入后台运行。

  • 停止时使用 killall -9 go2rtc_linux_arm 强制终止。

启动保护机制:为防止前端频繁误操作导致 go2rtc 频繁重启,WebDeal 记录启动时间,3 秒内收到的停止请求将被忽略(日志显示 stop go2rtc ignored)。这是实际量产中积累的经验——用户快速切换摄像头时,若不加保护会导致 go2rtc 不断启停,影响体验。

3.3 go2rtc 的 Web 播放接口

go2rtc 自带的 stream.html 通用播放器支持通过 URL 参数控制播放行为:

text

http://<device-ip>:1984/stream.html?src=rtsp://...&mode=webrtc,mse,hls,mjpeg&autoplay=1&embed=1&hide=1

参数说明:

参数说明
srcRTSP 地址或已定义的流名称
mode按优先级尝试多种播放方式,webrtc 优先,降级到 mse/hls/mjpeg
autoplay=1自动播放
embed=1嵌入模式(隐藏部分界面元素)
hide=1尝试隐藏更多界面元素

前端通过 <iframe> 嵌入该 URL,实现零插件的浏览器播放。

3.4 go2rtc 的架构原理(深入)

go2rtc 的架构核心是一套基于 Go channel 的流数据抽象。代码组织主要分为以下几个层次:

text

github.com/AlexxIT/go2rtc/
├── internal/
│   ├── streams/      # 流管理层
│   ├── sources/      # 协议源(拉流)
│   ├── targets/      # 协议目标(推流)
│   └── core/         # 核心类型定义
├── pkg/              # 独立功能包
└── cmd/              # 命令行入口

协议转换的技术实现包括三个层面:

  1. 协议适配层:每个协议有独立实现模块。RTSP 处理模块负责解析 RTSP 信令和 RTP/RTCP 数据包,WebRTC 模块处理 ICE 协商、DTLS 加密和 SRTP 传输。

  2. 媒体处理流水线:包括编解码器协商(自动检测客户端支持的编解码器)、容器格式转换(MPEG-TS/RTP → WebRTC 可用格式)、时钟同步(PTS/DTS 时间戳对齐)。

  3. 网络传输优化:通过 UDP 传输、前向纠错和自适应码率控制,在复杂网络环境下保持稳定低延迟。

go2rtc 没有 CGO 依赖,所有构建均使用 CGO_ENABLED=0,因此交叉编译非常直接。


四、BOA 服务器与 CGI 配置

4.1 BOA 服务器配置

BOA 是一款轻量级嵌入式 Web 服务器,配置文件 boa.conf 的关键配置项:

conf

# 文档根目录(静态网页存放位置)
DocumentRoot /customer/web

# CGI 脚本映射路径(请求 /cgi-bin/ 时执行对应程序)
ScriptAlias /cgi-bin/ /customer/web/cgi-bin/

# 默认首页
DirectoryIndex index.html

# 服务器名称
ServerName device.local

4.2 CGI 工作机制

当请求命中 ScriptAlias 配置的路径时,BOA 触发 CGI 执行流程:

  1. BOA 通过 fork() 创建子进程。

  2. 子进程通过 execve() 执行 CGI 程序,与主服务进程隔离。

  3. BOA 在调用前预设环境变量,包含完整的请求上下文:

    • REQUEST_METHOD=GET 或 POST

    • QUERY_STRING(GET 参数)

    • CONTENT_TYPE、CONTENT_LENGTH 等

4.3 CGI 与 WebDeal 的 Socket 通信协议

CGI 不直接处理业务逻辑,而是通过 Socket 与 WebDeal 守护进程通信。消息结构体定义如下:

c

typedef struct _fifo_msg_t {
    unsigned int  msg_type;      // 消息类型:MSG_GET_IPC_LIST / MSG_START_GO2RTC / MSG_STOP_GO2RTC
    unsigned int  sub_type;      // 子消息类型
    unsigned int  ack_flag;      // 应答状态:ACK_SUCCESS / ACK_ERROR_*
    unsigned int  session_id;    // 会话 ID
    unsigned char reserved[4];   // 预留
    unsigned int  data_len;      // 数据长度
    unsigned char data_buf[1024]; // 负载数据
} fifo_msg_t;

CGI 通过 CreateSocketClient() 连接 WebDeal 的 7777 端口:

c

int CreateSocketClient(fifo_msg_t *p_send_msg_fifo, fifo_msg_t* p_result_fifo_msg)
{
    // 1. 创建 Socket
    sock_fp = socket(AF_INET, SOCK_STREAM, 0);
    // 2. 连接 127.0.0.1:7777
    connect(sock_fp, (void*)&pin, sizeof(pin));
    // 3. 发送请求消息
    send(sock_fp, &s_write_fifo_msg, sizeof(fifo_msg_t), 0);
    // 4. 接收应答消息
    recv(sock_fp, &s_read_fifo_msg, sizeof(fifo_msg_t), 0);
    // 5. 关闭连接
    close(sock_fp);
}

WebDeal 在独立线程中循环 accept() 处理连接,调用 web_cgi_dealMsg() 分发消息。这种设计的优势在于:

  • 解耦:CGI 程序极简,只做协议转换,业务逻辑集中在 WebDeal。

  • 高效:WebDeal 长期运行,无需为每个请求重新加载。

  • 安全:CGI 以低权限运行,WebDeal 负责敏感操作(如启动进程)。


五、前端页面设计

5.1 界面布局

webrtc.html 采用左右布局:

  • 左侧:IPC 列表,显示摄像头名称和 RTSP 地址(小字),点击后高亮并触发播放。

  • 右侧:播放器区域,包含 <iframe>、占位符、加载动画和状态栏。

5.2 状态反馈机制

为提升用户体验,实现了多层级状态反馈:

反馈位置状态表现
列表项Connecting状态点橙色脉动 + “Connecting...” 文字
列表项Playing状态点绿色 + “Playing” 文字
列表项Failed状态点红色 + “Failed” 文字
播放器加载中旋转圆圈动画 + “Connecting to stream...”
状态栏全局状态Idle / Connecting / Playing / Error 等,颜色区分

5.3 防重入与启动保护

javascript

var isProcessing = false;  // 防止同时多次点击
var minRunTime = 2000;     // 启动后 2 秒内保护

function playStream(rtspUrl, itemElement) {
    if (isProcessing) return;           // 防重入
    if (currentRtsp === rtspUrl) {      // 相同流仅高亮
        // 高亮处理
        return;
    }
    isProcessing = true;
    // ... 启动逻辑
}

function stopPlay(silent, force) {
    var elapsed = Date.now() - startTime;
    // 非强制停止且处于保护期内,忽略
    if (!force && elapsed < minRunTime && !silent) {
        // 仅清空画面,不发送停止请求
        return;
    }
    // ... 停止逻辑
}

5.4 页面卸载自动清理

监听 window.unload 事件,若当前有流播放则发送停止请求:

javascript

window.addEventListener('unload', function() {
    if (currentRtsp && !stopRequested) {
        fetch('./control_go2rtc.cgi?action=stop', {
            keepalive: true,  // 确保页面关闭时仍能发出
            method: 'GET'
        }).catch(function() {});
    }
});

keepalive: true 是浏览器特性,即使页面正在卸载,也会尽力发送请求,避免 go2rtc 进程残留。


六、IPC 配置存储格式

WebDeal 使用 LL_SETT(轻量级键值存储)保存 IPC 信息,键为 SETT_IPC01 ~ SETT_IPC10,每条记录格式为:

text

名称@#用户名@#密码@#IP地址@#端口@#码流路径

示例:

text

Camera1@#admin@#123456@#192.168.1.100@#554@#live/0/SUB

WebDeal 在 MSG_GET_IPC_LIST 中解析该格式:

c

while (field_index < 6) {
    end = strstr(start, delimiter);  // 查找 "@#"
    if (end != NULL) {
        *end = '\0';
    }
    switch (field_index) {
        case 0: snprintf(name, sizeof(name), "%s", start); break;
        case 1: snprintf(user, sizeof(user), "%s", start); break;
        case 2: snprintf(pass, sizeof(pass), "%s", start); break;
        case 3: snprintf(ip, sizeof(ip), "%s", start); break;
        case 4: snprintf(port, sizeof(port), "%s", start); break;
        case 5: snprintf(path, sizeof(path), "%s", start); break;
    }
    field_index++;
    if (end == NULL) break;
    start = end + delimiter_len;
}

前端据此构造完整的 RTSP URL:

javascript

var rtsp = 'rtsp://' + user + ':' + pass + '@' + ip + ':' + port + '/' + path;

七、登录会话管理(15 分钟超时)

为实现登录态管理,WebDeal 维护了全局状态:

c

static int g_login_status = 0;      // 0=未登录, 1=已登录
static time_t g_login_time = 0;     // 登录成功的时间戳

在 web_cgi_dealMsg() 开头添加拦截检查:

c

if (p_msg->msg_type != MSG_LOGIN && p_msg->msg_type != MSG_LOGOUT) {
    if (g_login_status == 0) {
        p_msg->ack_flag = ACK_ERROR_OTHER;
        return 0;
    }
    time_t now = time(NULL);
    if (now - g_login_time > 900) {  // 15分钟 = 900秒
        g_login_status = 0;
        p_msg->ack_flag = ACK_ERROR_OTHER;
        return 0;
    }
}

登录成功时设置状态:

c

if (/* 验证通过 */) {
    p_msg->ack_flag = ACK_SUCCESS;
    g_login_status = 1;
    g_login_time = time(NULL);
}

登出处理(MSG_LOGOUT):

c

case MSG_LOGOUT: {
    g_login_status = 0;
    g_login_time = 0;
    p_msg->ack_flag = ACK_SUCCESS;
    break;
}

八、go2rtc 交叉编译详细过程

8.1 准备交叉编译工具链

SSD202D 芯片使用 ARM Cortex-A7 双核 @1.2GHz,交叉编译工具链为 arm-linux-gnueabihf-gcc 8.2.1:

bash

# 安装工具链(Ubuntu/Debian)
sudo apt install gcc-arm-linux-gnueabihf binutils-arm-linux-gnueabihf

# 验证
arm-linux-gnueabihf-gcc --version
# 应显示 8.2.1 或相近版本

8.2 安装 Go 语言环境

go2rtc 使用 Go 编写,需要 Go 1.24 或更新版本:

bash

# 下载 Go(以 1.24.0 为例)
wget https://go.dev/dl/go1.24.0.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.24.0.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
go version

8.3 获取 go2rtc 源码

bash

git clone https://github.com/AlexxIT/go2rtc.git
cd go2rtc

8.4 交叉编译配置

go2rtc 没有 CGO 依赖,所有构建均使用 CGO_ENABLED=0,交叉编译非常直接。标准构建命令为:

bash

go build -ldflags "-s -w" -trimpath

各参数含义:

参数作用
-ldflags "-s -w"去除符号表和 DWARF 调试信息,减小二进制体积
-trimpath从二进制中移除本地文件系统路径
CGO_ENABLED=0禁用 CGO,生成完全静态链接的二进制文件

ARMv7 架构编译(对应 SSD202D):

bash

export GOOS=linux
export GOARCH=arm
export GOARM=7
export CGO_ENABLED=0
go build -ldflags "-s -w" -trimpath -o go2rtc_linux_arm

ARMv6 架构编译(树莓派 Zero 等):

bash

export GOARM=6
go build -ldflags "-s -w" -trimpath -o go2rtc_linux_armv6

ARM64 架构编译(如需):

bash

export GOARCH=arm64
go build -ldflags "-s -w" -trimpath -o go2rtc_linux_arm64

8.5 预编译二进制(推荐)

对于生产环境,更推荐直接使用官方预编译的二进制文件:

bash

# 下载 ARMv7 版本
wget https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_linux_arm
chmod +x go2rtc_linux_arm

官方 CI 为所有支持的平台生产预构建产物,ARMv7 版本对应 go2rtc_linux_arm。UPX 压缩应用于 Linux 二进制文件以进一步减小体积。

8.6 验证编译结果

bash

# 检查架构
file go2rtc_linux_arm
# 应显示: ELF 32-bit LSB executable, ARM, version 1 (SYSV), statically linked

# 检查版本
./go2rtc_linux_arm -version

九、部署与测试步骤

9.1 文件部署

text

/customer/
├── go2rtc_linux_arm          # go2rtc 可执行文件
├── main                      # WebDeal 主程序(已集成 go2rtc 管理逻辑)
└── web/
    ├── webrtc.html           # IPC 预览页面
    ├── get_ipc_list.cgi      # 获取 IPC 列表 CGI
    ├── control_go2rtc.cgi    # 启停 go2rtc CGI
    ├── cgic.c                # CGI 库(编译依赖)
    └── cgi_fifo.c            # Socket 通信库(编译依赖)

9.2 CGI 编译

bash

# 编译 get_ipc_list.cgi
arm-linux-gnueabihf-gcc get_ipc_list.c cgic.c cgi_fifo.c -o get_ipc_list.cgi

# 编译 control_go2rtc.cgi
arm-linux-gnueabihf-gcc control_go2rtc.c cgic.c cgi_fifo.c -o control_go2rtc.cgi

# 设置执行权限
chmod +x /customer/web/*.cgi
chmod +x /customer/go2rtc_linux_arm

9.3 写入测试 IPC 配置

通过设备其他设置界面或直接调用 LL_SETT_SET_STR 写入测试数据:

text

SETT_IPC01 = "Camera1@#admin@#123456@#192.168.1.100@#554@#live/0/SUB"

9.4 启动服务

bash

# 启动 WebDeal(通常设备开机自启)
# 若需手动启动:
/customer/main &

# 验证 WebDeal 监听
netstat -tuln | grep 7777

# 验证 go2rtc(通过 Web 页面触发后)
ps | grep go2rtc
netstat -tuln | grep 1984

9.5 访问测试

浏览器访问 http://device-ip/webrtc.html,应看到 IPC 列表,点击即可播放。


十、常见问题与调优

问题原因解决方案
go2rtc 启动后立即退出RTSP 地址无效或认证失败查看 /tmp/go2rtc.log 确认错误信息
浏览器无法播放编码格式不兼容(如 H.265)摄像头侧改为 H.264,或通过 FFmpeg 转码
go2rtc 启动后立刻停止前端误发 stop 请求WebDeal 的 3 秒保护机制会自动忽略
页面显示旧版本浏览器缓存菜单跳转添加 ?t=Date.now() 参数
CORS 跨域错误go2rtc 未开启 CORS启动时设置 CORS_ALLOW_ALL=1 环境变量
WebRTC 连接失败NAT 穿透问题go2rtc 配置中指定 candidates 本地 IP
延迟过高使用了 HLS 降级检查 mode 参数,确保 WebRTC 优先

十一、总结

通过集成 go2rtc,我们以极低的开发成本实现了嵌入式设备的 Web 无插件 RTSP 播放。整个系统的核心设计思路可总结为:

  1. 协议转换层:go2rtc 作为轻量级流媒体网关,承担 RTSP → WebRTC/FLV 的核心转换任务,其零拷贝设计和 UDP 优先传输保证了低延迟体验。

  2. 业务逻辑层:WebDeal 守护进程集中管理 IPC 配置、go2rtc 子进程生命周期和登录会话,通过 Socket 与 CGI 通信,实现前后端解耦。

  3. 展示层:前端页面通过 iframe 嵌入 go2rtc 自带播放器,配合状态反馈和防重入机制,提供流畅的用户体验。

如有侵权,或需要完整代码,请及时联系博主。

更多推荐