@[toc]

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
 **/
@Slf4j
public class StreamPushHandle {

    private FFmpegFrameGrabber grabber = null;
    private FFmpegFrameRecorder recorder = null;
    private PipedOutputStream outputStream;
    private PipedInputStream inputStream;
    private String pushAddress;
    private SdkStreamClose streamClose;
    private LinkedBlockingDeque<byte[]> frameDataQueue;
    private Long handleId;
    private AVPacket avPacket = null;

    public StreamPushHandle(String pushAddress, SdkStreamClose streamClose) {
        this.pushAddress = pushAddress;
        this.streamClose = streamClose;
        this.outputStream = new PipedOutputStream();
        this.inputStream = new PipedInputStream(256* 1024);
        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();
            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());
              //h264只需要转封装
            if (grabber.getVideoCodec() == avcodec.AV_CODEC_ID_H264) {
                recorder.start(grabber.getFormatContext());
                while ((avPacket = grabber.grabPacket()) != null) {
                    recorder.recordPacket(avPacket);
                }
            } 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();
                while ((frame = grabber.grab()) != null) {
                    recorder.record(frame);
                }
            }
        } 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);
          //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 一起研究。