欢迎光临
我们一直在努力

基于 SmartMediaKit 的Linux平台多路RTSP转RTMP推送实战详解

摘要:本文结合RTSP、RTMP协议规范与实际转发应用场景,深入剖析大牛直播SDK(SmartMediaKit)在Linux平台下实现多路RTSP拉流转RTMP推送的完整技术方案,涵盖架构设计、核心代码解读、性能优化等维度,并提供可直接参考的生产级示例代码。


1. 背景与应用场景

在视频监控、智慧园区、直播平台、工业物联网等领域,RTSP转RTMP的流媒体转发需求极为普遍。其核心诉求在于:

  • 摄像头接入:海康、大华、宇视等品牌IP摄像头普遍基于RTSP协议输出视频流,而CDN、直播平台、Web播放器通常以RTMP作为推流标准。
  • 协议桥接:RTSP面向局域网,天然支持TCP/UDP双模式;RTMP面向互联网,具备更广泛的播放端兼容性,两者之间需要一座"桥梁"。
  • 多路并发:企业级场景动辄需要同时转发数十路乃至数百路视频流,对系统资源利用率和稳定性要求极高。
  • 附加功能:转发的同时,还需支持本地录像存档、定时截图、视频分析等扩展能力。

传统方案(如FFmpeg管道调用)在多路并发、低延迟、资源复用等方面存在明显短板。大牛直播SDK(SmartMediaKit)针对上述痛点,提供了一套专为生产环境设计的高性能流媒体中转解决方案。


2. RTSP与RTMP协议核心规范回顾

2.1 RTSP(Real Time Streaming Protocol)

RTSP(RFC 2326)是一种应用层控制协议,专为多媒体数据流的实时传输设计。其核心特征包括:

  • 会话控制导向:RTSP本身不传输媒体数据,仅负责流媒体会话的建立、控制(播放/暂停/停止)和拆除,实际媒体数据通过RTP/RTCP传输。
  • 双模传输:支持RTP over UDP(低延迟,适合局域网)和RTP over TCP(穿透NAT,适合复杂网络环境)。
  • 典型交互流程:OPTIONS → DESCRIBE → SETUP → PLAY → (RTP/RTCP数据流) → TEARDOWN
  • 地址格式:rtsp://[username:password@]host[:port]/path

RTSP流在经过路由器、防火墙时容易被拦截,不适合直接对外提供服务,这是RTSP→RTMP转发需求的根本驱动力。

2.2 RTMP(Real-Time Messaging Protocol)

RTMP(Adobe Systems)是一种基于TCP的流媒体传输协议,专为低延迟音视频传输设计:

  • 连接握手:基于TCP三次握手之上,RTMP自有三次握手(C0/C1/C2 ↔ S0/S1/S2),建立连接后进行Stream创建。
  • 消息分块:数据以Chunk形式传输,默认Chunk Size为128字节,支持动态调整以适应不同网络条件。
  • 消息类型:包括音频消息(类型8)、视频消息(类型9)、数据消息(AMF编码)、控制消息等。
  • H.264封装规范:视频数据以NALU形式封装,关键帧(IDR)前必须携带SPS/PPS(即AVCDecoderConfigurationRecord),这是转发过程中需要特殊处理的关键细节。
  • AAC封装规范:音频数据前需发送AudioSpecificConfig作为初始化包。

2.3 转发中的核心转换点

维度RTSP/RTPRTMP
传输层 UDP / TCP TCP
时间戳单位 采样率相关(视频90000Hz,音频8000/44100Hz等) 毫秒(ms)
H.264封装 Annex B(00 00 00 01前缀)或AVCC AVCC(带长度前缀)
音频封装 RTP负载(G.711/AAC/Speex等) FLV AudioTag
关键帧标识 RTP Marker Bit / SPS/PPS NAL FLV VideoTag KeyFrame标志位

时间戳的精确转换和封装格式的无缝适配,是转发延迟与画面质量的关键所在。


3. RTSP转RTMP的技术挑战

实现一个生产级的RTSP→RTMP转发系统,需要攻克以下技术难题:

① 时间戳对齐与连续性保障 RTSP流的RTP时间戳基于采样率(如视频90000Hz),需精确换算为毫秒并保证单调递增,否则RTMP服务端可能出现丢帧、花屏。

② 码流格式转换 H.264在RTP中以Annex B格式传输(起始码00 00 00 01),而RTMP要求AVCC格式(4字节长度前缀)。SPS/PPS需从码流中提取并在推流前作为DecoderConfigurationRecord单独发送。

③ 网络异常自动重连 摄像头断电、网络抖动等场景下,需要自动重连拉流,且重连后需重新发送SPS/PPS等初始化数据,保证推流端不中断。

④ 多路并发资源隔离 数十路流并发转发时,每路流的拉流、解析、推送状态需完全隔离,一路异常不能影响其他路。

⑤ 句柄复用与资源节省 当同一路RTSP流既需要推RTMP又需要录像或截图时,若各自独立拉一路流,资源消耗翻倍。优秀的架构应支持"一拉多用"——同一个拉流句柄同时服务多种功能。


4. 大牛直播SDK(SmartMediaKit)简介

大牛直播SDK(SmartMediaKit)是国内专注流媒体领域的高性能SDK产品,历经多年商业化打磨,具备以下核心优势:

4.1 低延迟

  • 拉流到推流的端到端延迟可控制在200ms以内。
  • 时间戳精确透传,避免帧积压引起的延迟累积。
  • 支持低延时模式(SetLowLatencyMode),适配监控实时监看场景。

4.2 高性能

  • 纯C/C++实现,无JVM、无GC开销,内存占用极低。
  • 多路并发时,单路流资源占用极低(取决于分辨率和硬件),支持多路并发转发。
  • 异步非阻塞架构,拉流、推流线程完全解耦,互不干扰。

4.3 功能全面

  • 支持RTSP(含TCP/UDP自动切换)、RTMP拉流。
  • 推流支持RTMP、RTSP、RTSP Server等多种输出方式。
  • 内置本地录像(MP4/FLV),支持按文件大小自动切片。
  • 内置定时截图,输出PNG格式图片。
  • 音频转码AAC,兼容主流编码格式(PCM-A/U、Speex等)。
  • 完整的事件回调体系(连接状态、下载速度、录像文件状态等)。

4.4 架构灵活

  • 句柄复用机制:一个拉流句柄可同时用于推送RTMP和本地录像,最小化资源占用。
  • 数据回调解耦:视频/音频编码数据通过回调透明暴露,开发者可接入自定义处理逻辑(AI分析、水印叠加等)。
  • 模块化封装:拉流(SmartPlayerSDK)与推流(SmartPublisherSDK)完全独立,灵活组合。

5. SDK整体架构设计解析

5.1 SDK模块划分

SmartMediaKit
├── SmartPlayerSDK(拉流/播放模块)
│ ├── RTSP拉流(TCP/UDP自动切换)
│ ├── RTMP拉流
│ ├── 视频帧回调(YUV/RGB)
│ ├── 音频PCM回调
│ ├── 编码数据回调(H.264/AAC裸流)
│ ├── 本地录像
│ └── 截图
└── SmartPublisherSDK(推流/发布模块)
├── RTMP推流
├── RTSP推流
├── RTSP Server
├── 编码数据注入接口
└── 本地录像

5.2 RTSP转RTMP数据流向

RTSP摄像头


SmartPlayerSDK(拉流句柄)

├──→ SetPullStreamVideoDataCallBack → 视频编码数据回调
│ │
│ ▼
│ SmartPublisherSDK(推流句柄)
│ │
│ └──→ PostVideoEncodedDataV2 → RTMP服务器

└──→ SetPullStreamAudioDataCallBack → 音频编码数据回调


SmartPublisherSDK(推流句柄)

└──→ PostAudioEncodedData → RTMP服务器

5.3 Wrapper层架构

Demo代码在SDK原生接口上封装了一套优雅的Wrapper层,体现了良好的工程设计:

RelaySDKWrapper ← 转发任务管理(一路转发的完整生命周期)
├── PullStreamSDKHandleWrapper ← 拉流句柄包装(含事件分发)
└── PushStreamSDKHandleWrapper ← 推流句柄包装

PullStreamSDKEventHandler ← 事件监听接口(下载速度、连接状态等)

这种分层设计使得:

  • 拉流句柄可以被多个RelaySDKWrapper共享(句柄复用)。
  • 事件处理通过观察者模式分发,支持多监听者。
  • 推流句柄的访问通过recursive_mutex保护,线程安全。

6. 核心数据结构与API接口详解

6.1 拉流视频数据回调结构

// 拉流吐视频数据时附带的关键信息
typedef struct _NT_SP_PullStreamVideoDataInfo
{
NT_INT32 is_key_frame_; // 1:关键帧(IDR), 0:非关键帧
NT_UINT64 timestamp_; // 解码时间戳(DTS),单位毫秒
NT_INT32 width_; // 一般为0(由SPS中获取)
NT_INT32 height_; // 一般为0
NT_BYTE* parameter_info_; // 一般为NULL
NT_UINT32 parameter_info_size_; // 一般为0
NT_UINT64 presentation_timestamp_; // 显示时间戳(PTS),≥ timestamp_,单位毫秒
} NT_SP_PullStreamVideoDataInfo;

设计亮点:同时提供DTS和PTS,确保推送到RTMP服务端后播放端不出现时序异常。

6.2 拉流音频数据回调结构

typedef struct _NT_SP_PullStreamAuidoDataInfo
{
NT_INT32 is_key_frame_; // 音频关键帧标识
NT_UINT64 timestamp_; // 时间戳,单位毫秒
NT_INT32 sample_rate_; // 采样率(一般为0,由parameter_info中解析)
NT_INT32 channel_; // 通道数
NT_BYTE* parameter_info_; // AAC: AudioSpecificConfig; 其他编码一般为NULL
NT_UINT32 parameter_info_size_; // parameter_info的字节长度
NT_UINT64 reserve_; // 保留
} NT_SP_PullStreamAuidoDataInfo;

特别说明:parameter_info_对于AAC音频至关重要,它是AudioSpecificConfig,推流端调用PostAudioEncodedData时必须透传,RTMP服务器依此完成音频流初始化。

6.3 关键推流接口

// 推送H.264/H.265编码视频数据(V2版本,支持DTS+PTS双时间戳)
NT_UINT32 PostVideoEncodedDataV2(
NT_HANDLE handle,
NT_UINT32 codec_id, // 参考NT_MEDIA_CODEC_ID
const NT_BYTE* data, // 编码数据
NT_UINT32 size, // 数据大小
NT_INT32 is_key_frame, // 是否关键帧
NT_UINT64 timestamp, // 解码时间戳(ms)
NT_UINT64 presentation_timestamp // 显示时间戳(ms),必须 >= timestamp
);

// 推送AAC/Speex等编码音频数据
NT_UINT32 PostAudioEncodedData(
NT_HANDLE handle,
NT_UINT32 codec_id,
const NT_BYTE* data,
NT_UINT32 size,
NT_INT32 is_key_frame,
NT_UINT64 timestamp,
const NT_BYTE* parameter_info, // AAC: AudioSpecificConfig
NT_UINT32 parameter_info_size
);


7. 完整实战代码解读

7.1 初始化:日志与SDK

// 日志系统初始化(推荐在main函数最开始调用)
void LogInit()
{
SmartLogAPI log_api;
memset(&log_api, 0, sizeof(log_api));
GetSmartLogAPI(&log_api);

log_api.SetLevel(SL_INFO_LEVEL); // 日志级别:INFO
log_api.SetPath((NT_PVOID)"./"); // 日志输出路径
}

// 拉流SDK + 推流SDK双初始化
bool PullPushSDKInit(SmartPlayerSDKAPI& pull_api, NT_SmartPublisherSDKAPI& push_api)
{
memset(&pull_api, 0, sizeof(pull_api));
GetSmartPlayerSDKAPI(&pull_api); // 获取拉流SDK函数指针表

auto ret = pull_api.Init(0, nullptr);
if (NT_ERC_OK != ret) {
fprintf(stderr, "pull_api.Init failed!\\n");
return false;
}

memset(&push_api, 0, sizeof(push_api));
NT_GetSmartPublisherSDKAPI(&push_api); // 获取推流SDK函数指针表

ret = push_api.Init(0, nullptr);
if (NT_ERC_OK != ret) {
pull_api.UnInit();
fprintf(stderr, "push_api.Init failed!\\n");
return false;
}

return true;
}

工程要点:SDK API通过函数指针结构体(SmartPlayerSDKAPI / NT_SmartPublisherSDKAPI)暴露,memset清零后调用GetXxxSDKAPI填充,是典型的C风格动态库接口范式,便于ABI兼容和跨版本升级。

7.2 转发任务配置

// 每路转发任务的配置项
struct RelayConfigItem
{
const char* pull_url_; // 拉流地址(支持RTSP/RTMP)
bool is_push_rtmp_; // 是否转RTMP推送
bool is_recorder_; // 是否本地录像
bool is_capture_image_; // 是否定时截图
};

// 实际配置示例(可扩展为多路)
RelayConfigItem relay_conf_items[] = {
{
"rtsp://admin:daniulive12345@192.168.0.120:554/h264/ch1/main/av_stream",
true, // 开启RTMP推送
false, // 不录像
false, // 不截图
},
// 可继续增加更多路…
};

// RTMP推流基地址
const char* rtmp_push_base_url = "rtmp://192.168.0.107/live/";

// 自动生成各路推流URL(避免URL冲突)
std::string MakeRtmpPushUrl(int index)
{
std::ostringstream ss;
ss << rtmp_push_base_url << "linuxrelaytest" << index;
return ss.str();
// 生成如:rtmp://192.168.0.107/live/linuxrelaytest1
}

7.3 启动转发任务(核心流程)

void StartTasks(SmartPlayerSDKAPI* pull_api, NT_SmartPublisherSDKAPI* push_api,
std::vector<std::shared_ptr<nt_app::RelaySDKWrapper>>& relays,
std::vector<std::shared_ptr<nt_app::PullStreamSDKHandleWrapper>>& player_handles)
{
int i = 0;

for (const auto& conf_item : relay_conf_items)
{
++i;
if (nullptr == conf_item.pull_url_ || '0' == conf_item.pull_url_[0])
continue;

std::shared_ptr<nt_app::PullStreamSDKHandleWrapper> pull_handle;

// ========== 截图场景:独立拉一路播放流(用于视频帧回调)==========
if (conf_item.is_capture_image_)
{
auto handle = std::make_shared<nt_app::PullStreamSDKHandleWrapper>(pull_api);

if (handle->Open(conf_item.pull_url_, 0))
{
// 设置视频帧回调,I420格式(性能优先,避免YUV→RGB转换开销)
pull_api->SetVideoFrameCallBack(handle->Handle(),
NT_SP_E_VIDEO_FRAME_FROMAT_I420,
nullptr, OnPlayerSDKVideoFrameHandle);

if (NT_ERC_OK == pull_api->StartPlay(handle->Handle()))
{
player_handles.push_back(handle);
pull_handle = handle; // 记录句柄供后续复用
}
}
}

if (!conf_item.is_push_rtmp_ && !conf_item.is_recorder_)
continue;

// ========== 创建转发任务 ==========
auto relay_item = std::make_shared<nt_app::RelaySDKWrapper>(pull_api, push_api);

relay_item->SetPullURL(conf_item.pull_url_);

// 【关键优化】:如果截图已经建立了拉流句柄,则复用它
// 避免对同一摄像头建立两条RTSP连接,节省带宽和摄像头资源
if (pull_handle && pull_handle->IsOpened())
{
relay_item->AttachPullHandle(pull_handle);
}

// ========== RTMP推送配置 ==========
if (conf_item.is_push_rtmp_)
{
relay_item->SetRelayVideo(true); // 转发视频
relay_item->SetRelayAudio(true); // 转发音频(可按需关闭)

if (relay_item->StartPull())
{
if (!relay_item->StartPushRtmp(MakeRtmpPushUrl(i)))
{
fprintf(stderr, "start push rtmp failed!\\n");
relay_item->StopPull();
}
}
else
{
fprintf(stderr, "start pull failed!\\n");
}
}

// ========== 录像配置 ==========
if (conf_item.is_recorder_)
{
relay_item->SetRecorderDirectory(rec_dir);
relay_item->SetRecorderFileMaxSize(200); // 单个文件最大200MB
relay_item->SetRecorderAudioTranscodeAAC(true); // 音频转码为AAC(通用性强)
relay_item->SetRecorderAppendDate(true); // 文件名含日期
relay_item->SetRecorderAppendTime(true); // 文件名含时间

relay_item->SetRecorderVideo(true);
relay_item->SetRecorderAudio(false); // 本示例不录音频

std::ostringstream ss;
ss << "rt-" << i << "-";
relay_item->SetRecorderFileNamePrefix(ss.str()); // 文件名前缀:rt-1-

if (!relay_item->StartRecorder())
{
fprintf(stderr, "start recorder failed!\\n");
}
}

if (relay_item->IsWorking())
{
relays.push_back(relay_item);
}
}
}

7.4 视频/音频数据透传(核心回调)

这是整个转发链路的心脏部分。拉流SDK获取到编码数据后,直接注入推流SDK,全程无解码、无重编码,保持最低延迟和最高质量:

// 视频编码数据透传回调
extern "C" NT_VOID NT_CALLBACK
NT_APP_SP_SDK_Wrapper_PullStreamVideoDataHandle(
NT_HANDLE handle, NT_PVOID user_data,
NT_UINT32 video_codec_id,
NT_BYTE* data, NT_UINT32 size,
NT_SP_PullStreamVideoDataInfo* info,
NT_PVOID reserve)
{
auto relay_wrapper = reinterpret_cast<nt_app::RelaySDKWrapper*>(user_data);
if (relay_wrapper == nullptr)
return;

relay_wrapper->OnPullVideoDataHandle(handle, video_codec_id, data, size, info);
}

// RelaySDKWrapper内部实现:将拉到的视频数据推给推流SDK
void RelaySDKWrapper::OnPullVideoDataHandle(NT_HANDLE handle,
NT_UINT32 video_codec_id, NT_BYTE* data, NT_UINT32 size,
NT_SP_PullStreamVideoDataInfo* info)
{
if (!is_relay_video_) return; // 未启用视频转发
if (!is_pushing_) return; // 推流未启动
if (!pull_handle_) return;
if (pull_handle_->Handle() != handle) return; // 句柄校验(多路隔离)
if (nullptr == data || size < 1) return;
if (nullptr == info) return;

// 加锁保护推流句柄(StopPushRtmp可能并发调用)
std::unique_lock<std::recursive_mutex> lock(push_handle_mutex_);

if (!is_pushing_ || !push_handle_ || nullptr == push_handle_->Handle())
return;

// 直接透传:视频编码数据 → RTMP推流
// 同时传入DTS和PTS,完整支持B帧场景
push_api_->PostVideoEncodedDataV2(
push_handle_->Handle(),
video_codec_id,
data, size,
info->is_key_frame_,
info->timestamp_, // DTS
info->presentation_timestamp_ // PTS
);
}

// 音频编码数据透传回调
void RelaySDKWrapper::OnPullAudioDataHandle(NT_HANDLE handle,
NT_UINT32 audio_codec_id, NT_BYTE* data, NT_UINT32 size,
NT_SP_PullStreamAuidoDataInfo* info)
{
if (!is_relay_audio_) return;
if (!is_pushing_) return;
if (!pull_handle_) return;
if (pull_handle_->Handle() != handle) return;
if (nullptr == data || size < 1) return;
if (nullptr == info) return;

std::unique_lock<std::recursive_mutex> lock(push_handle_mutex_);

if (!is_pushing_ || !push_handle_ || nullptr == push_handle_->Handle())
return;

// 音频透传:关键是同时传入parameter_info(AAC的AudioSpecificConfig)
push_api_->PostAudioEncodedData(
push_handle_->Handle(),
audio_codec_id,
data, size,
info->is_key_frame_,
info->timestamp_,
info->parameter_info_, // AAC必须传此字段
info->parameter_info_size_
);
}

性能关键点:整个数据路径是零拷贝零解码的直通模式。拉流SDK将RTSP中的H.264/AAC编码数据直接回调出来,应用层不做任何解码处理,直接调用推流SDK的PostVideoEncodedDataV2 / PostAudioEncodedData注入,推流SDK负责重新封装为RTMP格式发送。这种设计使得CPU消耗极低,延迟控制在毫秒级。

7.5 拉流句柄包装(Wrapper层解析)

// 打开拉流句柄,配置关键参数
bool PullStreamSDKHandleWrapper::Open(const std::string& url, NT_INT32 buffer)
{
NT_HANDLE handle = nullptr;

// Linux平台Open接口
if (NT_ERC_OK != sdk_api_->Open(&handle, 0, nullptr))
return false;

// 设置事件回调(连接状态、下载速度等事件通知)
sdk_api_->SetEventCallBack(handle, this,
&NT_APP_PULL_STREAM_SDK_HandleWrapper_SDKEventHandle);

// 设置缓冲(0表示最小缓冲,适合低延迟场景)
sdk_api_->SetBuffer(handle, buffer);

// RTSP自动切换TCP/UDP模式(先UDP,失败自动切TCP,或反之)
// 这对于穿越NAT的摄像头接入至关重要
sdk_api_->SetRtspAutoSwitchTcpUdp(handle, 1);

// 每3秒上报一次下载速度(用于监控带宽使用)
sdk_api_->SetReportDownloadSpeed(handle, 1, 3);

if (NT_ERC_OK != sdk_api_->SetURL(handle, url.c_str()))
{
sdk_api_->Close(handle);
return false;
}

handle_ = handle;
url_ = url;
return true;
}

SetRtspAutoSwitchTcpUdp的重要性:在实际摄像头接入中,UDP模式延迟低但易受网络抖动影响;TCP模式稳定但延迟略高。开启自动切换后,SDK会智能探测并选择最佳传输方式,这对于跨网段、跨VLAN的摄像头接入尤为重要。

7.6 推流句柄初始化

// 推流句柄打开:指定视频和音频源类型为"编码后的数据"
bool PushStreamSDKHandleWrapper::Open(NT_UINT32 video_option, NT_UINT32 auido_option)
{
NT_HANDLE handle = nullptr;

// video_option = NT_PB_E_VIDEO_OPTION_ENCODED_DATA (= 0x4)
// auido_option = NT_PB_E_AUDIO_OPTION_ENCODED_DATA (= 0x4)
// 表明推流数据来源是外部注入的编码数据,而非摄像头采集或屏幕录制
if (NT_ERC_OK != sdk_api_->Open(&handle,
video_option, auido_option, 0, nullptr))
return false;

handle_ = handle;
return true;
}

7.7 启动推流到RTMP服务器

bool RelaySDKWrapper::StartPushRtmp(const std::string& url)
{
if (is_pushing_) return false;
if (url.empty()) return false;

if (!OpenPushHandle()) return false;

auto push_handle = GetPushHandle();

// 设置RTMP推送目标URL
if (NT_ERC_OK != push_api_->SetURL(push_handle->Handle(), url.c_str(), nullptr))
{
ResetPushHandle();
return false;
}

// 启动推流
if (NT_ERC_OK != push_api_->StartPublisher(push_handle->Handle(), nullptr))
{
ResetPushHandle();
return false;
}

is_pushing_ = true;
return true;
}

7.8 主循环与优雅退出

int main(int argc, char *argv[])
{
// 注册SIGINT信号处理,支持Ctrl+C或kill -s SIGINT优雅退出
signal(SIGINT, &OnSigIntHandler);

LogInit();

SmartPlayerSDKAPI pull_api;
NT_SmartPublisherSDKAPI push_api;

if (!PullPushSDKInit(pull_api, push_api))
return 0;

std::vector<std::shared_ptr<nt_app::RelaySDKWrapper>> relays;
std::vector<std::shared_ptr<nt_app::PullStreamSDKHandleWrapper>> player_handles;

StartTasks(&pull_api, &push_api, relays, player_handles);

time_t last_capture_image_time = 0;

// 主循环:每秒唤醒一次,处理定时截图
while (!g_is_exit)
{
sleep(1);
CaptureImage(&pull_api, player_handles, last_capture_image_time);
}

// 按序停止:先停播放,再停转发,最后反初始化SDK
StopTasks(&pull_api, &push_api, relays, player_handles);

push_api.UnInit();
pull_api.UnInit();

return 0;
}

停止顺序的重要性:StopTasks中先停止播放句柄(截图用),再停止所有relay(含推流和录像)。必须在推流StopPublisher和录像StopRecorder完成后,才能调用拉流StopPullStream和Close,否则SDK内部数据管道可能出现野指针访问。


8. 多路并发转发性能优化

8.1 句柄复用机制

当同一路RTSP流需要同时推RTMP、录像、截图时,Demo中展示了句柄复用策略:

// 如果截图已经建立了PullHandle,转发任务直接AttachPullHandle复用
if (pull_handle && pull_handle->IsOpened())
{
relay_item->AttachPullHandle(pull_handle);
}

AttachPullHandle内部通过观察者模式将RelaySDKWrapper注册为拉流句柄的事件监听者:

bool RelaySDKWrapper::AttachPullHandle(
const std::shared_ptr<PullStreamSDKHandleWrapper>& pull_handle)
{
if (is_pulling_) return false;

pull_handle_ = pull_handle;
if (pull_handle_)
{
pull_handle_->AddEventHandler(shared_from_this());
}
return true;
}

这样,同一个RTSP连接上的数据,可以同时分发给推流逻辑和录像逻辑,节省了一半的网络带宽和摄像头并发连接数。

8.2 线程安全设计

推流句柄的访问使用recursive_mutex而非普通mutex,原因在于StopPushRtmp和OnPullVideoDataHandle可能在不同线程并发执行:

// 数据回调线程中
void RelaySDKWrapper::OnPullVideoDataHandle(…)
{
std::unique_lock<std::recursive_mutex> lock(push_handle_mutex_);
// 加锁后再次检查is_pushing_,防止TOCTOU竞态
if (!is_pushing_ || !push_handle_) return;
push_api_->PostVideoEncodedDataV2(…);
}

// 控制线程中
void RelaySDKWrapper::StopPushRtmp()
{
if (!is_pushing_) return;
is_pushing_ = false; // 先设置原子标志

std::unique_lock<std::recursive_mutex> lock(push_handle_mutex_);
push_api_->StopPublisher(push_handle_->Handle());
push_handle_.reset();
}

注意is_pushing_被声明为std::atomic_bool,其写操作(is_pushing_ = false)在加锁前完成,使数据回调线程能尽快退出临界区,减少锁争用。

8.3 弱引用事件处理

拉流句柄的事件处理列表使用std::weak_ptr持有监听者:

std::vector<std::weak_ptr<PullStreamSDKEventHandler>> event_handlers_;

OnSDKEvent分发时自动跳过已销毁的监听者,避免了因组件生命周期不一致导致的悬空引用问题。


9. 录像与截图功能集成

9.1 录像配置详解

// 录像配置(以RelaySDKWrapper为例)
relay_item->SetRecorderDirectory("./testrec"); // 录像存储目录
relay_item->SetRecorderFileMaxSize(200); // 单文件最大200MB(超出自动切片)
relay_item->SetRecorderAudioTranscodeAAC(true); // 非AAC音频自动转码为AAC
relay_item->SetRecorderAppendDate(true); // 文件名含日期:rt-1-2024-03-15
relay_item->SetRecorderAppendTime(true); // 文件名含时间:rt-1-2024-03-15-14-30-00
relay_item->SetRecorderVideo(true); // 录制视频
relay_item->SetRecorderAudio(false); // 不录制音频(按需开启)
relay_item->SetRecorderFileNamePrefix("rt-1-"); // 文件名前缀

relay_item->StartRecorder();

录像回调可监听文件切片事件:

// 录像状态回调
extern "C" NT_VOID NT_CALLBACK NT_APP_SP_SDK_Wrapper_RecorderHandle(
NT_HANDLE handle, NT_PVOID user_data,
NT_UINT32 status, // 1:开始写新文件, 2:一个文件写完
NT_PCSTR file_name) // 实际录像文件路径
{
auto relay_wrapper = reinterpret_cast<nt_app::RelaySDKWrapper*>(user_data);
if (relay_wrapper == nullptr) return;

std::string name;
if (file_name != nullptr) name = file_name;

relay_wrapper->OnRecorderHandle(handle, status, name);
}

9.2 定时截图实现

void CaptureImage(SmartPlayerSDKAPI* player_api,
const std::vector<std::shared_ptr<nt_app::PullStreamSDKHandleWrapper>>& player_handles,
time_t& last_capture_image_time)
{
if (player_handles.empty()) return;

auto cur_t = time(nullptr);

// 每30秒截图一次
if (0 == last_capture_image_time ||
cur_t >= (last_capture_image_time + capture_image_interval))
{
int i = 0;
for (auto& handle : player_handles)
{
++i;
if (handle && handle->IsOpened())
{
// 生成带时间戳的文件名
// 格式:./testcapture/tc-1-03-15-14-30-00.png
player_api->CaptureImage(
handle->Handle(),
MakeCaptureImageFileName(i).c_str(),
nullptr,
&OnPlayerSDKCaptureImageCallBack // 截图完成回调
);
}
}
last_capture_image_time = time(nullptr);
}
}

// 截图完成回调
void OnPlayerSDKCaptureImageCallBack(NT_HANDLE handle, NT_PVOID user_data,
NT_UINT32 result, NT_PCSTR file_name)
{
if (NT_ERC_OK == result)
fprintf(stdout, "capture image ok: %s handle:%p\\n", file_name, handle);
else
fprintf(stdout, "capture image failed, handle:%p\\n", handle);
}

注意事项:截图为异步操作,PNG编码较耗时(通常需数百毫秒),SDK内部做了请求队列限流,若短时间内频繁调用会返回NT_ERC_SP_TOO_MANY_CAPTURE_IMAGE_REQUESTS,应用层应加频率限制(如上述30秒间隔)。


10. 生产环境部署建议

10.1 编译与运行环境

# 测试环境:CentOS 7.6 64位(也支持Ubuntu 18.04+)
# 依赖:gcc/g++ 4.8+,支持C++11

# 后台运行(推荐)
nohup ./SmartStreamRelayDemo >/dev/null 2>&1 &

# 优雅终止(会触发SIGINT,执行正常停止流程)
kill -s SIGINT <pid>

# 不推荐:kill -9 会导致录像文件尾部损坏,推流不正常断开

10.2 录像和截图目录权限

mkdir -p ./testrec ./testcapture
chmod 777 ./testrec ./testcapture

10.3 多路规模化配置建议

当需要转发大量摄像头时,建议将relay_conf_items替换为从配置文件(JSON/YAML)或数据库动态读取,并增加以下生产级特性:

  • 健康检测:定期检查IsWorking()状态,对异常任务自动重启。
  • 带宽监控:利用NT_SP_E_EVENT_ID_DOWNLOAD_SPEED事件记录各路流的下载速度,及时发现摄像头信号异常。
  • 资源配额:根据服务器CPU/内存情况限制最大并发路数(建议单机不超过设备资源上限的80%)。
  • 进程守护:使用systemd或supervisor管理Demo进程,崩溃后自动重启。

11. 总结

本文以大牛直播SDK(SmartMediaKit)的Linux多路RTSP转RTMP转发Demo为蓝本,从RTSP/RTMP协议规范出发,深入解析了生产级流媒体转发系统的各个技术层面。

核心优势总结:

维度大牛直播SDK表现
延迟 端到端 < 200ms,数据零解码直通
性能 纯C++,单路CPU占用超低,支持多路并发
稳定性 自动TCP/UDP切换,完善的错误处理和状态管理
功能 推流+录像+截图+音频转码,一套SDK全覆盖
架构 句柄复用、事件驱动、线程安全,工程化程度高
易用性 API设计清晰,Wrapper层封装完善,上手成本低

对于有RTSP摄像头接入、多路流转发、监控录像等需求的开发者,大牛直播SDK提供了一套经过验证的完整解决方案,可大幅降低开发成本,缩短上线周期。


📎 CSDN官方博客:音视频牛哥-CSDN博客

赞(0)
未经允许不得转载:171主机测评 » 基于 SmartMediaKit 的Linux平台多路RTSP转RTMP推送实战详解
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址