当前位置:首页 » 《随便一记》 » 正文

JavaCv对接海康、大华摄像头SDK取流并转推到RTMP服务器

29 人参与  2024年02月06日 09:41  分类 : 《随便一记》  评论

点击全文阅读


文章目录

1. 前言2. 对接过程 以海康SDK取流推流为例1. 引入JavaCv Maven依赖,按需引入2. 流处理类3. 注册海康SDK取流回调函数4. 取流回调函数 3. 小结

1. 前言

支持H265转H264编码
本文主要介绍海康、大华SDK取流推流过程,这里就不展示对接海康、大华SDK了
这个是重点 Native.setCallbackThreadInitializer(this, new CallbackThreadInitializer(true, false, "HikRealStream-" + RandomUtil.randomNumbers(8)));
增加回放流速度控制

2. 对接过程 以海康SDK取流推流为例

1. 引入JavaCv Maven依赖,按需引入

   <!--JavaCV相关依赖-->        <dependency>            <groupId>org.bytedeco</groupId>            <artifactId>javacv</artifactId>            <version>1.5.9</version>        </dependency>        <dependency>            <groupId>org.bytedeco</groupId>            <artifactId>ffmpeg</artifactId>            <version>6.0-1.5.9</version>            <classifier>windows-x86_64-gpl</classifier>        </dependency>        <dependency>            <groupId>org.bytedeco</groupId>            <artifactId>ffmpeg</artifactId>            <version>6.0-1.5.9</version>            <classifier>linux-x86_64-gpl</classifier>        </dependency>

2. 流处理类

/** * 推流处理 * * @author lidaofu * @since 2023/11/10 **/@Slf4jpublic class StreamPushHandle {    private FFmpegFrameGrabber grabber = null;    private FFmpegFrameRecorder recorder = null;    private PipedOutputStream outputStream;    private PipedInputStream inputStream;    private String pushAddress;    private SdkStreamClose streamClose;    private Long handleId;    private AVPacket avPacket = null;    private Frame frame = null;    private boolean isPlayBack=false;    private double frameRate = 25.0;         public StreamPushHandle(String pushAddress, SdkStreamClose streamClose, Boolean isPlayBack) {        this.pushAddress = pushAddress;        this.streamClose = streamClose;        this.outputStream = new PipedOutputStream();        this.inputStream = new PipedInputStream(256* 1024);        this.isPlayBack = isPlayBack;        try {        //建立管道连接            inputStream.connect(outputStream);        } catch (IOException e) {            throw new FFmpegException("创建输入管道失败");        }    }       /**     * 设置播放句柄     *     * @param handleId     */    public void setHandleId(Long handleId) {        this.handleId = handleId;    }     /**     * 异步接收海康/大华/宇视设备sdk回调实时视频裸流数据     */    public void write(byte[] data,int dwBufSize) {        try {            outputStream.write(data, 0, dwBufSize);        } catch (IOException e) {            throw new FFmpegException("写入数据失败", e);        }    }    /**     * 推流     */    public void push() {        try {            FFmpegLogCallback.setLevel(avutil.AV_LOG_ERROR);            grabber = new FFmpegFrameGrabber(inputStream, 0);            //有些码率什么可以自己设置、不过没有必要            grabber.setVideoCodec(avcodec.AV_CODEC_ID_H264);            // 设置读取的最大数据,单位字节 为了加快首播速度            grabber.setOption("probesize", "8192");            // 设置分析的最长时间,单位微秒 为了加快首播速度            grabber.setOption("analyzeduration", "1000000");            // 5秒超时 单位微秒            grabber.setOption("stimeout", "5000000");            // 5秒超时 单位微秒            grabber.setOption("rw_timeout", "5000000");            // 设置缓存大小,提高画质、减少卡顿花屏            grabber.setOption("buffer_size", "1024000");            grabber.start();            // 部分监控设备流信息里携带的帧率为9000,如出现此问题,会导致dts、pts时间戳计算失败,播放器无法播放,故出现错误的帧率时,默认为25帧            if (grabber.getFrameRate() > 0 && grabber.getFrameRate() < 100) {                frameRate = grabber.getFrameRate();            }            recorder = new FFmpegFrameRecorder(pushAddress, grabber.getImageWidth(), grabber.getImageHeight());            recorder.setFormat("flv");            recorder.setInterleaved(true);            recorder.setVideoOption("preset", "ultrafast");            recorder.setVideoOption("tune", "zerolatency");            recorder.setVideoOption("crf", "25");            recorder.setSampleRate(grabber.getSampleRate());            recorder.setFrameRate(grabber.getFrameRate());            recorder.setVideoBitrate(grabber.getVideoBitrate());            int videoIndex=0;            AVFormatContext context = grabber.getFormatContext();            for (int i = 0; i < context.nb_streams(); i++) {                if (context.streams(i).codecpar().codec_type()==avutil.AVMEDIA_TYPE_VIDEO){                    videoIndex=i;                }            }            //需要等待的时间            long waitTime = (long) (1000 / frameRate);            //h264只需要转封装            if (grabber.getVideoCodec() == avcodec.AV_CODEC_ID_H264) {                recorder.start(context);                 //初次执行时间                long exStartTime = System.currentTimeMillis();                while ((avPacket = grabber.grabPacket()) != null) {                    recorder.recordPacket(avPacket);                    //如果是回放则使用视频来控制流速度                    if (isPlayBack && avPacket.stream_index() == videoIndex) {                        long exEndTime = System.currentTimeMillis();                        long diffTime = exEndTime - exStartTime;                        //计算需要等待的时间                        if (diffTime < waitTime) {                            Thread.sleep(waitTime - diffTime);                        }                        exStartTime = System.currentTimeMillis();                    }                }            } else {                if (grabber.getAudioChannels() > 0) {                    recorder.setAudioChannels(grabber.getAudioChannels());                    recorder.setAudioBitrate(grabber.getAudioBitrate());                    recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);                }                recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);                //使用libx264加速解码 注意需要使用gpl版的 不然还是默认是思科的h264编解码器                recorder.setVideoCodecName("libx264");                recorder.start();                //初次执行时间                long exStartTime = System.currentTimeMillis();                while ((frame = grabber.grab()) != null) {                    recorder.record(frame);                    //如果是回放则使用视频来控制流速度                    if (isPlayBack && frame.streamIndex == videoIndex) {                        long exEndTime = System.currentTimeMillis();                        long diffTime = exEndTime - exStartTime;                        //计算需要等待的时间                        if (diffTime < waitTime) {                            Thread.sleep(waitTime - diffTime);                        }                        exStartTime = System.currentTimeMillis();                    }                }            }        } catch (Exception e) {            log.warn("【FFMPEG】推送SDK流失败 推流地址:{}", pushAddress);        } finally {        //回调SDK关流            streamClose.closeStream(handleId);            try {                if (recorder != null) {                    recorder.close();                }                if (grabber != null) {                    grabber.close();                }            } catch (Exception e) {                throw new FFmpegException("关闭取流器失败");            }        }    }}

3. 注册海康SDK取流回调函数

//流处理器 HkSdkRequest::stopSdkPlay是关流回调函数的实现  StreamPushHandle streamPushHandle = new StreamPushHandle(pushUrl, HkSdkRequest::stopSdkPlay,fasle);  //SDK实时取流回调        FRealDataCallBack fRealDataCallBack = new FRealDataCallBack(streamPushHandle);        HCNetSDK.NET_DVR_PREVIEWINFO netDvrPreviewinfo = new HCNetSDK.NET_DVR_PREVIEWINFO();        netDvrPreviewinfo.lChannel = channelId;        netDvrPreviewinfo.dwStreamType = isMain ? 0 : 1;        netDvrPreviewinfo.bBlocked = 0;        netDvrPreviewinfo.dwLinkMode = 0;        netDvrPreviewinfo.byProtoType = 0;        //开启实时预览及设置回调函数        long ret = HkSdkClientContext.HCNETSDK.NET_DVR_RealPlay_V40(userId, netDvrPreviewinfo, fRealDataCallBack, null);        if (ret == -1) {            log.error("【海康SDK】通过sdk播放视频失败! 错误码:{}", HkSdkClientContext.HCNETSDK.NET_DVR_GetLastError());            return false;        }        //设置播放句柄 为了回调关流用的,懂得都懂        streamPushHandle.setHandleId(ret);        //我这里需要同时播放多台设备则需要添加回调函数到map中增加索引防止回调函数被gc回收 如果不需要同时预览可以使用全局final staic回调函数        REAL_PLAY_STREAM_MAP.put(ret, fRealDataCallBack);

4. 取流回调函数

public class FRealDataCallBack implements HCNetSDK.FRealDataCallBack_V30 {    private StreamPushHandle streamPushHandle;    private Boolean start = false;    public FRealDataCallBack(StreamPushHandle streamPushHandle) {    //这里是关键,默认回调是一个数据包产生一个线程,因为管道的机制,第一次写入和读取的线程会和管道绑定,如果线程G了管道也会关闭会出现 Write end dead \ Pipe closed错误,所以设置一个线程回调解决这个错误问题,如果不想设置这里可以用队列来解决,这里不详细阐述    Native.setCallbackThreadInitializer(this, new CallbackThreadInitializer(true, false, "HikRealStream-" + RandomUtil.randomNumbers(8)));        this.streamPushHandle = streamPushHandle;    }    /**     * 这个回调方法会使用海康sdk申请的线程来调用 同一个设备也是由不同的线程来提调用     */    public void invoke(long lRealHandle, int dwDataType, ByteByReference pBuffer, int dwBufSize, Pointer pUser) {    //混合码流        if (dwDataType == HCNetSDK.NET_DVR_STREAMDATA) { streamPushHandle.write(pBuffer.getByteArray(0L, dwBufSize), dwBufSize);            if (!start) {                start = true;                //这里用的线程池 保证管道读取和写入分别用的是一个独立的线程                FfmpegThreadPool.execute(streamPushHandle::push);            }        }    }}
/** *  sdk关流回调接口 * * @author lidaofu * @since 2023/11/13 **/public interface SdkStreamClose { /**      *  当推流停止或者发生异常会调用此接口 此接口再去调用海康、大华的sdk关流接口      * @param handle      */     void  closeStream(Long handle);}

3. 小结

上面就是全部对接流程,可能会有问题,但不多,有问题可以wx联系我:L746101210 一起研究。


点击全文阅读


本文链接:http://zhangshiyu.com/post/67530.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1