车载视频JT1078协议视频接入(C++)
把之前做的JT1078协议车载视频接入进行文档整理如下:
-----------------------------------------------------------------------------------------------
一。背景;
平台能够通过jt808协议接入车辆GPS定位信息的基础上,扩展车载视频JT1078协议的接入。实现车辆位置信息和视频信息的可视化。该功能可广泛应用于工厂、园区、物流运输等行业,实现对客运、货运、出租车等营运车辆的实时监控。
二。技术架构:
整体架构采用自研视频流媒体平台。通过JT808协议进行信令管理,包括车辆注册、车辆鉴权、位置信息上报、请求视频、关闭视频等信令交互,实现车辆的基本信息注册和位置信息接入,视频控制指令的下发。通过JT1078流媒体服务解析车载终端上传的RTP视频流数据(包含H264视频和AAC音频),将解析后的音视频数据封装为rtmp格式推送到流媒体服务器。zlmediakit流媒体服务器实现rtmp转为flv或者hls等前端页面可以直接播放的视频流格式。
目前只实现了实时视频的接入。历史视频、云台控制、语音对讲等功能在JT1078协议中有对应的规范,后续可扩展实现。
三。模块描述:
1.JT808信令服务:在原有的JT808服务之上扩展了对9101和9102消息类型的支持,能够实现对实时视频的传输控制指令。
2.JT1078流媒体服务:启动TCP服务,接收车载终端上传的RTP视频流数据,根据JT1078协议对RTP数据包的H264视频帧进行解析,将视频帧封装为rtmp协议后推送到流媒体服务器。暂时只支持H264视频帧,未支持H265视频、音频。
3.zlmediakit流媒体服务器:开源的流媒体服务器,实现多种视频协议的接入和输出,能够提供前端页面可以直接播放的flv或hls格式的视频流。
4.视频流管理后台:云视频的管理后台,主要功能为视频设备管理、视频流管理、播放控制、云台控制、第三方视频平台接入等功能。
四。调试工具:
1.一个好用的车载终端模拟工具:CamClient。既可以模拟808的注册和GPS定位上报,也可以通过配置RTSP视频流来模拟车载视频推送。
2.报文解析工具: https://jttools.smallchi.cn/jt808,可以用来分析或组装报文。
3.TCP Server模拟工具。在JT808服务端模拟器上发送9101请求视频流命令:7E 91 01 00 11 01 80 21 54 36 03 00 00 09 31 32 37 2E 30 2E 30 2E 31 2A 1C 00 00 01 01 00 54 7E。设备端会将视频流推送到指定的流媒体服务127.0.0.1:10780端口
五。视频流RTP报文解析,关键代码:
1.JT1078Server.h
#include <memory> #include <string> #include <mutex> #include <unordered_map> #include "net/TcpServer.h" #include "net/EventLoop.h" namespace xop { enum JT1078_SUBMARK { eAtomic, eFirst, eLast, eIntermediate, eUnSupportSubMark, }; enum JT1078_CODEC_TYPE { eG711A, eG711U, eAAC, eAdpcm, eH264, eH265, eUnSupportCodingType, }; enum JT1078_STREAM_TYPE { eVideoI, eVideoP, eVideoB, eAudio, ePassthrough, eUnSupportDataType, }; struct JT1078_RTP_HEADER { uint32_t DWFramHeadMark; //帧头标识 uint8_t V2 : 2; //固定为2 uint8_t P1 : 1; //固定为0 uint8_t X1 : 1; //RTP头是否需要扩展位,固定为0 uint8_t CC4 : 4; //固定为1 uint8_t M1 : 1; //标志位,确定是否是完整数据帧的边界 uint8_t PT7 : 7; //负载类型 uint16_t WdPackageSequence; //RTP数据包序号每发送一个RTP数据包序列号加1 uint8_t BCDSIMCardNumber[6]; //SIM卡号 uint8_t Bt1LogicChannelNumber; //逻辑通道号 uint8_t DataType4 : 4; //数据类型 uint8_t SubpackageHandleMark4 : 4; //分包处理标记 uint64_t Bt8timeStamp; //时间戳 uint16_t WdLastIFrameInterval; //与上一帧的时间间隔 uint16_t WdLastFrameInterval; //与上一帧的时间间隔 uint16_t WdBodyLen; //数据体长度 JT1078_CODEC_TYPE CodecType; JT1078_STREAM_TYPE StreamType; JT1078_SUBMARK SubMark; std::string SimCode; }; struct JT1078_VIDEO_CHANNEL { std::string simCode = ""; int channel = 0; int mH264Len = 0; bool mIsComplete = false; SOCKET clientSocket = 0; char* H264Data = nullptr; }; class JT1078Server : public TcpServer { public: typedef std::function<void(char* frame, int size)> VideoFrameCallback; static std::shared_ptr<JT1078Server> Create(xop::EventLoop* loop); std::string AddToken(std::string simCode, int channel); void SetFrameCallBack(const VideoFrameCallback& cb) { video_cb_ = cb; }; ~JT1078Server(); private: JT1078Server(xop::EventLoop* loop); bool ParseJT1078(BufferReader& buffer, SOCKET sockfd); virtual TcpConnection::Ptr OnConnect(SOCKET sockfd); int ParseRtpHead(std::vector<uint8_t> buffer, JT1078_RTP_HEADER& jtHeader); int BcdToString(std::vector<uint8_t> const& in, std::string* out); uint8_t BcdToHex(uint8_t const& src); uint64_t ByteToU64BigEnd(uint8_t* ByteBuf); private: std::mutex mutex_; VideoFrameCallback video_cb_ = nullptr; std::unordered_map<std::string, std::shared_ptr<JT1078_VIDEO_CHANNEL>> video_channel_map_; }; }
2.JT1078Server.cpp
#include "JT1078Server.h" using namespace xop; using namespace std; #define H264_BUFF_LEN 1024000 std::shared_ptr<JT1078Server> JT1078Server::Create(EventLoop* loop) { std::shared_ptr<JT1078Server> server(new JT1078Server(loop)); return server; } std::string xop::JT1078Server::AddToken(std::string simCode, int channel) { std::string token = simCode + "_" + std::to_string(channel); std::lock_guard<std::mutex> locker(mutex_); if (video_channel_map_.find(token) != video_channel_map_.end()) { return token; } std::shared_ptr<JT1078_VIDEO_CHANNEL> videoChannel(new JT1078_VIDEO_CHANNEL()); videoChannel->simCode = simCode; videoChannel->channel = channel; videoChannel->mH264Len = 0; videoChannel->mIsComplete = false; videoChannel->clientSocket = 0; videoChannel->H264Data = new char[H264_BUFF_LEN]; memset(videoChannel->H264Data, 0, H264_BUFF_LEN); video_channel_map_.emplace(token, videoChannel); return token; } JT1078Server::JT1078Server(EventLoop* loop) : TcpServer(loop) { } JT1078Server::~JT1078Server() { } bool JT1078Server::ParseJT1078(BufferReader& buffer, SOCKET sockfd) { int nBuffLen = buffer.ReadableBytes(); printf("接收到 %d 条数据\n", nBuffLen); //uint8_t* packetData = (uint8_t*)buffer.Peek(); uint8_t* packetData = new uint8_t[nBuffLen]; memcpy(packetData, buffer.Peek(), nBuffLen); buffer.RetrieveAll(); int index = 0; int headPos = 0; bool bFirst = true; int nFirst = 0; while (index < nBuffLen) { bool bFindHeader = false; while (true) { if ((index + 4) > nBuffLen) { break; } if ((packetData[index] == 0x30) && (packetData[index + 1] == 0x31) && (packetData[index + 2] == 0x63) && (packetData[index + 3] == 0x64)) { bFindHeader = true; headPos = index; break; } else { index++; } } if (!bFindHeader) { printf("未查找到包头\n"); break; } if (bFirst && headPos > 0) { nFirst = headPos; } else { nFirst = 0; } bFirst = false; JT1078_RTP_HEADER jtRtpHeader; vector<uint8_t> headData; for (int index = 0; index < 34; index++) { headData.push_back(packetData[headPos + index]); } int ret = ParseRtpHead(headData, jtRtpHeader); if (ret < 0) { printf("解析报文头失败\n"); index++; continue; } int headLength = 30; int temp = 0; if (jtRtpHeader.StreamType == eVideoI || jtRtpHeader.StreamType == eVideoP || jtRtpHeader.StreamType == eVideoB) { temp = headLength; } else if (jtRtpHeader.StreamType == eAudio) { temp = headLength - 8; } else if (jtRtpHeader.StreamType == ePassthrough) { temp = headLength - 8 - 2 - 2; } index = headPos + temp + jtRtpHeader.WdBodyLen; std::string token = jtRtpHeader.SimCode + "_" + std::to_string(jtRtpHeader.Bt1LogicChannelNumber); mutex_.lock(); auto videoIter = video_channel_map_.find(token); if (videoIter == video_channel_map_.end()) { printf("未配置的通道:%s_%d\n", jtRtpHeader.SimCode.c_str(), jtRtpHeader.Bt1LogicChannelNumber); mutex_.unlock(); continue; } if (jtRtpHeader.CodecType == eH264) { int railDateLen = jtRtpHeader.WdBodyLen; if (index > nBuffLen && jtRtpHeader.WdBodyLen > 0) { // I帧时,有可能视频分包。视频流长度超过当前包总长度 railDateLen = nBuffLen - headPos - headLength; } if (railDateLen <= 0) { // 当前包中已无可以视频数据 mutex_.unlock(); continue; } if (nFirst > 0 && videoIter->second->clientSocket == sockfd) { //LOG_ERROR("**** nFirst: " << nFirst); memcpy(videoIter->second->H264Data + videoIter->second->mH264Len, (char*)packetData, nFirst); videoIter->second->mH264Len += nFirst; } videoIter->second->clientSocket = sockfd; // --- 解决数据分包问题 if (jtRtpHeader.SubMark == eAtomic) { memset(videoIter->second->H264Data, 0, H264_BUFF_LEN); memcpy(videoIter->second->H264Data, (char*)packetData + headPos + headLength, railDateLen); videoIter->second->mH264Len = railDateLen; videoIter->second->mIsComplete = true; } else if (jtRtpHeader.SubMark == eFirst) { memset(videoIter->second->H264Data, 0, H264_BUFF_LEN); memcpy(videoIter->second->H264Data, (char*)packetData + headPos + headLength, railDateLen); videoIter->second->mH264Len = railDateLen; videoIter->second->mIsComplete = false; } else if (jtRtpHeader.SubMark == eIntermediate) { memcpy(videoIter->second->H264Data + videoIter->second->mH264Len, (char*)packetData + headPos + headLength, railDateLen); videoIter->second->mH264Len += railDateLen; videoIter->second->mIsComplete = false; } else if (jtRtpHeader.SubMark == eLast) { memcpy(videoIter->second->H264Data + videoIter->second->mH264Len, (char*)packetData + headPos + headLength, railDateLen); videoIter->second->mH264Len += railDateLen; videoIter->second->mIsComplete = true; } if (videoIter->second->mIsComplete) { std::cout << "----------------------- 保存H264,数据长度:" << videoIter->second->mH264Len << std::endl; if (video_cb_) { video_cb_(videoIter->second->H264Data, videoIter->second->mH264Len); } } } mutex_.unlock(); } delete[]packetData; return true; } TcpConnection::Ptr JT1078Server::OnConnect(SOCKET sockfd) { auto conn = std::make_shared<TcpConnection>(event_loop_->GetTaskScheduler().get(), sockfd); conn->SetReadCallback([this, sockfd](xop::TcpConnection::Ptr conn, xop::BufferReader& buffer) { return this->ParseJT1078(buffer, sockfd); }); return conn; } int xop::JT1078Server::ParseRtpHead(std::vector<uint8_t> buffer, JT1078_RTP_HEADER& jtHeader) { if (buffer.size() < 30) { printf("帧长度不足,丢弃\n"); return -1; } if (buffer[0] != 0x30 || buffer[1] != 0x31 || buffer[2] != 0x63 || buffer[3] != 0x64) { printf("帧头错误,丢弃\n"); return -2; } jtHeader.V2 = (buffer[4] >> 6) & 0x03; jtHeader.P1 = (buffer[4] >> 5) & 0x01; jtHeader.X1 = (buffer[4] >> 4) & 0x01; jtHeader.CC4 = buffer[4] & 0x0F; jtHeader.M1 = (buffer[5] >> 7) & 0x01; jtHeader.PT7 = buffer[5] & 0x7F; switch (jtHeader.PT7) { case 6: jtHeader.CodecType = eG711A; break; case 7: jtHeader.CodecType = eG711U; break; case 19: jtHeader.CodecType = eAdpcm; break; case 98: jtHeader.CodecType = eH264; break; case 99: jtHeader.CodecType = eH265; break; default: printf("不支持的编码类型\n"); return -3; } jtHeader.WdPackageSequence = (buffer[6] << 8) + buffer[7]; memcpy(jtHeader.BCDSIMCardNumber, &buffer[0] + 8, 6); string simString; vector<uint8_t> simCode; for (int i = 0; i < 6; i++) { simCode.push_back(jtHeader.BCDSIMCardNumber[i]); } BcdToString(simCode, &simString); jtHeader.SimCode = "0" + simString; jtHeader.Bt1LogicChannelNumber = buffer[14]; jtHeader.DataType4 = (buffer[15] >> 4) & 0x0F; switch (jtHeader.DataType4) { case 0x00://I jtHeader.StreamType = eVideoI; break; case 0x01://P jtHeader.StreamType = eVideoP; break; case 0x02://B jtHeader.StreamType = eVideoB; break; case 0x03://音频 jtHeader.StreamType = eAudio; break; case 0x04://透传 jtHeader.StreamType = ePassthrough; break; default: printf("不支持的流类型\n"); return -4; } jtHeader.SubpackageHandleMark4 = buffer[15] & 0x0F; switch (jtHeader.SubpackageHandleMark4) { case 0x00: jtHeader.SubMark = eAtomic; break; case 0x01: jtHeader.SubMark = eFirst; break; case 0x02: jtHeader.SubMark = eLast; break; case 0x03: jtHeader.SubMark = eIntermediate; break; default: printf("不支持的分包处理标识\n"); return -5; } if (jtHeader.StreamType == eVideoI || jtHeader.StreamType == eVideoP || jtHeader.StreamType == eVideoB) { jtHeader.Bt8timeStamp = ByteToU64BigEnd(&buffer[0]+ 16); std::cout << "time:" << jtHeader.Bt8timeStamp << std::endl; jtHeader.WdLastIFrameInterval = (buffer[24] << 8) + buffer[25]; jtHeader.WdLastFrameInterval = (buffer[26] << 8) + buffer[27]; jtHeader.WdBodyLen = (buffer[28] << 8) + buffer[29]; } if (jtHeader.StreamType == eAudio) { jtHeader.Bt8timeStamp = ByteToU64BigEnd(&buffer[0] + 16); jtHeader.WdBodyLen = (buffer[24] << 8) + buffer[25]; } if (jtHeader.StreamType == ePassthrough) { jtHeader.WdLastIFrameInterval = (buffer[24] << 8) + buffer[25]; jtHeader.WdLastFrameInterval = (buffer[26] << 8) + buffer[27]; jtHeader.WdBodyLen = (buffer[28] << 8) + buffer[29]; } if (jtHeader.WdBodyLen > 950) { jtHeader.WdBodyLen = 950; } return 0; } int xop::JT1078Server::BcdToString(std::vector<uint8_t> const& in, std::string* out) { if (out == nullptr) return -1; out->clear(); size_t pos = 0; uint8_t tmp = BcdToHex(in[pos]); if (tmp / 10 == 0) { out->push_back(tmp % 10 + '0'); ++pos; } for (; pos < in.size(); ++pos) { tmp = BcdToHex(in[pos]); out->push_back(tmp / 10 + '0'); out->push_back(tmp % 10 + '0'); } return 0; } uint8_t xop::JT1078Server::BcdToHex(uint8_t const& src) { uint8_t temp; temp = (src >> 4) * 10 + (src & 0x0f); return temp; } uint64_t xop::JT1078Server::ByteToU64BigEnd(uint8_t* ByteBuf) { uint64_t u64Value = 0; for (uint8_t i = 0; i < 8; i++) { u64Value <<= 8; u64Value |= ByteBuf[i]; } return u64Value; }