摄像头接入

目前摄像头直播的方案主要有以下几种方式:

  1. rtsp方式接入,只能实现视频预览

  2. 国标协议接入,实现比较复杂,需要多实现SIP服务器

  3. 通过netsdk获取到视频码流,推流到流媒体服务器,通过wsflv,flv,hls等流媒体协议播放,H265不支持

一、采用方案

对比后最终采用了第三种方式,java使用jna的方式接入大华netsdk,获取到dav视频码流后去除大华头尾,拿到H264裸码流,通过javacv(对ffmpeg、opencv等库的封装)推送到流媒体服务器(选用了一个开源的node写的nms)

 

二、JNA

2.1 JNI java调用c的过程

若已有编译好的.dll/.so文件—>需先用是C语言另外写一个.dll/.so共享库,使用SUN规定的数据结构代替C语言的数据结构,调用已有的dll/so中公布的函数—>java中载入这个库—>java编写Native函数作为链接库中函数的代理

2.2 JNA

JNA是建立在JNI技术基础之上的一个java类库,它提供了一个动态的C语言编写的转发器,可以自动实现java和C的数据类型映射,开发者只需要使用java接口描述目标本地库的功能和结构,不需要再编写C动态链接库,可方便使用java直接访问动态链接库中的函数。此外JNA包括一个已与许多本地函数映射的平台库,以及一组简化本地访问的共用接口。JNA性能上有些微损失,无法实现c调用java。

2.3 windows下简单使用

(1))编译一个dll库,新建cal.cpp、cal.h,使用vs编译出dll文件(linux下编译so文件)

cal.h

extern "C"  __declspec(dllexport) int cal(int a, int b, int calType);

cal.cpp

#include "cal.h"
int cal(int a,int b,int calType) {
    if (calType == 1) {
        return a + b;
    }else{
        return a - b;
    }
}

(2)新建java项目,maven引入jna

<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna</artifactId>
    <version>4.5.1</version>
</dependency>

将上面编译出的cal.dll放到项目根路径,新建java接口描述dll库的功能和结构

import com.sun.jna.Library;
import com.sun.jna.Native;
​
public interface CalSDK extends Library {
    //实例由JNA通过反射自动生成,可以拓展为根据不同的系统加载不同的动态链接库
    CalSDK CalSDK_INSTANCE = (CalSDK)Native.loadLibrary("cal", CalSDK.class);
    //此方法为链接库中的方法
    int cal(int a, int b, int calType);
}
​

调用

public class Main {
    public static void main(String[] args) {
        System.out.println(CalSDK.CalSDK_INSTANCE.cal(1,2,1));
    }
}

2.4 类型映射

jna官方提供的

 

img

2.5 函数回调

以大华netsdk设备实时预览回调为例,c代码

// 实时监视数据回调函数原形
typedef void (CALLBACK *fRealDataCallBackEx)(
    LLONG lRealHandle, 
    DWORD dwDataType, 
    BYTE *pBuffer, 
    DWORD dwBufSize, 
    LONG param, 
    LDWORD dwUser);

java代码,可以在实现类的invoke方法里获取到视频流

 public interface fRealDataCallBackEx extends StdCallCallback {
        public void invoke(
            LLong lRealHandle, 
            int dwDataType, 
            Pointer pBuffer, 
            int dwBufSize, 
            int param, 
            Pointer dwUser);
    }

开启播放后掉接口设置回调

public void startRealplay(LLong loginHandle,Integer playSign,CameraPojo cameraPojo){
        //默认为主码流
        int streamType = 0;
        //开启预览
        LLong lRealHandle= NetSDKConfig.netSdk.CLIENT_RealPlayEx(
            loginHandle, 
            cameraPojo.getChannel(), 
            null, 
            streamType);
        //设置视频流回调 
        if(lRealHandle.longValue()!=0){
            try {     
                NetSDKConfig.netSdk.CLIENT_SetRealDataCallBackEx(
                    lRealHandle,
                    RealDataCallBack.getInstance(),
                    null, 
                    31)
            } catch (IOException e) {
                log.error(e.getMessage());
            }
        }
    }

2.6 指针

c中void * 指针,指向一个地址, 对应jna中的Pointer ,下面主动注册回调代码中就是将指针中的数据取出的一种方法

 public int invoke(NetSDKLib.LLong lHandle, final String pIp, final int wPort,
                      int lCommand, Pointer pParam, int dwParamLen,
                      Pointer dwUserData) {
​
        // 将 pParam 转化为序列号
        byte[] buffer = new byte[dwParamLen];
        pParam.read(0, buffer, 0, dwParamLen);
        String serNum = "";
        try {
            serNum = new String(buffer, "GBK").trim();
        } catch (UnsupportedEncodingException e) {
            log.error(e.getMessage());
        }
  }

 

2.7 结构体对应java类

类中字段顺序,需要与c++中对齐,不然报NoSuchFieldError错误!

c

// 定时录像配置信息
typedef struct tagCFG_RECORD_INFO
{
    int                 nChannelID;                 // 通道号(0开始)
    CFG_TIME_SECTION    stuTimeSection[WEEK_DAY_NUM][MAX_REC_TSECT]; // 时间表
    int                 nPreRecTime;                // 预录时间,为零时表示关闭(0~300)
    BOOL                bRedundancyEn;              // 录像冗余开关
    int                 nStreamType;                // 0-主码流,1-辅码流1,2-辅码流2,3-辅码流3
    int                 nProtocolVer;               // 协议版本号, 只读
​
    // 能力
    BOOL                abHolidaySchedule;          // 为true时有假日配置信息,bHolidayEn、stuHolTimeSection才有效;
    BOOL                bHolidayEn;                 // 假日录像使能TRUE:使能,FALSE:未使能
    CFG_TIME_SECTION    stuHolTimeSection[MAX_REC_TSECT];          // 假日录像时间表
} CFG_RECORD_INFO;

java

// 定时录像配置信息
    public static class CFG_RECORD_INFO extends Structure
    {
        public int                          nChannelID;                 // 通道号(0开始)
        public TIME_SECTION_WEEK_DAY_6[]    stuTimeSection = (TIME_SECTION_WEEK_DAY_6[])new TIME_SECTION_WEEK_DAY_6().toArray(WEEK_DAY_NUM); // 时间表
        public int                          nPreRecTime;                // 预录时间,为零时表示关闭(0~300)
        public int                          bRedundancyEn;              // 录像冗余开关
        public int                          nStreamType;                // 0-主码流,1-辅码流1,2-辅码流2,3-辅码流3
        public int                          nProtocolVer;               // 协议版本号, 只读
        public int                          abHolidaySchedule;          // 为true时有假日配置信息,bHolidayEn、stuHolTimeSection才有效;
        public int                          bHolidayEn;                 // 假日录像使能TRUE:使能,FALSE:未使能
        public TIME_SECTION_WEEK_DAY_6      stuHolTimeSection;          // 假日录像时间表
    }

 

//将c的指针转为jna结构体
public static void GetPointerDataToStruct(Pointer pNativeData, long OffsetOfpNativeData, Structure pJavaStu) {
    pJavaStu.write();
    Pointer pJavaMem = pJavaStu.getPointer();
    pJavaMem.write(0, pNativeData.getByteArray(OffsetOfpNativeData, pJavaStu.size()), 0,
                   pJavaStu.size());
    pJavaStu.read();
}

 

三、JAVACV

JavaCV 提供了在计算机视觉领域的封装库,包括:OpenCV、ARToolKitPlus、libdc1394 2.x 、PGR FlyCapture和FFmpeg。可以对音视频图片等进行处理。

3.1 javacv引入

<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>javacv</artifactId>
    <version>1.5.3</version>
</dependency>
<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>ffmpeg</artifactId>
    <version>4.2.2-1.5.3</version>
</dependency>
//可以配成根据不同的profile引入不同环境下的依赖
<dependency>
    <groupId>org.bytedeco</groupId>
    <artifactId>ffmpeg</artifactId>
    <version>4.2.2-1.5.3</version>
    <classifier>windows-x86_64</classifier>
    //linux环境下使用
    <!--<classifier>linux-x86_64</classifier>-->
</dependency>

3.2 使用

上面通过netsdk拿到的大华视频流经过去大华封装,拿到H264裸码流,判断为关键帧后,开始通过Java的管道流,将H264裸码流写入PipedOutputStream中,然后将对应的PipedInputStream当做参数传入到javacv的FFmpegFrameGrabber的构造方法中,通过JAVACV中的FFMPEG推到流媒体服务器

主要使用两个类FFmpegFrameGrabber(帧抓取器),FFmpegFrameRecorder(帧录制器/推流器)

/**
     * @return RtmpPush
     * @Title: from
     * @Description:分析视频流
     **/
    public RtmpPush from(){
        try {
            grabber = new FFmpegFrameGrabber(inputStream, 0);
            grabber.setVideoOption("vcodec", "copy");
            grabber.setFormat("h264");
            grabber.setOption("rtsp_transport", "tcp");
            grabber.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
            grabber.setVideoCodec(avcodec.AV_CODEC_ID_HEVC);
​
            // 用于检测大华sdk回调函数是否有数据流产生,从而避免没有数据流导致avformat_open_input()函数阻塞
            long stime = new Date().getTime();
            while (true) {
                Thread.sleep(100);
                if (new Date().getTime() - stime > 20*1000) {
                    log.error("[id:" + pojo.getSerNum() + " ch:" + pojo.getChannel() + "] 无视频流数据超过20s");
                    release();
                    return null;
                }
                if (inputStream.available() == 1024) {
                    log.error("[id:" + pojo.getSerNum() + " ch:"+ pojo.getChannel() +"]inputStream is 1024");
                    break;
                }
            }
//            grabber.setOption("stimeout", "20000");
            //此处在设备信号不好的时候会阻塞,暂时修改源码去掉org.bytedeco.ffmpeg.global.avcodec.class的synchronized
            grabber.start();
            isGrabberStartOver=true;
            log.error("[id:" + pojo.getSerNum() + " ch:" + pojo.getChannel() +"]Grabber strat over!");
            // 一般来说摄像头的帧率是25
            if (grabber.getFrameRate() > 0 && grabber.getFrameRate() < 100) {
                framerate = grabber.getFrameRate();
            } else {
                framerate = 25.0;
            }
        } catch (Exception e) {
           log.error(e.getMessage());
           release();
           return null;
        }
        return this;
    }
/**
     * @return void
     * @Title: push
     * @Description:推送视频
     **/
    public void push() {
        try {
            //封装为flv格式,通过rtmp协议推送到流媒体服务器
            this.recorder = new FFmpegFrameRecorder(pojo.getRtmp(), grabber.getImageWidth(), grabber.getImageHeight(),0);
            this.recorder.setInterleaved(true);
            this.recorder.setVideoOptions(this.videoOption);
            // 设置比特率
            this.recorder.setVideoBitrate(bitrate);
            // h264编/解码器
            this.recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
            // 封装flv格式
            this.recorder.setFormat("flv");
            this.recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
            // 视频帧率(保证视频质量的情况下最低25,低于25会出现闪屏)
            this.recorder.setFrameRate(framerate);
            // 关键帧间隔,一般与帧率相同或者是视频帧率的两倍
            this.recorder.setGopSize((int) framerate * 2);
            AVFormatContext fc = null;
            fc = grabber.getFormatContext();
            this.recorder.start(fc);
​
            AVPacket pkt = null;
            long dts = 0;
            long pts = 0;
​
            //空包时长大于30s退出
            Long startTs = CurrentTimeMillisClock.getInstance().now();
            Long nullTs;
            for (int no_frame_index = 0; err_index < 5 && !isExit; ) {
                if (exitcode == 1) {
                    break;
                }
                pkt = grabber.grabPacket();
​
                if (pkt == null || pkt.size() <= 0 || pkt.data() == null) {
                    if(no_frame_index==0){
                        log.error("[id:" + pojo.getSerNum() + " ch:" + pojo.getChannel() +"]pkt is null,continue...");
                    }
                    nullTs = CurrentTimeMillisClock.getInstance().now();
                    //空包记录次数跳过
                    no_frame_index++;
                    exitcode = 2;
                    if(nullTs-startTs>30*1000){
                        break;
                    }
                    Thread.sleep(100);
                    continue;
                }
                startTs = CurrentTimeMillisClock.getInstance().now();
                if(no_frame_index!=0){
                    log.error("pkt is not null");
                }
                no_frame_index=0;
                // 过滤音频
                if (pkt.stream_index() == 1) {
                    continue;
                } else {
                    // 矫正sdk回调数据的dts,pts每次不从0开始累加所导致的播放器无法续播问题
                    pkt.pts(pts);
                    pkt.dts(dts);
                    err_index += (recorder.recordPacket(pkt) ? 0 : 1);
                }
​
                // pts,dts累加
                timebase = grabber.getFormatContext().streams(pkt.stream_index()).time_base().den();
                pts += timebase / (int) framerate;
                dts += timebase / (int) framerate;
                // 将缓存空间的引用计数-1,并将Packet中的其他字段设为初始值。如果引用计数为0,自动的释放缓存空间。
                av_packet_unref(pkt);
            }
        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            release();
        }
    }

 

四、实现效果

配制好摄像头,部署好nms之后,运行项目,等待摄像头主动注册上来 在这里插入图片描述 swagger调用实现的开启视频流的接口,将主码流的视频推送到nms 在这里插入图片描述 将推送成功后的flv地址复制,在vlc播放网络串流查看 在这里插入图片描述 同时也可以在其他流媒体播放器上播放,页面可以使用b站的flv.js等播放。可以在nms的管理页面也可以看到这一视频流。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zb95731

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值