大华监控对接

1. 下载大华sdk

1.1 下载流媒体服务器

1.2 引用

  • maven引入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>
<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>

1.3 再windows下引用大华sdk的dll文件(解决java打成jar包无法读取dll文件)

  • 把大华sdk中的dll文件复制到 C:\Windows\System32中
  • java引用dll文件会从(Windows\System32)读取
  • 只适合windows系统,linux系统为.so文件
  • C:\Windows\System32 大都存储dll文件

1.4 配置大华平台的设备

  • 把中心服务器和监控设备都插在交换机网口,存储服务器插再中心服务器网口
  • 192.168.1.108一般为中心服务器端口 192.168.1.109一般为存储服务器端口
  • 监控设备默认为和中心服务器端口一样需要登大华平台进行修改ip地址(登录地址为当前设备ip,不能重复)
  • 可以登上中心服务器下载 Dss客户端查看实时视频.
  • 下载ConfigTool软件搜索设备信息(方便调试设备)

1.5 在大华平台配置设备

  • 登录智慧园区综合管理平台在设备管理中的基础应用----设备管理-----编码器中新增存储服务器设备然后把摄像头都注册到存储设备中.
  • 注册到存储设备中的摄像头通道为存储设备编码+100,如果有2个设备一个为1000010100,1000010101 最后的0代表通道号,依次递增

2. 开始根据sdk开发

2.1 在大华平台配置设备

  • 所有大华设备和调试都在线了大华平台可以播放实时视频,开启调试sdk
  • idea 在libraries设置native的访问目录
  • 大华给的测试类TestDPSDKMain其中核心类为IDpsdkCore,这个类中都是大华Native的方法
  • OnCreate() 初始化sdk方法;
  • OnLogin() 登录中心服务器;
  • LoadGroup() 加载组织结构;
  • GetGroupStr() 获取组织结构串(如果需要则用解析xml);
  • GetReal () 请求开启实时视频;
  • GetExternUrl() 获取实时视频 rtsp地址
  • CloseReal() 关闭流
  • startDownLoadRecordByTime() 开启录像下载请求Node-Media-Server.zip
  • app.OnLogout() 登出;
  • app.OnDestroy() 释放内存 (释放内存很重要,服务关后需要调用)

2.2 进行拉流,推流,转封装成rtmp中flv格式

参考地址 -https://blog.csdn.net/qq_40741855/article/details/103878683;
建议自己测试的时候可以下载 ffmpeg 通过cmd命令进行推流测试,javacv底层也是用
ffmpeg进行操作,进行编解码
命令:
ffmpeg -re -i ./video.mp4 -c copy -f flv rtmp://localhost:1935/live/STREAM_NAME #把MP4转换成rtmp流
ffmpeg -re -rtsp_transport tcp -i "rtsp://192.168.1.108:9090/dss/monitor/param?cameraid=1000003%240&substream=1" -c copy -f flv "rtmp://localhost:1935/live/STREAM_NAME" #rtsp转换成rtmp流

public class CameraPush {


    FFmpegFrameGrabber grabber = null;
    FFmpegFrameRecorder record = null;
    int width = -1, height = -1;

    // 视频参数
    protected int audiocodecid;
    protected int codecid;
    // 帧率
    protected double framerate;
    // 比特率
    protected int bitrate;

    // 退出状态码:0-正常退出;1-手动中断
    protected int exitcode;

    public void setExitcode(int exitcode) {
        this.exitcode = exitcode;
    }

    public int getExitcode() {
        return exitcode;
    }

    /**
     * 选择视频源.
     */
    public CameraPush from(String src) throws Exception {
        // 采集器
        try {
            grabber = new FFmpegFrameGrabber(src);
            if(src.indexOf("rtsp")>=0) {
                //采用tcp协议 udp会丢包
                grabber.setOption("rtsp_transport","tcp");
                // 设置采集器构造超时时间(单位微秒,1秒=1000000微秒)
                grabber.setOption("stimeout", "2000000");
            }
            // 开始之后ffmpeg会采集视频信息,之后就可以获取音视频信息
            grabber.start();
            width = grabber.getImageWidth();
            height = grabber.getImageHeight();
            if(width == 0 && height == 0){
                System.err.println("拉流超时");
                return null;
            }

            // 视频参数
            audiocodecid = grabber.getAudioCodec();
            codecid = grabber.getVideoCodec();
            framerate = grabber.getVideoFrameRate();
            bitrate = grabber.getVideoBitrate();
            return this;
        } catch (FrameGrabber.Exception e) {
            e.printStackTrace();
            log.error("拉流失败");
        }
        return this;

    }

    /**
     * 选择输出.
     */
    public CameraPush to(String out) throws IOException {
        try {
            // 推流器
            record = new FFmpegFrameRecorder(out, width, height);
            // 画面质量参数,0~51;18~28是一个合理范围
            record.setVideoOption("crf", "28");
            record.setGopSize(2);
            record.setFrameRate(framerate);
            record.setVideoBitrate(bitrate);

            AVFormatContext fc = null;
            if (out.indexOf("rtmp") >= 0 || out.indexOf("flv") > 0) {
                // 封装格式flv
                record.setFormat("flv");
                record.setAudioCodecName("aac");
                record.setVideoCodec(codecid);
                fc = grabber.getFormatContext();
            }
            record.start(fc);
        } catch (FrameRecorder.Exception e) {
            e.printStackTrace();
            log.error("取流转封装失败");
        }
        return this;
    }

    /**
     * 转封装.
     */
    public CameraPush go() throws IOException {

        //采集或推流导致的错误次数
        long err_index = 0;

        //将探测时留下的数据帧释放掉,以免因为dts,pts的问题对推流造成影响
        grabber.flush();

        //连续五次没有采集到帧则认为视频采集结束,程序错误次数超过5次即中断程序
        for(int no_frame_index=0; no_frame_index<5 || err_index < 5;) {

            AVPacket pkt=null;

            if (exitcode == 1) {
                break;
            }

            try {
                //没有解码的音视频帧
                pkt=grabber.grabPacket();
                if(pkt==null||pkt.size()<=0||pkt.data()==null) {
                    //空包记录次数跳过
                    no_frame_index++;
                    err_index++;
                    continue;
                }
                //不需要编码直接把音视频帧推出去, 如果失败err_index自增1
                err_index += (record.recordPacket(pkt) ? 0 : 1);
                av_packet_unref(pkt);
                //System.out.println("推流成功");
            }catch (Exception e) {
                err_index++;
                System.out.println("推流失败");
            }
        }

        grabber.close();
        record.close();

        System.out.println("推流完毕");

        return this;
    }


}
  • 这边需要启动之前下载好的流媒体服务器进行推流.
  • 把sdk获取到的rtsp路径传进去,再把推送地址写 rtmp://localhost:1935/live/100001010$0 端口就是流媒体服务器中app.js的配置 .
    mp4test可以自行定义(建议为设备通道号);
  • 推流成功后流媒体服务器会生成一个rtmp流和http访问路径列:
  1. rtmp://localhost:1935/live/mp4test
    2.http://127.0.0.1:8000/live/100001010$0.flv

2.4 前端使用flv.js播放

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<script src="https://cdn.bootcss.com/flv.js/1.4.0/flv.min.js"></script>
<div>
    <video id="videoElement" style="width: 45%;height: 600px;"  controls="controls"></video>
</div>

<script>
    if (flvjs.isSupported()) {
        var videoElement = document.getElementById('videoElement');
        var flvPlayer = flvjs.createPlayer({
            type: 'flv',
            url:'http://127.0.0.1:8000/live/1000010$1$0$0.flv'
        });
        flvPlayer.attachMediaElement(videoElement);
        flvPlayer.load();
        flvPlayer.play();
    }
</script>
</body>
</html>

3. 实时视频保活机制和关流机制

  • 每个监控设备按照通道号开启流的时候都会记录开启的时间和唯一标识存放到缓存当中,当用户关闭播放的时候对此通道当前使用人数减1,当定时任务检测到此通道已经没有人观看的时候自动关闭流(调用sdk的CloseReal()方法)

  • 设置保活时间,当定时任务检测到当前时间-开始播放时间大于保活时间的活会自动关闭实时视频流

  • 前端每次调用保活接口修改播放时间为当前时间.

定时任务示例:

public ReturnT<String> monitorChannelJob(String param) {
        log.info("-----------------------------XXL-JOB, MonitorChannelJob开始执行-------------------------------");
        log.info("定时任务  当前有" + MonitorSdkFacadeImpl.JOBMAP.size() + "个推流任务正在进行推流");
        // 管理缓存
        //如果已经有开始推的流
        if (null != CacheUtil.STREATMAP && 0 != CacheUtil.STREATMAP.size()) {
            Set<String> keys = CacheUtil.STREATMAP.keySet();
            for (String key : keys) {
                try {
                    // 最后打开时间
                    long openTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
                            .parse(CacheUtil.STREATMAP.get(key).getOpentime()).getTime();
                    // 当前系统时间
                    long newTime = new Date().getTime();
                    CameraDTO cameraDTO =CacheUtil.STREATMAP.get(key);
                    // 如果通道使用人数为0,则关闭推流
                    if (CacheUtil.STREATMAP.get(key).getCount() == 0) {
                        //使用sdk关闭流
                        dpsdkUtil.CloseReal(cameraDTO.getNRealSeq());
                        // 结束线程
                        MonitorSdkFacadeImpl.JOBMAP.get(key).setInterrupted(key);
                        log.info("定时任务 当前设备使用人数为0结束推流 设备信息:"+cameraDTO.toString());
                    }
                    else if (null == CacheUtil.STREATMAP.get(key).getStarttime()
                            && (newTime - openTime) / 1000 / 60 >= Integer.valueOf(dejiCommonConfig.getKeepalive())) {
                        dpsdkUtil.CloseReal(cameraDTO.getNRealSeq());
                        MonitorSdkFacadeImpl.JOBMAP.get(key).setInterrupted(key);
                        log.info("定时任务 当前设备使用时间超时结束推流 设备信息:"+cameraDTO.toString());
                    }
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        }

-参考地址:https://blog.csdn.net/weixin_40777510/article/details/103764198

4.录像回放功能

4.1 按照设备通道号和开始时间和结束时间调用sdk获取录像文件(dav类型)

4.2 把dav文件转换成MP4文件上传到fast客户端并且记录路径做后期清除功能

4.3 如果按时间段查询到有多个录像文件,会拼接成1个录像文件 (文件大小存储服务器管理平台上可以进行设置,也可以设置摄像头录制视频的时间)

/**
     * 下载录像回放文件
     */
    public int startDownLoadRecordByTime(RecordDTO recordDTO) {
        Long beginTime = DateUtil.timeToLong(recordDTO.getBeginTime()) / 1000;
        log.info("录像开始时间转为秒:{}", beginTime);

        Long endTime = DateUtil.timeToLong(recordDTO.getEndTime()) / 1000;
        log.info("录像结束时间转为秒:{}", endTime);

        Query_Record_Info_t queryInfo = new Query_Record_Info_t();
        Return_Value_Info_t nRecordCount = new Return_Value_Info_t();
        queryInfo.szCameraId = recordDTO.getChannelId().getBytes();
        //下载模式
        queryInfo.nRecordType = dpsdk_record_type_e.DPSDK_CORE_PB_RECORD_UNKONWN;
        //不检查权限,请求视频流,无需加载组织结构
        queryInfo.nRight = dpsdk_check_right_e.DPSDK_CORE_NOT_CHECK_RIGHT;
        //设备录像
        queryInfo.nSource = 2;
        //转换成秒;
        queryInfo.uBeginTime = beginTime;
        queryInfo.uEndTime = endTime;
        int nRet = IDpsdkCore.DPSDK_QueryRecord(m_nDLLHandle, queryInfo, nRecordCount, 60 * 1000);

        if (nRet != 0) {
            log.error("录像查询失败,nRet={}", nRet);
            return -1;
        }

        if (nRecordCount.nReturnValue == 0) {
            log.error("没有录像!!!!!");
            return -1;
        }

        Return_Value_Info_t nDownLoadSeq = new Return_Value_Info_t();
        Get_RecordStream_Time_Info_t getInfo = new Get_RecordStream_Time_Info_t();
        getInfo.szCameraId = recordDTO.getChannelId().getBytes();
        //下载模式
        getInfo.nMode = 2;
        //不检查权限,请求视频流,无需加载组织结构
        getInfo.nRight = dpsdk_check_right_e.DPSDK_CORE_NOT_CHECK_RIGHT;
        //设备录像
        getInfo.nSource = 2;
        log.info("开始录像下载   begintime = {}, endtime = {}", beginTime, endTime);
        //转换成秒
        getInfo.uBeginTime = beginTime;
        getInfo.uEndTime = endTime;


        nRet = IDpsdkCore.DPSDK_GetRecordStreamByTime(
                m_nDLLHandle,
                nDownLoadSeq,
                getInfo,
                new FMediaDataCallback() {

                    FileOutputStream writer = null;
                    String davPath = dejiCommonConfig.getStorageFileName() + "\\" + IdUtil.nextStringId() + ".dav";
                    File file = new File(davPath);

                    @Override
                    public void invoke(int nPDLLHandle, int nSeq, int nMediaType, byte[] szNodeId, int nParamVal, byte[] szData, int nDataLen) {
                        log.info("录像流回调");


                        if (!file.exists()) {
                            try {
                                file.createNewFile();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }

                        //nMediaType==2 音频
                        //录像下载结束,开线程调用停止录像,否则接口会超时
                        if (nMediaType == 2 && nDataLen == 0) {

                            Thread t = new Thread(new Runnable() {
                                @Override
                                public void run() {

                                    try {

                                        if (writer != null) {
                                            writer.flush();
                                            writer.close();
                                            writer = null;
                                        }

                                        int nRet = IDpsdkCore.DPSDK_CloseRecordStreamBySeq(m_nDLLHandle, nSeq, 10000);
                                        String mp4Path = davPath;
                                        mp4Path = mp4Path.replace(".dav", ".mp4");
                                        log.info("dav路径{}", davPath);
                                        log.info("mp4路径{}", mp4Path);
                                        log.info("dav视频转成mp4");
                                        Boolean flag = VideoUtil.convertToMp4(davPath, mp4Path);

                                        if (flag) {
                                            log.info("dav视频转为MP4成功,开始上传到fastDFS");
                                            String data = fastDfsClient.uploadFileWithFilepath(mp4Path);
                                            //如果录像文件上传到了服务器,删除本地文件
                                            File davFile = new File(davPath);
                                            File mp4File = new File(mp4Path);
                                            if (davFile.exists()) {
                                                log.info("删除本地dav文件");
                                                davFile.delete();
                                            }
                                            if (mp4File.exists()) {
                                                log.info("删除本地mp4文件");
                                                mp4File.delete();
                                            }
                                            String videoUrl = dejiCommonConfig.getFastdfsurl() + data;

                                            RBucket<String> bucket = redissonClient.getBucket(recordDTO.getRecordId());
                                            String json = bucket.get();
                                            RecordDTO recordDTO = JSON.parseObject(json, RecordDTO.class);
                                            recordDTO.setVideoUrl(videoUrl);
                                            bucket.set(JSON.toJSONString(recordDTO));
                                            log.info("录像前端播放地址:{}", videoUrl);
                                            //把远程服务器文件路径记录下来
                                            log.info("把远程服务器文件路径保存到数据库");
                                            monitorVideoRecordService.insert(data,recordDTO.getRecordId());
                                        }
                                        if (nRet == 0) {
                                            System.out.println("nRet=0");
                                        }
                                        log.info("下载结束,停止下载nRet = {}", nRet);
                                    } catch (Exception e) {
                                        e.printStackTrace();
                                    }
                                }
                            });
                            t.start();
                        }

                        try {

                            if (writer == null) {
                                writer = new FileOutputStream(davPath, true);
                            }
                            if (nDataLen > 0) {
                                writer.write(szData);
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }

                },
                10000);

        if (nRet == dpsdk_retval_e.DPSDK_RET_SUCCESS) {
            log.info("开始录像下载成功,nRet = {}, nSeq = {}", nRet, nDownLoadSeq.nReturnValue);
            return nDownLoadSeq.nReturnValue;
        } else {
            log.info("开始录像下载失败,nRet = {}", nRet);
            return -1;
        }

    }

4.4 dav文件转MP4文件

 /**
     * 转换视频文件为mp4
     *
     * @param srDavPath   dav 文件路径
     * @param destMp4Path 转换的MP4  文件路径
     * @return
     */
    public static boolean convertToMp4(String srDavPath, String destMp4Path) throws Exception {

        System.err.println(srDavPath + "     " + destMp4Path);

        List<String> commend = new java.util.ArrayList<String>();
        //ffmpeg -i "30.dav" -vcodec copy -acodec copy "30.mp4"
        commend.add("ffmpeg");
        commend.add("-i");
        commend.add("\"" + srDavPath + "\"");
        commend.add("-vcodec");
        commend.add("copy");
        commend.add("-acodec");
        commend.add("copy");
        commend.add("\"" + destMp4Path + "\"");
        ProcessBuilder builder = new ProcessBuilder();
        InputStream in = null;
        try {
            builder.command(commend);
            builder.redirectErrorStream(true);
            Process p = builder.start();
            byte[] b = new byte[2048];
            int readbytes = -1;
            StringBuffer output = new StringBuffer();
            // 读取进程输出值
            in = p.getInputStream();

            while ((readbytes = in.read(b)) != -1) {
                output.append(new String(b, 0, readbytes));
            }
            return true;

        } catch (Exception e) {
            e.printStackTrace();
            throw e;
        } finally {
            in.close();
        }
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容