一 概述
本文章实现了一个国标视频播放客户端.支持IPC相机通过GB28181协议获取的视频流到客户端,将获取的PS流解析为h264/h265流,通过vlc解码后显示到界面.
二 代码架构
该客户端由两部分组成:1.GB28181Player 界面模块 ; 2.GB28181Agent 播放引擎.
GB28181Player模块实现了客户端界面布局和功能操作;
GB28181Agent模块实现的功能有:
1.实现osip服务端;
2.实现udp服务端;
3.获取注册相机;
4.播放/关闭相机.
客户端界面如下:

三 关键代码
1.GB28181Agent对外接口
gbplayerengine.h
cpp
#ifndef GBPLAYERENGINE_H
#define GBPLAYERENGINE_H
#ifdef __cplusplus
extern "C" {
#endif
#if defined(_WIN32) || defined(WIN32)
# define GB_API __declspec(dllexport)
#else
# define GB_API __attribute__((visibility("default")))
#endif
#pragma pack(push, 1)
// 相机信息
typedef struct
{
char deviceId[64];
char deviceName[64];
char ipAddr[32];
int cid;// call id
int did;//dialog id
int port;
int status; // 0=离线 1=在线
int playStatus; // 0=未播放 1=播放中
int channelId;
char reserve[128];
} GB_CameraInfo;
// 相机列表
typedef struct
{
int count;
GB_CameraInfo cameras[128];
} GB_CameraList;
// 播放参数
typedef struct
{
char deviceId[64];
char devIp[32];
int cid;//给IPC发送请求媒体流成功后的call id
int did;
void* renderWnd;
int transportType;
int cacheTime;
int enableAudio;
int port;
char reserve[256];
} GB_PlayParam;
#pragma pack(pop)
// 引擎初始化(启动OSIP + UDP服务)
GB_API int GB_InitEngine(const char* localId,const char* localIp, int localSipPort, int localUdpMediaPort);
// 反初始化
GB_API int GB_UnInitEngine();
// 1. 获取已注册相机列表
GB_API int GB_GetRegisteredCameraList(GB_CameraList* pOutList);
// 2. 播放指定相机
GB_API int GB_StartPlay(GB_PlayParam* pParam);
// 3. 关闭指定相机
GB_API int GB_StopPlay(GB_PlayParam* pParam);
// 工具
GB_API const char* GB_GetVersion();
GB_API const char* GB_GetLastError();
#ifdef __cplusplus
}
#endif
#endif // GBPLAYERENGINE_H
gbplayerengine.cpp
cpp
#include "gbplayerengine.h"
#include "gbsipserver.h"
#include "gbudpmediaserver.h"
#include "gbcameramanager.h"
static char g_lastError[256] = {0};
GlobalData g_globalData;
INITIALIZE_EASYLOGGINGPP
void initializeLogging() {
// 加载配置文件
el::Loggers::configureFromGlobal("global.conf");
}
GB_API int GB_InitEngine(const char* localId,
const char* localIp,
int localSipPort,
int localUdpMediaPort)
{
if (!localIp) return -1;
// 初始化相机管理器
GBCameraManager::getInstance();
g_globalData.local_sipId = localId;
g_globalData.localIp = localIp;
g_globalData.localSipPort = localSipPort;
g_globalData.localUdpMediaPort = localUdpMediaPort;
// 启动OSIP服务
if (!GBSipServer::getInstance()->startServer(localIp, localSipPort)) {
sprintf(g_lastError, "SIP服务启动失败");
LOG(ERROR) << "SIP服务启动失败";
return -2;
}
// 启动UDP媒体服务
if (!GBUdpMediaServer::getInstance()->startServer(localIp, localUdpMediaPort)) {
sprintf(g_lastError, "UDP媒体服务启动失败");
LOG(ERROR) << "UDP媒体服务启动失败";
return -3;
}
sprintf(g_lastError, "初始化成功");
LOG(INFO) << "SIP服务 UDP服务 初始化成功";
return 0;
}
GB_API int GB_UnInitEngine()
{
GBSipServer::getInstance()->stopServer();
GBUdpMediaServer::getInstance()->stopServer();
GBCameraManager::getInstance()->clearAll();
sprintf(g_lastError, "反初始化成功");
return 0;
}
GB_API int GB_GetRegisteredCameraList(GB_CameraList* pOutList)
{
if (!pOutList) return -1;
return GBCameraManager::getInstance()->getCameraList(pOutList);
}
GB_API int GB_StartPlay(GB_PlayParam* pParam)
{
if (!pParam) return -1;
return GBCameraManager::getInstance()->startPlay(pParam);
}
GB_API int GB_StopPlay(GB_PlayParam* pParam)
{
if (!pParam) return -1;
return GBCameraManager::getInstance()->stopPlay(pParam->deviceId);
}
GB_API const char* GB_GetVersion() {
return "GB28181 Player Engine v1.0";
}
GB_API const char* GB_GetLastError() {
return g_lastError;
}
2.udp服务类
gbudpmediaserver.h
cpp
#ifndef GBUDPMEDIASERVER_H
#define GBUDPMEDIASERVER_H
#include <QObject>
#include <QUdpSocket>
#include "gbcameramanager.h"
#include "PsParser.h"
class GBUdpMediaServer : public QObject
{
Q_OBJECT
public:
static GBUdpMediaServer* getInstance();
bool startServer(const QString& ip, int port);
void stopServer();
private slots:
void onReadData();
private:
explicit GBUdpMediaServer(QObject *parent = nullptr);
static GBUdpMediaServer* instance;
QUdpSocket* m_socket = nullptr;
bool m_running = false;
};
#endif // GBUDPMEDIASERVER_H
gbudpmediaserver.cpp
cpp
#include "gbudpmediaserver.h"
GBUdpMediaServer* GBUdpMediaServer::instance = nullptr;
GBUdpMediaServer* GBUdpMediaServer::getInstance()
{
if (!instance) instance = new GBUdpMediaServer;
return instance;
}
GBUdpMediaServer::GBUdpMediaServer(QObject *parent) : QObject(parent) {}
bool GBUdpMediaServer::startServer(const QString& ip, int port)
{
if (m_running) return true;
m_socket = new QUdpSocket(this);
bool ok = m_socket->bind(QHostAddress(ip), port, QUdpSocket::ShareAddress);
m_running = ok;
connect(m_socket, &QUdpSocket::readyRead, this, &GBUdpMediaServer::onReadData);
return ok;
}
void GBUdpMediaServer::stopServer()
{
if (m_socket) {
m_socket->close();
delete m_socket;
m_socket = nullptr;
}
m_running = false;
}
void GBUdpMediaServer::onReadData()
{
while (m_socket->hasPendingDatagrams()) {
QByteArray data;
data.resize(m_socket->pendingDatagramSize());
QHostAddress clientAddr;
quint16 clientPort;
string ipaddr ;
m_socket->readDatagram(data.data(), data.size(),&clientAddr,&clientPort);
ipaddr = clientAddr.toString().toStdString();
// 分发媒体流到对应相机播放会话
GBCameraManager::getInstance()->inputStream(data.data(),data.size(),ipaddr);
}
}
3.sip服务类
gbsipserver.h
cpp
#ifndef GBSIPSERVER_H
#define GBSIPSERVER_H
#include <QObject>
#include <QString>
#include "basic.h"
#include "eXosip2/eXosip.h"
class GBSipServer : public QObject
{
Q_OBJECT
public:
static GBSipServer* getInstance();
bool startServer(const QString& ip, int port, bool isUDP = true);
void stopServer();
/**
* @brief send_invite_to_ipc 给IPC发送INVITE消息,请求视频流
* @param ipc_ip
* @param local_ip
* @param rtp_port
* @return
*/
int send_invite_to_ipc(string ipc_ip, string ipc_id, int ipc_port);
int stop_ipc_media_stream(int cid, int did);
private:
static void exosip_event_loop(void *ctx);
void osip_event_loop_impl();
void handle_exosip_event(eXosip_event_t* event);
void processMessage(eXosip_event_t *osipEvent);
void parse_register_info(eXosip_event_t *e);
void parse_message_info(eXosip_event_t *osipEvent);
private:
explicit GBSipServer(QObject *parent = nullptr);
static GBSipServer* instance;
bool m_running = false;
eXosip_t* m_excontext = nullptr;
};
#endif // GBSIPSERVER_H
gbsipserver.cpp
cpp
#include "gbsipserver.h"
#include "gbcameramanager.h"
#include <QDebug>
GBSipServer* GBSipServer::instance = nullptr;
extern GlobalData g_globalData;
extern void initializeLogging();
int get_ipc_ip_from_event_400(eXosip_event_t *evt, char *ip_buf, int buf_len);
int get_ipc_media_ip_from_sdp(eXosip_event_t *evt, char *media_ip, char *sipId);
GBSipServer* GBSipServer::getInstance()
{
if (!instance) instance = new GBSipServer;
return instance;
}
GBSipServer::GBSipServer(QObject *parent) : QObject(parent)
{
initializeLogging();
}
bool GBSipServer::startServer(const QString& ip, int port,bool isUDP)
{
if (m_running) return true;
m_running = true;
// 1. 初始化eXosip上下文
m_excontext = eXosip_malloc();
int ret = eXosip_init(m_excontext);
if (ret != 0) {
std::cerr << "eXosip初始化失败:" << ret << std::endl;
LOG(ERROR) << "这是默认日志记录器日志";
return false;
}
// 2. 监听UDP端口(国标SIP默认5060)
if(isUDP){
ret = eXosip_listen_addr(m_excontext, IPPROTO_UDP, ip.toStdString().c_str(), port, AF_INET, 0);
if (ret != 0) {
std::cerr << "监听SIP端口失败:" << ret << std::endl;
LOG(ERROR) << "监听SIP端口失败:" << ret;
eXosip_quit(m_excontext);
return -1;
}
}else{
ret = eXosip_listen_addr(m_excontext, IPPROTO_TCP, ip.toStdString().c_str(), port, AF_INET, 0);
if (ret != 0) {
std::cerr << "监听SIP端口失败:" << ret << std::endl;
LOG(ERROR) << "监听SIP端口失败:" << ret;
eXosip_quit(m_excontext);
return -1;
}
}
std::cout << "SIP Server启动成功,监听:" << ip.toStdString() << ":" << port <<": "<< (isUDP==true ?"UDP":"TCP") << std::endl;
LOG(INFO) << "SIP Server启动成功,监听:" << ip.toStdString() << ":" << port <<": "<< (isUDP==true ?"UDP":"TCP");
std::thread event_thread(&GBSipServer::exosip_event_loop,this);
event_thread.detach();
return true;
}
void GBSipServer::stopServer()
{
m_running = false;
}
int GBSipServer::send_invite_to_ipc(std::string ipc_ip, std::string ipc_id, int ipc_port)
{
if (m_excontext == nullptr) {
LOG(ERROR) << "【致命错误】m_excontext 是空指针!!!" << std::endl;
return -1;
}
qDebug() << "【调试】m_excontext 指针有效:" << m_excontext;
std::string to = "sip:"+ipc_id+"@"+ipc_ip+":"+to_string(ipc_port);
std::string from = "sip:"+g_globalData.local_sipId+"@"+g_globalData.localIp+":"+to_string(g_globalData.localUdpMediaPort);
// 1. 创建呼叫会话
osip_message_t * invite = nullptr;
int ret = eXosip_call_build_initial_invite(
m_excontext,
&invite,
to.c_str(),
from.c_str(),
nullptr,
nullptr
);
if (ret != 0) {
std::cerr << "创建呼叫会话失败:" << ret << std::endl;
return -1;
}
// 2. 构造SDP(核心:告诉IPC推流到你的IP和端口)
char sdp[1024] = {0};
snprintf(sdp, sizeof(sdp),
"v=0\r\n"
"o=%s 0 0 IN IP4 %s\r\n"
"s=Play\r\n"
"c=IN IP4 %s\r\n"
"t=0 0\r\n"
"m=video %d RTP/AVP 96\r\n"
"a=rtpmap:96 PS/90000\r\n"
"a=recvonly\r\n",
g_globalData.local_sipId.c_str(), g_globalData.localIp.c_str(), g_globalData.localIp.c_str(), g_globalData.localUdpMediaPort);
osip_message_set_body(invite, sdp, strlen(sdp));
osip_message_set_content_type(invite, "application/sdp");
// 3. 发送Invite请求
eXosip_lock(m_excontext);
ret = eXosip_call_send_initial_invite(m_excontext, invite);
eXosip_unlock(m_excontext);
if (ret > 0) {
std::cout << "Invite请求发送成功,等待IPC响应..." << std::endl;
qDebug() << "IPC:" << ipc_ip.data() <<" Invite请求发送成功,等待IPC响应... call id:" << ret;
} else {
qDebug() << "发送Invite失败:" << ret ;
LOG(ERROR) <<"发送Invite失败:" << ret ;
}
return ret;
}
int GBSipServer::stop_ipc_media_stream(int cid,int did)
{
int ret;
osip_message_t *bye_msg = NULL;
if (cid <= 0)
return -1;
// ================================
// 发送 BYE 到 IPC 相机
// ================================
ret = eXosip_call_terminate(m_excontext, cid, did);
if (ret != 0)
{
qDebug("发送 BYE 失败 ret:%d cid:%d did:%d",ret,cid,did);
return -3;
}
qDebug("发送 BYE 成功 → IPC 相机已停止推流\n");
return 0;
}
void GBSipServer::exosip_event_loop(void *ctx)
{
GBSipServer* sipServer = (GBSipServer*)ctx;
sipServer->osip_event_loop_impl();
}
void GBSipServer::osip_event_loop_impl()
{
while (true) {
if(nullptr == m_excontext)
return;
// 阻塞等待事件(超时100ms,避免卡死)
eXosip_event_t* event = eXosip_event_wait(m_excontext, 0,100);
if (!event) {
continue;
}
eXosip_lock(m_excontext);
eXosip_automatic_action(m_excontext);
eXosip_unlock(m_excontext);
// 处理事件(核心逻辑)
handle_exosip_event(event);
// 释放事件(必须!否则内存泄漏)
eXosip_event_free(event);
}
}
void GBSipServer::handle_exosip_event(eXosip_event_t *event)
{
if (!event) return;
#if 0
qDebug("收到OSIP事件... %d 内容长度:%d\n",
event->type,(event->request->content_length? event->request->content_length->value:0));
#endif
if(nullptr == event->request)
return;
// 根据事件类型处理(覆盖国标核心场景)
switch (event->type) {
// 1. 处理IPC的注册请求(Register)
case EXOSIP_REGISTRATION_SUCCESS: {
std::cout << "收到注册请求:" << event->request->req_uri << std::endl;
break;
}
// 2. 处理IPC对Invite的响应(推流确认)
case EXOSIP_CALL_ANSWERED: {
osip_body* body = nullptr;
osip_message_get_body (event->response, 0, &body);
string newBuffer;
Gb2312ToUtf8(body->body, newBuffer);
qDebug("body:\n%s",newBuffer.c_str());
eXosip_lock(m_excontext);
int ret = eXosip_call_send_ack(m_excontext, event->did, nullptr);
eXosip_unlock(m_excontext);
if(0 != ret) {
qDebug("调用eXosip_call_send_ack()失败,返回值:%d\n",ret);
return;
}
char ipcAddr[32];
char ipcSipId[32];
get_ipc_media_ip_from_sdp(event,ipcAddr,ipcSipId);
qDebug() << "IPC ip地址:"<<ipcAddr <<" sip id:"<<ipcSipId <<"同意推流请求,开始接收RTP流...cid:" << event->cid <<" did:" << event->did;
//缓存当前相机的call id dialog id 后面关闭流是会用
GBCameraManager::getInstance()->updateCameraInfo(ipcSipId,event->cid,event->did);
// 这里启动RTP接收器
// init_rtp_receiver(g_local_ip.c_str(), g_rtp_port); // 你的本地RTP接收端口
break;
}
// 3. 处理IPC断连(Bye)
case EXOSIP_CALL_CLOSED: {
std::cout << "IPC停止推流,会话关闭" << std::endl;
break;
}
case EXOSIP_CALL_PROCEEDING:
break;
case EXOSIP_CALL_MESSAGE_ANSWERED:
{
qDebug("收到 IPC 对 MESSAGE/INFO 的 200 OK");
}
break;
// 其他事件(可选处理)
case EXOSIP_REGISTRATION_FAILURE:
std::cerr << "IPC注册失败:" << event->ss_status << std::endl;
break;
case EXOSIP_CALL_REQUESTFAILURE:
std::cerr << "Invite请求失败:" << event->ss_status << std::endl;
break;
case EXOSIP_MESSAGE_NEW:
processMessage(event);
break;
default:
// 忽略其他无关事件
break;
}
}
void GBSipServer::processMessage(eXosip_event_t *osipEvent)
{
if (!osipEvent)
return;
if(MSG_IS_BYE(osipEvent->request))
{
qDebug("IS_BYE\n");
}
else if(MSG_IS_NOTIFY(osipEvent->request))
{
qDebug("IS_NOTIFY\n");
}
else if(MSG_IS_MESSAGE(osipEvent->request))
{
qDebug("IS_MESSAGE\n");
if (g_globalData.isRegister) {//IPC注册成功后,才去处理Message消息
parse_message_info(osipEvent);
}
}
else if (MSG_IS_REGISTER(osipEvent->request))
{
qDebug("IS_REGISTER\n");
//处理注册消息
parse_register_info(osipEvent);
}
else
{
qDebug("MESSAGE TYPE:%s\n",osipEvent->request->sip_method);
}
}
int get_ipc_media_ip_from_sdp(eXosip_event_t *evt, char *media_ip, char *sipId)
{
osip_message_t *msg = NULL;
sdp_message_t *sdp = NULL;
int ret = -1;
if (!evt || !media_ip || !sipId)
return -1;
// 1. 获取响应消息(ANSWERED 事件里取 response)
msg = evt->response;
if (!msg) {
qDebug("无消息体\n");
return -1;
}
osip_body* body = nullptr;
osip_message_get_body (evt->response, 0, &body);
sdp_message_init(&sdp);
// 2. 从消息 body 中解析 SDP
sdp_message_parse(sdp,body->body);
if (!sdp) {
qDebug("无SDP消息体\n");
sdp_message_free(sdp);
return -1;
}
// ======================================================
// 3. 【核心】从 SDP 中获取 IPC 媒体信息
if (sdp->c_connection->c_addr) {
strncpy(media_ip, sdp->c_connection->c_addr, 63);
} else {
qDebug("SDP无c=地址\n");
sdp_message_free(sdp);
return -1;
}
//从SDP中获取IPC 的sip id
if(sdp->o_username){
strcpy(sipId,sdp->o_username);
}else{
sdp_message_free(sdp);
return -1;
}
sdp_message_free(sdp);
return 0; // 成功拿到IPC媒体IP+端口
}
int get_ipc_ip_from_event_400(eXosip_event_t *evt, char *ip_buf, int buf_len)
{
osip_message_t *msg = NULL;
osip_contact_t *contact = NULL;
osip_via_t *via = NULL;
int ret = -1;
if (!evt || !ip_buf || buf_len < 16)
return -1;
memset(ip_buf, 0, buf_len);
// 优先取 request(REGISTER 请求)
msg = evt->request;
if (!msg)
msg = evt->response;
if (!msg)
return -1;
// --------------------------
// 方法1:从 Contact 头取 IP(最常用)
// --------------------------
if (osip_message_get_contact(msg, 0, &contact) == 0 &&
contact != NULL && contact->url != NULL && contact->url->host != NULL)
{
strncpy(ip_buf, contact->url->host, buf_len - 1);
ret = 0;
}
// --------------------------
// 方法2:Contact 取不到,从 Via 头取(网络路径 IP)
// --------------------------
else if (osip_message_get_via(msg, 0, &via) == 0 &&
via != NULL && via->host != NULL)
{
strncpy(ip_buf, via->host, buf_len - 1);
ret = 0;
}
// --------------------------
// 方法3:Via 取不到,从 From 头取
// --------------------------
else if (msg->from != NULL && msg->from->url != NULL && msg->from->url->host != NULL)
{
strncpy(ip_buf, msg->from->url->host, buf_len - 1);
ret = 0;
}
return ret;
}
void GBSipServer::parse_register_info(eXosip_event_t *e)
{
if (e == NULL || osip_strcasecmp(e->request->cseq->method, "REGISTER") != 0) {
qDebug("不是REGISTER请求 请求方法:%s\n",e->request->cseq->method);
return;
}
char* t = nullptr;
size_t len = 0;
osip_message_to_str(e->request, &t, &len);
qDebug() << QString("InfoBegin(type=%1\n%2\nInfoEnd").arg(e->type).arg(t);
osip_free(t);
GB_CameraInfo camInfo;
// 1. 提取设备ID(From头域)
osip_uri_t *from_uri = NULL;
from_uri = osip_from_get_url(e->request->from);
qDebug("注册客户端From地址:%s\n", from_uri->host);
qDebug("注册客户端FromID: %s",from_uri->username);
g_globalData.isRegister = true;
// 2. 提取Contact(客户端实际地址)
osip_uri_t *contact_uri = NULL;
contact_uri = osip_contact_get_url(e->request->from);
qDebug("注册客户端Contact ID: %s",from_uri->username);
qDebug("注册客户端Contact Port: %s",from_uri->port != nullptr ?from_uri->port:"nullptr");
string camIp = contact_uri->host;
char ip_buf[32] ;
get_ipc_ip_from_event_400(e,ip_buf,sizeof(ip_buf));
camIp = ip_buf;
qDebug() << "注册客户端IPC ip地址:" << camIp.c_str();
string camId = contact_uri->username;
strcpy(camInfo.ipAddr,camIp.c_str());
if(contact_uri->port){
camInfo.port = atoi(contact_uri->port);
}else{
camInfo.port = 5060;
}
strcpy(camInfo.deviceId,camId.c_str());
camInfo.status = 1;
//将注册的新IPC相机加入缓存中
GBCameraManager::getInstance()->setCameraInfo(camInfo);
// 3. 提取注册有效期
int expires = 0;
osip_header_t* header = nullptr;
osip_message_header_get_byname(e->request, "expires", 0, &header);
if(nullptr != header && nullptr != header->hvalue)
expires = atoi(header->hvalue);
else
qDebug("注册信息中没有expires字段\n");
qDebug("注册周期是expires=%d\n", expires);
// 4. 提取Call-ID
#if 0
char *call_id = NULL;
call_id = osip_call_id_get_host(e->request->call_id);
qDebug("注册Call-ID:%s\n", call_id );
#else
int cid = e->cid;
qDebug("cid=%d\n", cid);
int rid = e->rid;
qDebug("rid=%d\n", rid);
#endif
// 响应200 OK,完成IPC注册
osip_message_t *answer = nullptr;
eXosip_lock(m_excontext);
int result = eXosip_message_build_answer(m_excontext, e->tid, 200, &answer);
//增加过期字段
char buff[512];
sprintf(buff, "%d", expires );
osip_message_set_header(answer, "Expires", buff);
//增加时间字段
string dt;
//"%4d-%02d-%02dT%02d:%02d:%02d.%03d";
osip_message_set_header(answer, "Date", dt.c_str());
if (OSIP_SUCCESS != result)
eXosip_message_send_answer(m_excontext, e->tid, 400, nullptr);
else
eXosip_message_send_answer(m_excontext, e->tid, 200, answer);//发送消息体
eXosip_unlock(m_excontext);
}
void GBSipServer::parse_message_info(eXosip_event_t *osipEvent)
{
osip_body* body = nullptr;
osip_message_get_body (osipEvent->request, 0, &body);
string buffer = body->body;
bool is_error=false;
std::string cmdType("");
string newBuffer ="";
string eName = "";
if ( buffer.find("<Notify>") != string::npos )
{
eName = "Notify";
}
else if(buffer.find("<Response>") != string::npos )
{
eName = "Response";
}
else if(buffer.find("<Control>") != string::npos )
{
eName = "Control";
}
else if(buffer.find("<Query>") != string::npos )
{
eName = "Query";
}
else
{
qDebug("接收到MESSAGE报文,报文类别为(%s),未处理。报文内容为%s\n",eName.c_str(),buffer.c_str());
return ;
}
Gb2312ToUtf8(buffer,newBuffer);
//xml解析
XMLDocument doc;
XMLError ret = doc.Parse(newBuffer.c_str(),newBuffer.size());
if (ret != XML_SUCCESS) {
qDebug("xml 解析失败\n");
return ;
}
if (eName == "Notify") {
XMLElement* root = doc.FirstChildElement("Notify");
// 4. 读取各个字段(GB28181 心跳字段)
cmdType = root->FirstChildElement("CmdType")->GetText(); // Keepalive
const char* SN = root->FirstChildElement("SN")->GetText();
const char* DeviceID = root->FirstChildElement("DeviceID")->GetText(); // 设备ID
const char* Status = root->FirstChildElement("Status")->GetText(); // OK
}
if(cmdType == "Keepalive"&& eName == "Notify")//心跳
{
eXosip_message_send_answer(m_excontext,osipEvent->tid, 200, NULL);
qDebug("收到来自IPC的心跳包\n");
return ;
}
}
4.相机管理类
gbcameramanager.h
cpp
#ifndef GBCAMERAMANAGER_H
#define GBCAMERAMANAGER_H
#include <QObject>
#include <QMap>
#include <mutex>
#include "basic.h"
#include "gbplayerengine.h"
#include "gbplayersession.h"
class GBCameraManager : public QObject
{
Q_OBJECT
public:
static GBCameraManager* getInstance();
int getCameraList(GB_CameraList* out);
int startPlay(GB_PlayParam* param);
int stopPlay(const char* deviceId);
void clearAll();
/**
* @brief setCameraInfo 设置相机信息
* @param info
* @return
*/
int setCameraInfo(GB_CameraInfo info);
/**
* @brief updateCameraInfo 更新相机缓存信息
* @param ipc_id
* @param callId
* @return
*/
int updateCameraInfo(string ipc_id, int callId, int dialogId);
/**
* @brief inputStream 接收到IPC相机的媒体流
* @param data
* @param maxlen
* @param clientIp
* @return
*/
int inputStream(char *data, qint64 maxlen,string clientIp);
/**
* @brief getIdByDeviceId 获取当前IPC播放时的cid did
* @param deviceId
* @param cid
* @param did
*/
void getIdByDeviceId(string deviceId,int &cid,int &did);
private:
explicit GBCameraManager(QObject *parent = nullptr);
static GBCameraManager* instance;
mutex m_cameraMapMutex;
QMap<QString, GB_CameraInfo> m_cameraMap;
mutex m_sessionMapMutex;
QMap<QString, GBPlayerSession*> m_sessionMap;
};
#endif // GBCAMERAMANAGER_H
gbcameramanager.cpp
cpp
#include "gbcameramanager.h"
#include <QDebug>
#include "gbsipserver.h"
GBCameraManager* GBCameraManager::instance = nullptr;
GBCameraManager* GBCameraManager::getInstance()
{
if (!instance) instance = new GBCameraManager;
return instance;
}
GBCameraManager::GBCameraManager(QObject *parent) : QObject(parent) {}
int GBCameraManager::getCameraList(GB_CameraList* out)
{
if (!out) return -1;
out->count = 0;
lock_guard<mutex> lck(m_cameraMapMutex);
for (auto info : m_cameraMap) {
if (out->count >= 128) break;
out->cameras[out->count++] = info;
}
return 0;
}
int GBCameraManager::startPlay(GB_PlayParam* param)
{
QString id = param->deviceId;
if (!m_cameraMap.contains(id)) return -1;
if(m_cameraMap[id].playStatus == 1){
qDebug() << "当前相机正在播放";
return -1;
}
{
lock_guard<mutex> lck(m_sessionMapMutex);
if (m_sessionMap.contains(id)) {
m_sessionMap[id]->stop();
delete m_sessionMap.take(id);
}
GBPlayerSession* session = new GBPlayerSession(param);
if(session->initialize()){
session->start();
m_sessionMap.insert(id, session);
}
}
m_cameraMap[id].playStatus = 1;
std::string ipc_ip =param->devIp;
std::string ipc_id = param->deviceId;
int ipc_port = param->port;
GBSipServer::getInstance()->send_invite_to_ipc(ipc_ip,ipc_id,ipc_port);
return 0;
}
int GBCameraManager::stopPlay(const char* deviceId)
{
int cid = 0;
int did = 0;
getIdByDeviceId(deviceId,cid,did);
if(cid >0){
GBSipServer::getInstance()->stop_ipc_media_stream(cid,did);
}
QString id = deviceId;
{
lock_guard<mutex> lck(m_sessionMapMutex);
if (m_sessionMap.contains(id)) {
m_sessionMap[id]->stop();
delete m_sessionMap.take(id);
}
}
{
lock_guard<mutex> lck(m_cameraMapMutex);
if (m_cameraMap.contains(id)) {
m_cameraMap[id].playStatus = 0;
}
}
return 0;
}
void GBCameraManager::clearAll()
{
for (auto s : m_sessionMap) {
s->stop();
delete s;
}
m_sessionMap.clear();
{
lock_guard<mutex> lck(m_cameraMapMutex);
m_cameraMap.clear();
}
}
int GBCameraManager::setCameraInfo(GB_CameraInfo info)
{
lock_guard<mutex> lck(m_cameraMapMutex);
if(m_cameraMap.find(info.deviceId) != m_cameraMap.end()){
m_cameraMap[info.deviceId] = info;
}else{
m_cameraMap.insert(info.deviceId,info);
}
return 0;
}
int GBCameraManager::updateCameraInfo(string ipc_id, int callId,int dialogId)
{
lock_guard<mutex> lck(m_cameraMapMutex);
if(m_cameraMap.find(ipc_id.data()) != m_cameraMap.end()){
m_cameraMap[ipc_id.data()].cid = callId ;
m_cameraMap[ipc_id.data()].did = dialogId ;
}
return 0;
}
int GBCameraManager::inputStream(char *data, qint64 maxlen, string clientIp)
{
// qDebug() << "来自IPC :" << clientIp.data() <<"的媒体流";
string ipc_id = "" ;
//通过相机IP获取到对应的id
{
lock_guard<mutex> lck(m_cameraMapMutex);
for(auto elem:m_cameraMap){
if(elem.ipAddr == clientIp){
ipc_id = elem.deviceId;
break;
}
}
}
if(ipc_id == "")
return -1;
lock_guard<mutex> lck(m_sessionMapMutex);
if(m_sessionMap.find(ipc_id.data()) != m_sessionMap.end()){
if(m_sessionMap[ipc_id.data()]){
m_sessionMap[ipc_id.data()]->inputStream(data,maxlen);
}
}
return 0;
}
void GBCameraManager::getIdByDeviceId(string deviceId, int &cid, int &did)
{
lock_guard<mutex> lck(m_cameraMapMutex);
if(m_cameraMap.find(deviceId.data()) != m_cameraMap.end()){
cid = m_cameraMap[deviceId.data()].cid ;
did = m_cameraMap[deviceId.data()].did ;
}
}
5.播放会话类
gbplayersession.h
cpp
#ifndef GBPLAYERSESSION_H
#define GBPLAYERSESSION_H
#include <QThread>
#include <memory>
#include "gbplayerengine.h"
#include "PsParser.h"
#include "vlcstreamdecoder.h"
class GBPlayerSession : public QThread
{
Q_OBJECT
public:
explicit GBPlayerSession(GB_PlayParam* param);
~GBPlayerSession();
bool initialize();
bool isInitialize(){return m_isInitialize;}
void stop();
/**
* @brief inputStream
* @param data
* @param len
*/
void inputStream(const char *data , int len);
private:
static void onMediaData(PsParser::MediaType type, const uint8_t* data, int len, uint64_t timestamp,void* opaque);
void handleMediaData(PsParser::MediaType type,
const uint8_t* data,
int len,
uint64_t timestamp);
protected:
void run() override;
private:
GB_PlayParam m_param;
bool m_running = false;
PsParser *m_psParser;
QList<QByteArray> m_streamDataList;
std::unique_ptr<VLCStreamDecoder> m_decoder;
bool m_isInitialize = false;
};
#endif // GBPLAYERSESSION_H
gbplayersession.cpp
cpp
#include "gbplayersession.h"
#include <QDebug>
GBPlayerSession::GBPlayerSession(GB_PlayParam* param)
{
if (param) m_param = *param;
}
GBPlayerSession::~GBPlayerSession()
{
if(m_psParser){
delete m_psParser;
}
}
bool GBPlayerSession::initialize()
{
m_decoder = std::make_unique<VLCStreamDecoder>(this);
m_decoder->OpenStream((WId)m_param.renderWnd);
m_psParser = new PsParser();
m_isInitialize = true;
return true;
}
void GBPlayerSession::stop()
{
m_running = false;
wait();
}
void GBPlayerSession::inputStream(const char *data, int len)
{
if(m_streamDataList.size() > 80){
qDebug() << "IPC相机:" << m_param.devIp <<" 媒体流缓存超出上限,被丢弃";
return;
}
QByteArray byteArr(data,len);
m_streamDataList.push_back(byteArr);
}
void GBPlayerSession::onMediaData(PsParser::MediaType type, const uint8_t* data, int len, uint64_t timestamp, void *opaque)
{
// 从 opaque 拿回 this 指针
GBPlayerSession* self = (GBPlayerSession*)opaque;
self->handleMediaData(type,data,len,timestamp);
}
void GBPlayerSession::handleMediaData(PsParser::MediaType type, const uint8_t *data, int len, uint64_t timestamp)
{
if (type == PsParser::MEDIA_TYPE_VIDEO_H264)
{
#if 0
qDebug() << "[H264] 帧长度:" << len << " 时间戳:" << timestamp ;
// 这里可以直接送给解码器/保存文件/推流
if (len > 30) {
QByteArray ba((const char*)data, 30);
qDebug() << "数据十六进制:" << ba.toHex(' ');
}
#endif
QByteArray ba((const char*)data, len);
if(isInitialize()){
if (m_decoder) {
m_decoder->InputStream(ba.constData(), ba.size(),"h264");
} else {
qDebug() << "警告:m_decoder 为 NULL,但 isInitialize() 返回 true!";
}
}
}
else if (type == PsParser::MEDIA_TYPE_VIDEO_H265)
{
#if 0
qDebug() << "[H265] 帧长度:" << len << " 时间戳:" << timestamp;
if (len > 30) {
QByteArray ba((const char*)data, 30);
qDebug() << "数据十六进制:" << ba.toHex(' ');
}else {
QByteArray ba((const char*)data, len);
qDebug() << "数据十六进制:" << ba.toHex(' ');
}
#endif
QByteArray ba((const char*)data, len);
if(isInitialize()){
if (m_decoder) {
m_decoder->InputStream(ba.constData(), ba.size(),"h265");
} else {
qDebug() << "警告:m_decoder 为 NULL,但 isInitialize() 返回 true!";
}
}
}
else if (type == PsParser::MEDIA_TYPE_AUDIO_AAC)
{
qDebug() << "[AAC] 帧长度:" << len << " 时间戳:" << timestamp;
}
}
void GBPlayerSession::run()
{
m_running = true;
m_psParser->setOpaque(this);
m_psParser->setCallback(&GBPlayerSession::onMediaData);
while (m_running) {
// 1. 从UDP服务获取PS流
// 2. 解封装PS
// 3. H264/H265解码
// 4. 渲染到窗口句柄
if(m_streamDataList.size() == 0){
msleep(10);
continue;
}
QByteArray byteArr = m_streamDataList.front();
m_streamDataList.pop_front();
m_psParser->parseRtpPsData((const uint8_t*)byteArr.data(),byteArr.length());
// msleep(10);
}
}
6.PS流解析类
cpp
#ifndef GBMEDIACLIENT_PSPARSER_H
#define GBMEDIACLIENT_PSPARSER_H
#include <iostream>
#include <cstdint>
#include <cstring>
#include <vector>
#include <QObject>
// 禁用命名空间污染,仅使用必要的类型
using namespace std;
/**
* @brief PS 封装格式解析器
* @note 适配:RTP over UDP + PS 封装(海康/大华/IPC网络摄像头通用)
* 输出标准 H264/H265 + AAC 裸码流
*/
class GBPlayerSession;
class PsParser
{
public:
enum MediaType
{
MEDIA_TYPE_VIDEO_H264 = 0x01,
MEDIA_TYPE_VIDEO_H265 = 0x02,
MEDIA_TYPE_AUDIO_AAC = 0x10,
MEDIA_TYPE_UNKNOWN
};
using OnMediaDataCallback = void(*)(MediaType type, const uint8_t* data, int len, uint64_t timestamp,void* opaque);
const int MAX_CACHE_SIZE = 2*1024 * 1024; // 最大缓存 1MB
public:
PsParser() : m_callback(nullptr), m_lastTimestamp(0) {
}
~PsParser() = default;
void setOpaque(void *ctx){m_ctx = (GBPlayerSession*)ctx;}
// 设置回调
void setCallback(OnMediaDataCallback cb) { m_callback = cb; }
// 入口:每次收到UDP数据就传入(自动缓存+拼包+找完整PS包)
void parseRtpPsData(const uint8_t* data, int len)
{
if (!data || len <= 0)
return;
// 1. 剥离 RTP 固定头 12 字节
if (len < 12)
return;
const uint8_t* psData = data + 12;
int psLen = len - 12;
// 2. 【仅追加数据,绝不主动清理未解析数据】
appendCache(psData, psLen);
// 3. 循环解析:只处理【完整PS包】
while (true)
{
size_t cacheSize = m_cache.size();
const uint8_t* cacheBuf = m_cache.data();
// --------------------------
// 步骤1:找【第一个 PS 包头】00 00 01 BA
// --------------------------
int firstBaPos = findPsHeader(cacheBuf, (int)cacheSize);
if (firstBaPos < 0)
{
// 没找到第一个包头,保留所有数据,退出
break;
}
// 如果包头不在起始位置,移除前面无效脏数据(不影响音视频)
if (firstBaPos > 0)
{
m_cache.erase(m_cache.begin(), m_cache.begin() + firstBaPos);
continue;
}
// --------------------------
// 步骤2:从第一个包头后,找【下一个 PS 包头】00 00 01 BA
// --------------------------
int secondBaPos = -1;
if (cacheSize > 14)
{
secondBaPos = findPsHeader(cacheBuf + 14, (int)(cacheSize - 14));
if (secondBaPos != -1)
{
secondBaPos += 14; // 偏移回原始缓存位置
}
}
// 没找到下一个包头 → 数据不完整,等待下次接收
if (secondBaPos == -1)
{
break;
}
// --------------------------
// 步骤3:两个包头之间 = 【完整PS包】
// --------------------------
int completePsLen = secondBaPos;
if (completePsLen < 14 || completePsLen > MAX_CACHE_SIZE)
{
// 异常包,丢弃当前包头
m_cache.erase(m_cache.begin(), m_cache.begin() + 4);
continue;
}
// --------------------------
// 步骤4:解析完整PS包
// --------------------------
parseCompletePs(cacheBuf, completePsLen);
// --------------------------
// 步骤5:【只删除已解析完成的PS数据】
// 剩余未解析数据 100% 保留
// --------------------------
m_cache.erase(m_cache.begin(), m_cache.begin() + completePsLen);
}
// 缓存溢出保护(极端情况)
if (m_cache.size() > MAX_CACHE_SIZE)
{
m_cache.clear();
}
}
private:
// 帧拼接缓存(关键:解决H265画面下半部分异常)
std::vector<uint8_t> m_frameCache;
uint64_t m_lastPts = 0;
MediaType m_lastMediaType = MEDIA_TYPE_UNKNOWN;
MediaType m_currentFrameType = MEDIA_TYPE_UNKNOWN;
uint64_t m_currentPts = 0;
GBPlayerSession *m_ctx;
private:
// -------------------------- 核心优化:解析完整PS包 --------------------------
void parseCompletePs(const uint8_t* data, int len)
{
#if 0
if (len > 30) {
for (int i = 0;i<30;i++) {
fprintf(stdout,"%02x ", data[i]);
}
fprintf(stdout,"\n");
h264WriteFrame((unsigned char*)data,len);
}
return ;
#endif
const uint8_t* p = data;
const uint8_t* end = data + len;
// 校验 PS 包头 00 00 01 BA
if (len < 14 || p[0] != 0x00 || p[1] != 0x00 || p[2] != 0x01 || p[3] != 0xBA)
return;
// 提取时间戳
uint64_t pts = getPtsFromPsHeader(p);
m_lastTimestamp = pts;
p += 14;
// 【重要】清空上一帧缓存
m_frameCache.clear();
m_currentFrameType = MEDIA_TYPE_UNKNOWN;
// 遍历解析包
while (p + 4 <= end)
{
// 必须是起始码 00 00 01
if (p[0] != 0x00 || p[1] != 0x00 || p[2] != 0x01)
{
p++;
continue;
}
uint8_t type = p[3];
// ==============================================
// 关键修复:遇到 0xBB (PS系统头) 直接跳过,不退出!
// ==============================================
if (type == 0xBB)
{
// 跳过 PS 系统头:6字节固定头 + 长度
if (p + 6 > end) break;
int sysLen = (p[4] << 8) | p[5];
p += (6 + sysLen);
continue;
}
if (type == 0xBC) // PSM 包
{
int slen = (p[4] << 8) | p[5];
p += 6 + slen;
continue;
}
// ==============================================
// 只有视频(0xE0/0xE1)、音频(0xC0)才解析PES
// ==============================================
if (type == 0xE0 || type == 0xE1 || type == 0xC0)
{
parsePesPacket(p, end - p, type);// 不 break!继续解析下一个PES!
}
// 其他未知类型,跳过
else
{
p += 4;
}
}
// ======================================================================
// PS包解析完成 → 帧拼接完成 → 一次性输出完整帧
// ======================================================================
if (!m_frameCache.empty() && m_callback) {
m_callback(m_currentFrameType,
m_frameCache.data(),
(int)m_frameCache.size(),
m_currentPts,
m_ctx
);
}
}
// 查找 PS 包头 00 00 01 BA
int findPsHeader(const uint8_t* data, int len)
{
for (int i = 0; i < len - 3; i++)
{
if (data[i] == 0x00 && data[i+1] == 0x00 && data[i+2] == 0x01 && data[i+3] == 0xBA)
return i;
}
return -1;
}
// 计算一个完整 PS 包的长度(从包头开始)
int getPsPacketLength(const uint8_t* ba, int bufLen)
{
if (bufLen < 14) return -1;
int packLen = 14;
const uint8_t* p = ba + 14;
while (true)
{
if (p + 4 > ba + bufLen) break;
if (p[0] == 0x00 && p[1] == 0x00 && p[2] == 0x01 && p[3] == 0xBB)
{
// PS 系统头
int sysLen = (p[4] << 8) | p[5];
packLen += 6 + sysLen;
p += 6 + sysLen;
}
else if (p[0] == 0x00 && p[1] == 0x00 && p[2] == 0x01)
{
// PES 包
if (p + 6 > ba + bufLen) break;
int pesLen = (p[4] << 8) | p[5];
packLen += 6 + pesLen;
p += 6 + pesLen;
}
else
{
p++;
}
if (packLen > bufLen || packLen > MAX_CACHE_SIZE)
return -1;
}
return packLen;
}
// 解析 PES
bool parsePesPacket(const uint8_t*& p, int remain, uint8_t stream)
{
// PES 最小长度校验
if (remain < 9) {
return false;
}
// 记录当前指针位置(用于长度计算)
const uint8_t* pesStart = p;
// 1. 跳过 PES 起始码 00 00 01 xx (4字节)
p += 4;
// remain -= 4;
// 2. 读取 PES 包总长度(2字节)
uint16_t pes_total_len = ((uint16_t)p[0] << 8) | p[1];
p += 2;
// remain -= 2;
if (pes_total_len == 0) {
// 未指定长度(视频帧允许),直接用剩余长度
pes_total_len = remain - 6;
}
// 3. 固定 PES 头 3 字节
uint8_t pes_header_len = p[2]; // 头部附加长度
p += 3;
// remain -= 3;
// 4. 计算完整头部长度
int full_header_len = 3 + pes_header_len;
if (full_header_len > pes_total_len) {
return false;
}
p += pes_header_len;
// remain -= pes_header_len;
// ==============================================
// 【核心修复】正确计算负载长度(解决H265画面异常)
// ==============================================
int payload_len = pes_total_len - full_header_len;
// 安全边界检查
if (payload_len <= 0) {
return true;
}
// ==============================================
// 正确识别 H264 / H265 (严格区分)
// ==============================================
if (m_currentFrameType == MEDIA_TYPE_UNKNOWN) {
MediaType type = MEDIA_TYPE_UNKNOWN;
if (stream == 0xE0 || stream == 0xE1) {
if (payload_len >= 4) {
bool isH264 = false;
bool isH265 = false;
// 3字节起始码 00 00 01
if (p[0] == 0x00 && p[1] == 0x00 && p[2] == 0x01) {
uint8_t nal_unit = p[3] & 0x1F;
// H264 NAL
if (nal_unit == 0x01 || nal_unit == 0x05 || nal_unit == 0x07 || nal_unit == 0x08) {
isH264 = true;
}
// H265 NAL (0x40~0x7F)
else {
uint8_t nal_hevc = (p[3] >> 1) & 0x3F;
if (nal_hevc >= 0 && nal_hevc <= 0x3F) {
isH265 = true;
}
}
}
// 4字节起始码 00 00 00 01
else if (p[0] == 0x00 && p[1] == 0x00 && p[2] == 0x00 && p[3] == 0x01) {
uint8_t nal_unit = p[4] & 0x1F;
// H264
if (nal_unit == 0x01 || nal_unit == 0x05 || nal_unit == 0x07 || nal_unit == 0x08) {
isH264 = true;
}
// H265
else {
uint8_t nal_hevc = (p[4] >> 1) & 0x3F;
if (nal_hevc >= 0 && nal_hevc <= 0x3F) {
isH265 = true;
}
}
}
if (isH264) type = MEDIA_TYPE_VIDEO_H264;
if (isH265) type = MEDIA_TYPE_VIDEO_H265;
}
}
// AAC 音频识别
else if (stream == 0xC0) {
if (payload_len >= 2 && (p[0] == 0xFF) && ((p[1] & 0xF0) == 0xF0)) {
type = MEDIA_TYPE_AUDIO_AAC;
}
}
m_currentFrameType = type;
}
// ------------------------------
// 视频:缓存拼接(不立即输出)
// ------------------------------
if (m_currentFrameType == MEDIA_TYPE_VIDEO_H264 || m_currentFrameType == MEDIA_TYPE_VIDEO_H265) {
if (m_frameCache.empty()) {
m_currentPts = m_lastTimestamp;
}
// 追加到帧缓存
m_frameCache.insert(m_frameCache.end(), p, p + payload_len);
}
// ------------------------------
// 音频:直接输出
// ------------------------------
else if (m_currentFrameType == MEDIA_TYPE_AUDIO_AAC && m_callback) {
m_callback(m_currentFrameType, p, payload_len, m_lastTimestamp,m_ctx);
}
// 指针后移
p += payload_len;
return true;
}
// PTS 提取
uint64_t getPtsFromPsHeader(const uint8_t* ba)
{
uint64_t pts = 0;
pts |= ((uint64_t)(ba[4] & 0x38)) << 27;
pts |= ((uint64_t)(ba[4] & 0x07)) << 28;
pts |= ((uint64_t)ba[5]) << 20;
pts |= ((uint64_t)(ba[6] & 0xF8)) << 12;
pts |= ((uint64_t)(ba[6] & 0x07)) << 13;
pts |= ((uint64_t)ba[7]) << 5;
pts |= ((uint64_t)(ba[8] & 0xF8) >> 3);
return pts / 90;
}
// 追加缓存
void appendCache(const uint8_t* data, int len)
{
if (m_cache.size() + len > MAX_CACHE_SIZE)
m_cache.clear();
m_cache.insert(m_cache.end(), data, data + len);
}
private:
vector<uint8_t> m_cache; // 数据拼接缓存
OnMediaDataCallback m_callback;
uint64_t m_lastTimestamp;
};
#endif //GBMEDIACLIENT_PSPARSER_H
四 运行日志
1.相机注册日志
SIP Server启动成功,监听:192.168.22.53:5060: UDP
2026-04-09 13:55:29,737 INFO [default] SIP Server启动成功,监听:192.168.22.53:5060: UDP
2026-04-09 13:55:29,737 INFO [default] SIP服务 UDP服务 初始化成功
IS_REGISTER
"InfoBegin(type=23\nREGISTER sip:11010801002000000001@1101080100 SIP/2.0\r\nVia: SIP/2.0/UDP 192.168.250.42:5060;rport=5060;branch=z9hG4bK850945342\r\nFrom: <sip:11010801131310000009@1101080100>;tag=387350747\r\nTo: <sip:11010801131310000009@1101080100>\r\nCall-ID: 1348197467\r\nCSeq: 1 REGISTER\r\nContact: <sip:11010801131310000009@192.168.250.42:5060>\r\nMax-forwards: 70\r\nUser-agent: IP Camera\r\nExpires: 3600\r\nContent-Length: 0\r\n\r\n\nInfoEnd"
注册客户端From地址:1101080100
注册客户端FromID: 11010801131310000009
注册客户端Contact ID: 11010801131310000009
注册客户端Contact Port: nullptr
注册客户端IPC ip地址: 192.168.250.42
注册周期是expires=3600
cid=0
rid=0
IS_MESSAGE
收到来自IPC的心跳包
2.相机播放/关闭日志
选中播放相机ID: 11011401111316000012
VLC 打开流成功,窗口句柄: 69206041
【调试】m_excontext 指针有效: 0x55afd5c375c0
Invite请求发送成功,等待IPC响应...
IPC: 192.168.250.114 Invite请求发送成功,等待IPC响应... call id: 1
body:
v=0
o=11011401111316000012 0 0 IN IP4 192.168.250.114
s=Play
i=VCam Live Video
c=IN IP4 192.168.250.114
t=0 0
m=video 10048 RTP/AVP 96
a=sendonly
a=rtpmap:96 PS/90000
a=streamprofile:0
a=streamnumber:0
y=0000000000
f=v/0/0/0/0/0a/0/0/0
IPC ip地址: 192.168.250.114 sip id: 11011401111316000012 同意推流请求,开始接收RTP流...cid: 1 did: 2
设置当前播放器支持解码类型为: h264
libva info: VA-API version 1.7.0
libva info: Trying to open /usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so
libva info: Found init function __vaDriverInit_1_7
libva info: va_openDriver() returns 0
Failed to open VDPAU backend libvdpau_va_gl.so: 无法打开共享对象文件: 没有那个文件或目录
发送 BYE 成功 → IPC 相机已停止推流
VLC 流已关闭
收到 IPC 对 MESSAGE/INFO 的 200 OK