车载视频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;
}

 

 

posted @ 2024-01-19 14:42  飞翔天空energy  阅读(1570)  评论(4编辑  收藏  举报