欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

spring boot集成javacv + websocket实现实时视频推流回放(延时1-2秒)

程序员文章站 2022-07-07 09:02:36
...

最近项目需要实时直播和回放,集成海康威视摄像头:(适合少量用户,或者内部系统使用)

<!-- 视频处理库 -->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>1.5.1</version>
        </dependency>

这里是利用javacv抓取rtsp地址的视频流,通过转换成图片使用websocket实时推送

首先了解rtsp地址,这里举例就用海康威视的规则

通道号下面详细介绍
#  单播
        rtsp://{用户名}:{密码}@{IP地址}:{端口号}/Streaming/Channels/{通道号}?transportmode=unicast
#  多播
        rtsp://{用户名}:{密码}@{IP地址}:{端口号}/Streaming/Channels/{通道号}?transportmode=multicast
#  分时获取
        rtsp://{用户名}:{密码}@{IP地址}:{端口号}/Streaming/tracks/{通道号}?starttime=20191008t063812z&endtime=20191008t064816z

上面地址获取不到时可以试试下面的其他版本地址:
        rtsp://{用户名}:{密码}@{IP地址}:{端口号}/Streaming/Unicast/channels/{通道号}
        rtsp://{用户名}:{密码}@{IP地址}:{端口号}/h264/{通道号}/main/av_stream

这里通道号分类是IP通道号和模拟通道号,如果是12年以前的老机器,地址建议使用最后一个获取实时视频

可以使用官方提供的java demo 来查看,测试是否能获取视频

这里有打包好的jar包和环境可以直接运行jar包使用,解压后将里面内容放置在System32下,cmd执行jar

地址 https://pan.baidu.com/s/1QxELvKLmgqjeKu6NKUGP0A  密码 67v1

下图就是IP通道号 若只是Camera20 则为模拟通道号

spring boot集成javacv + websocket实现实时视频推流回放(延时1-2秒)

通道号可以参考这里https://www.cnblogs.com/elesos/p/9881690.html

接下来就是java代码:

首先yml配置动态RTSP地址

myconfig:
  rtsp: rtsp://%s:%aaa@qq.com%s:%s/Streaming/Channels/%s01
  replay-rtsp: rtsp://%s:%aaa@qq.com%s:%s/Streaming/tracks/%s01?starttime=%s&endtime=%s

媒体工具类:

@Slf4j
@Component
public class MediaUtils {
    /**
     * 直播摄像机id集合,避免重复拉流
     */
    private static Set<Long> liveSet = new ConcurrentSet<>();
    /**
     * 用于构造回放rtsp地址
     */
    @Value("${myconfig.replay-rtsp}")
    private String rtspReplayPattern;
    /**
     * 视频帧率
     */
    public static int frameRate = 15;
    /**
     * 视频宽度
     */
    public static int frameWidth = 480;
    /**
     * 视频高度
     */
    public static int frameHeight = 270;

    /**
     * 摄像机直播
     * @param rtsp 摄像机直播地址
     * @param cameraName 摄像机名称
     * @param cameraId 摄像机id
     * @throws Exception e
     */
    @Async
    public void live(String rtsp, String cameraName, Long cameraId) throws Exception {
        if (liveSet.contains(cameraId)) {
            return;
        }
        liveSet.add(cameraId);
        FFmpegFrameGrabber grabber = createGrabber(rtsp);
        startCameraPush(grabber, cameraName, cameraId);
    }


    /**
     * 构造视频抓取器
     * @param rtsp 拉流地址
     * @return
     */
    public FFmpegFrameGrabber createGrabber(String rtsp) {
        // 获取视频源
        FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(rtsp);
        grabber.setOption("rtsp_transport","tcp");
        //设置帧率
        grabber.setFrameRate(frameRate);
        //设置获取的视频宽度
        grabber.setImageWidth(frameWidth);
        //设置获取的视频高度
        grabber.setImageHeight(frameHeight);
        //设置视频bit率
        grabber.setVideoBitrate(2000000);
        return grabber;
    }

    /**
     * 推送图片(摄像机直播)
     * @param grabber
     * @throws Exception
     */
    @Async
    public void startCameraPush(FFmpegFrameGrabber grabber, String cameraName, Long cameraId) throws Exception {
        Java2DFrameConverter java2DFrameConverter = new Java2DFrameConverter();
        try {
            grabber.start();
            int i = 1;
            while (liveSet.contains(cameraId)) {
                Frame frame = grabber.grabImage();
                if (null == frame) {
                    continue;
                }
                BufferedImage bufferedImage = java2DFrameConverter.getBufferedImage(frame);

                byte[] bytes = imageToBytes(bufferedImage, "jpg");

                //使用websocket发送图片数据
                LiveWebsocket.sendImage(ByteBuffer.wrap(bytes), cameraId);
            }
        } finally {
            if (grabber != null) {
                grabber.stop();
            }
        }
    }

    /**
     * 图片转字节数组
     * @param bImage 图片数据
     * @param format 格式
     * @return 图片字节码
     */
    private byte[] imageToBytes(BufferedImage bImage, String format) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            ImageIO.write(bImage, format, out);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return out.toByteArray();
    }

    /**
     * 回放视频播放超期检查
     * @param userId 用户id
     * @return
     */
    private boolean replayOverTime(Integer userId) {
        if (replayMap.containsKey(userId)) {
            Long updateTime = replayMap.get(userId);
            if (updateTime != null) {
                if (System.currentTimeMillis() - updateTime < 10000) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * 构造监控回放查询字段
     * @param date 时间
     * @param start
     * @return
     */
    private String formatPullTime(Date date, boolean start) {
        Calendar calendar = Calendar.getInstance();
        if (date != null) {
            calendar.setTime(date);
        }
        if (start) {
            calendar.add(Calendar.SECOND, -10);
        } else {
            calendar.add(Calendar.SECOND, 10);
        }
        //海康威视取回放的时间格式
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd#HHmmss$");
        String ret = sdf.format(calendar.getTime());
        ret = ret.replace("#", "t");
        ret = ret.replace("$", "z");
        return ret;
    }

 /**
     * 回放视频播放超期检查
     * @param userId 用户id
     * @return
     */
    private boolean replayOverTime(Integer userId) {
        if (replayMap.containsKey(userId)) {
            Long updateTime = replayMap.get(userId);
            if (updateTime != null) {
                if (System.currentTimeMillis() - updateTime < 10000) {
                    return false;
                }
            }
        }
        return true;
    }
 /**
     * 监控回放
     * @param userId 用户id  websocket 用户编号,作为心跳标识检测心跳
     * @param startDate 起始时间
     * @param endDate 结束时间
     * @param channel 通道号
     * @param username 用户名
     * @param password 密码
     * @param ip
     * @param port
     * @throws Exception e
     */
    @Async
    public void replayVideo(Integer userId, Date startDate, Date endDate,
                            Integer channel, String username, String password, String ip, Integer port) throws Exception {
        Java2DFrameConverter java2DFrameConverter = new Java2DFrameConverter();
            FFmpegFrameGrabber grabber = null;
            try {
                if (grabber != null) {
                    grabber.stop();
                }
                if (channel != null) {
                    String st = formatPullTime(startDate, true);
                    String et = formatPullTime(endDate, false);
                    //构造rtsp回放流地址 username password ip port
                    String rtsp = String.format(
                            rtspReplayPattern,
                            username,
                            password,
                            ip,
                            port,
                            channel,
                            st,
                            et
                    );
                    if (grabber != null) {
                        grabber.stop();
                    }
                    grabber = createGrabber(rtsp);
                    grabber.setTimeout(10000);
                    grabber.start();
                    //心跳消失停止推流
                    while (!replayOverTime(userId)) {
                        Frame frame = grabber.grabImage();
                        if (null == frame) {
                            continue;
                        }

                        BufferedImage bufferedImage = java2DFrameConverter.getBufferedImage(frame);

                        byte[] bytes = imageToBytes(bufferedImage, "jpg");
                        ByteBuffer buffer = ByteBuffer.wrap(bytes);

                        //使用websocket发送图片数据
                        ReplayWebsocke.sendImage(buffer, userId);
                    }
                }
            } catch (Exception e){
                e.printStackTrace();
            } finally {
                if (grabber != null) {
                    grabber.stop();
                }
            }
    }
}

视频水印添加:

@Component
public class ImgMarker {

    /**
     * 视频水印图片
     */
    BufferedImage logoImg;

    private Font font;
    private Font font2;
    private FontDesignMetrics metrics;
    private FontDesignMetrics metrics2;

    @PostConstruct
    private void init() {
        // 加水印图片
        try {
            ImageIO.read(new File("图片地址"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        font = new Font("黑体", Font.BOLD, 16);
        font2 = new Font("黑体", Font.BOLD, 24);
        metrics = FontDesignMetrics.getMetrics(font);
        metrics2 = FontDesignMetrics.getMetrics(font2);
    }

    /**
     * 加水印
     * @param bufImg 视频帧
     */
    public void mark(BufferedImage bufImg) {
        if (bufImg == null || logoImg == null) {
            return;
        }
        int width = bufImg.getWidth();
        int height = bufImg.getHeight();
        Graphics2D graphics = bufImg.createGraphics();
        graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
        //设置图片背景
        graphics.drawImage(bufImg, 0, 0, width, height, null);
        //添加右上角水印
        graphics.drawImage(logoImg, width - 130, 8, 121, 64, null);
    }

    /**
     *
     * @param bufImg 视频帧
     */
    public void markTag(BufferedImage bufImg, String msg, int videoWidth) {
        int width = bufImg.getWidth();
        int height = bufImg.getHeight();
        Graphics2D graphics = bufImg.createGraphics();
        graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        graphics.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
        //设置图片背景
        graphics.drawImage(bufImg, 0, 0, width, height, null);
        //设置由上方标签号
        graphics.setColor(Color.orange);
        if (videoWidth <= 400) {
            graphics.setFont(font2);
            graphics.drawString(msg,  width - metrics2.stringWidth(tagId) - 24, metrics2.getAscent());
        } else {
            graphics.setFont(font);
            graphics.drawString(msg,  width - metrics.stringWidth(msg) - 12, metrics.getAscent());
        }
        graphics.dispose();
    }

}

至此可以调用了(还有其他方式是直接调用SDK和推送视频流媒体服务器,后续文章更新)

websocket 涉及到高并发阻塞情况 后续更新,建议使用netty