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

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

<!-- 视频处理库 -->
        <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

地址 百度网盘 请输入提取码  密码 67v1

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

通道号可以参考这里最新海康摄像机、NVR、流媒体服务器、回放取流RTSP地址规则说明 - github.com/starRTC - 博客园

接下来就是java代码:

首先yml配置动态RTSP地址

myconfig:
  rtsp: rtsp://%s:%s@%s:%s/Streaming/Channels/%s01
  replay-rtsp: rtsp://%s:%s@%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

  • 6
    点赞
  • 79
    收藏
    觉得还不错? 一键收藏
  • 15
    评论
实现WebSocket-FLV直播服务,您可以结合使用JavaCVSpring Boot来完成。 首先,JavaCV是一个基于Java的开源计算机视觉和机器学习库,它提供了许多常见的计算机视觉和机器学习功能,如图像处理、视频处理、特征提取等。 而Spring Boot则是一个用于创建独立的、生产级别的Spring应用程序的框架,它简化了Spring应用程序的配置和部署过程,使开发者能够快速构建可靠的Java应用程序。 要实现WebSocket-FLV直播服务,您可以按照以下步骤进行: 1. 首先,使用Spring Boot创建一个新的项目。可以使用Spring Initializr(https://start.spring.io/)来初始化一个基本的Spring Boot项目。 2. 然后,添加JavaCV的依赖。您可以在项目的pom.xml文件中添加以下依赖来引入JavaCV: ```xml <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv</artifactId> <version>1.5.3</version> </dependency> ``` 3. 接下来,实现WebSocket连接和FLV视频流的处理。使用Spring Boot提供的WebSocket支持来建立和管理WebSocket连接,并使用JavaCV视频源生成FLV格式的视频流。 4. 在WebSocket处理程序中,可以使用JavaCV视频处理功能来读取视频源,然后将读取到的每一帧编码为FLV格式的视频流。可以使用JavaCV提供的FFmpegFrameGrabber类来读取视频源,并使用JavaCV提供的FFmpegFrameRecorder类来编码视频流为FLV格式。 5. 最后,将生成的FLV视频流发送给连接的WebSocket客户端。使用Spring Boot提供的WebSocket发送功能将FLV视频流发送给与WebSocket连接建立的客户端。 以上就是使用JavaCVSpring Boot实现WebSocket-FLV直播服务的基本步骤。您可以根据具体需求来调整和扩展这个基本实现

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值