(直播、监控)利用javacv解析rtsp流,转换为flv流,通过前端flv.js解析播放

前情提要:之前利用websocket解析过https://blog.csdn.net/IT_CREATE/article/details/105625858?spm=1001.2014.3001.5501,不过由于是处理图片帧的方式,导致前端不能播放声音,同时多开窗口分流后影响了图片的刷新率,所以改用当前方式进行解析,效率得到了提高,同时更加合理
展示效果
在这里插入图片描述
码云地址:https://gitee.com/dxl96/video-service

1、首先我们需要引入相关的jar包,javacv相关

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository --> 
    </parent>
    <groupId>com.de</groupId>
    <artifactId>videoservice</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>videoservice</name>
    <description>视频服务</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <commons.io.version>2.5</commons.io.version>
        <commons.fileupload.version>1.3.3</commons.fileupload.version>
        <hutool.version>4.6.4</hutool.version>
        <fastjson.version>1.2.47</fastjson.version>
        <lang3.version>3.9</lang3.version>
        <jsckson.version>2.10.3</jsckson.version>
        <javacv.version>1.5.1</javacv.version> 
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>${commons.io.version}</version>
        </dependency>
        <!--文件上传工具类 -->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>${commons.fileupload.version}</version>
        </dependency>
        <!-- 阿里JSON解析器 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <!--好用的工具集-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${lang3.version}</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>${jackson.version}</version>
        </dependency>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>${javacv.version}</version>
            <type>pom</type>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skipTests>true</skipTests>
                </configuration>
            </plugin>
        </plugins>
    </build>


</project>

2、编写javacv转flv(MediaVideoTransfer.java)

package com.de.rtsp;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.*;

import java.io.OutputStream;

/**
 * 转换rtsp为flv
 *
 * @author IT_CREATE
 * @date 2021/6/8 12:00:00
 */
@Slf4j
public class MediaVideoTransfer {
    @Setter
    private OutputStream outputStream;

    @Setter
    private String rtspUrl;

    @Setter
    private String rtspTransportType;

    private FFmpegFrameGrabber grabber;

    private FFmpegFrameRecorder recorder;

    private boolean isStart = false;

    /**
     * 开启获取rtsp流
     */
    public void live() {
        log.info("连接rtsp:" + rtspUrl + ",开始创建grabber");
        boolean isSuccess = createGrabber(rtspUrl);
        if (isSuccess) {
            log.info("创建grabber成功");
        } else {
            log.info("创建grabber失败");
        }
        startCameraPush();
    }

    /**
     * 构造视频抓取器
     *
     * @param rtsp 拉流地址
     * @return 创建成功与否
     */
    private boolean createGrabber(String rtsp) {
        // 获取视频源
        try {
            grabber = FFmpegFrameGrabber.createDefault(rtsp);
            grabber.setOption("rtsp_transport", rtspTransportType);
            grabber.start();
            isStart = true;

            recorder = new FFmpegFrameRecorder(outputStream, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
            //avcodec.AV_CODEC_ID_H264  //AV_CODEC_ID_MPEG4
            recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
            recorder.setFormat("flv");
            recorder.setFrameRate(grabber.getFrameRate());
            recorder.setSampleRate(grabber.getSampleRate());
            recorder.setAudioChannels(grabber.getAudioChannels());
            recorder.setFrameRate(grabber.getFrameRate());
            return true;
        } catch (FrameGrabber.Exception e) {
            log.error("创建解析rtsp FFmpegFrameGrabber 失败");
            log.error("create rtsp FFmpegFrameGrabber exception: ", e);
            stop();
            reset();
            return false;
        }
    }

    /**
     * 推送图片(摄像机直播)
     */
    private void startCameraPush() {
        if (grabber == null) {
            log.info("重试连接rtsp:" + rtspUrl + ",开始创建grabber");
            boolean isSuccess = createGrabber(rtspUrl);
            if (isSuccess) {
                log.info("创建grabber成功");
            } else {
                log.info("创建grabber失败");
            }
        }
        try {
            if (grabber != null) {
                recorder.start();
                Frame frame;
                while (isStart && (frame = grabber.grabFrame()) != null) {
                    recorder.setTimestamp(grabber.getTimestamp());
                    recorder.record(frame);
                }
                stop();
                reset();
            }
        } catch (FrameGrabber.Exception | RuntimeException | FrameRecorder.Exception e) {
            log.error(e.getMessage(), e);
            stop();
            reset();
        }
    }

    private void stop() {
        try {
            if (recorder != null) {
                recorder.stop();
                recorder.release();
            }
            if (grabber != null) {
                grabber.stop();
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    private void reset() {
        recorder = null;
        grabber = null;
        isStart = false;
    }
}

3、编写前端请求接口

package com.de.controller;

import com.de.entity.AjaxResult;
import com.de.rtsp.MediaVideoTransfer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * * @projectName videoservice
 * * @title IndexController
 * * @package com.de.controller
 * * @description  首页
 * * @author IT_CREAT     
 * * @date  2020 2020/5/17/017 5:15  
 * * @version c1.0.0
 */
@Slf4j
@Controller
public class IndexController {
    AtomicInteger sign = new AtomicInteger();
    ConcurrentHashMap<Integer, String> pathMap = new ConcurrentHashMap<>();
    ConcurrentHashMap<Integer, PipedOutputStream> outputStreamMap = new ConcurrentHashMap<>();
    ConcurrentHashMap<Integer, PipedInputStream> inputStreamMap = new ConcurrentHashMap<>();

    @GetMapping("/")
    public String indexView() {
        return "index";
    }

    @GetMapping("/test")
    public String testView() {
        return "test";
    }

    @PostMapping("/putVideo")
    @ResponseBody
    public AjaxResult putVideoPath(String path) {
        try {
            int id = sign.getAndIncrement();
            pathMap.put(id, path);
            PipedOutputStream pipedOutputStream = new PipedOutputStream();
            PipedInputStream pipedInputStream = new PipedInputStream();
            pipedOutputStream.connect(pipedInputStream);
            outputStreamMap.put(id, pipedOutputStream);
            inputStreamMap.put(id, pipedInputStream);
            return AjaxResult.success(id);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return AjaxResult.error();
        }
    }

    @GetMapping("/getVideo")
    public void getVideo(HttpServletRequest request, HttpServletResponse response, int id) {
        log.info("进来了" + id);
        String path = pathMap.get(id);
        String fileName = UUID.randomUUID().toString();
        // 用于测试的时候,本地文件读取走这里
        if (path.endsWith(".mp4")) {
            String[] split = new File(path).getName().split("\\.");
            fileName = split[0];
        }
        response.addHeader("Content-Disposition", "attachment;filename=" + fileName + ".flv");
        try {
            ServletOutputStream outputStream = response.getOutputStream();
            write(id, outputStream);
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
    }

    private void write(int id, OutputStream outputStream) {
        try {
            String path = pathMap.get(id);
            PipedOutputStream pipedOutputStream = outputStreamMap.get(id);
            new Thread(() -> {
                MediaVideoTransfer mediaVideoTransfer = new MediaVideoTransfer();
                mediaVideoTransfer.setOutputStream(pipedOutputStream);
                mediaVideoTransfer.setRtspTransportType("udp");
                mediaVideoTransfer.setRtspUrl(path);
                mediaVideoTransfer.live();
            }).start();

            print(inputStreamMap.get(id), outputStream);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

    private void print(InputStream inputStream, OutputStream outputStream) throws IOException {
        byte[] buffer = new byte[1024];
        int length;
        while ((length = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, length);
        }
    }

    public static void main(String[] args) throws FileNotFoundException {
        IndexController indexController = new IndexController();
        AjaxResult ajaxResult = indexController.putVideoPath("F:\\视频\\体育素材\\篮球视频素材\\哇哈体育\\篮球\\有片头进球集锦亚运决赛分p(中国vs伊朗)\\2018亚运男篮决赛台语解说剪辑版2三部分.mp4");
        indexController.write((int) ajaxResult.get("data"), new FileOutputStream("F:\\视频\\体育素材\\篮球视频素材\\哇哈体育\\篮球\\有片头进球集锦亚运决赛分p(中国vs伊朗)\\2018亚运男篮决赛台语解说剪辑版2三部分(负担).flv"));
    }
}

@GetMapping("/test") 前端请求页面
@PostMapping("/putVideo") 添加视频地址接口,因为前端get请求不能直接添加本地地址,所以需要先用post方式提交数据
@GetMapping("/getVideo") 通过get请求请求视频,将视频流写入response的outPutStream流中

注:
1、PipedOutputStream 和PipedInputStream是用于多线程的输出输入流,当PipedOutputStream 和PipedInputStream建立联系后,
写入到PipedOutputStream 的数据实际是写入到了PipedInputStream,我们通过读取PipedInputStream,就能实时读取写入到PipedOutputStream 的数据
2、也可以直接将response的outPutStream设置到MediaVideoTransfer 中,这样就不用单独开一个写入线程

4、编写前端请求页面test.html(注意切换到es6)

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
    <th:block th:include="include :: header('视频展示rtsp')"/>
    <link th:href="@{/css/video/video-js.min.css}" href="../static/css/video/video-js.min.css" rel="stylesheet"/>
    <style>
        .search {
            display: block;
            margin-bottom: 30px;
        }

        .mainContainer {
            display: block;
            width: 1024px;
            margin-left: auto;
            margin-right: auto;
        }

        .centeredVideo {
            display: block;
            width: 100%;
            height: 576px;
            margin-left: auto;
            margin-right: auto;
            margin-bottom: auto;
        }

        .controls {
            display: block;
            width: 100%;
            text-align: center;
            margin-left: auto;
            margin-right: auto;
            margin-top: 30px;
        }
    </style>
</head>

<body class="gray-bg">
<div style="padding: 20px">
    <p style="font-size: 20px;color: #0a7491;font-weight: bold;font-family: 楷体;text-align: center">rtsp拉取视频显示</p>
    <div style="text-align:center">
        <div class="search">
            文件地址(rtsp地址):<input id="video_path" type="text" style="width: 300px"/>
            <button type="button" onclick="changePath()">确定</button>
        </div>
        <div class="mainContainer">
            <video id="videoElement" class="centeredVideo" controls autoplay width="1024" height="576">Your browser is
                too old which doesn't support HTML5 video.
            </video>
        </div>
        <div class="controls">
            <button onclick="flv_start()">开始</button>
            <button onclick="flv_pause()">暂停</button>
            <button onclick="flv_destroy()">停止</button>
            <input style="width:200px" type="text" name="seekpoint" placeholder="输入时间点,int值,秒单位"/>
            <button onclick="flv_seekto()">跳转</button>
        </div>
    </div>

</div>
<th:block th:include="include :: footer"/>
<!--<script th:src="@{/js/video/video.min.js}" src="../static/js/video/video.min.js"></script>-->
<script th:src="@{/js/video/flv.js}" src="../static/js/video/flv.js"></script>
<script th:inline="javascript">
    let videoElement = document.getElementById('videoElement');

    function resetUrl(url) {
        if (flvjs.isSupported()) {
            let flvPlayer = flvjs.createPlayer({
                type: 'flv',
                "isLive": true,//<====加个这个
                url: url,//<==自行修改
            });
            flvPlayer.attachMediaElement(videoElement);
            flvPlayer.load(); //加载
            flvPlayer.play()
            flv_start();
        }
    }

    function flv_start() {
        videoElement.play();
    }

    function flv_pause() {
        videoElement.pause();
    }

    function flv_destroy() {
        videoElement.pause();
        videoElement.unload();
        videoElement.detachMediaElement();
        videoElement.destroy();
        videoElement = null;
    }

    function flv_seekto() {
        videoElement.currentTime = parseFloat(document.getElementsByName('seekpoint')[0].value);
    }

    function changePath() {
        let path = $("#video_path").val();
        if (path === null || path === "") {
            alert("请输入地址")
            return
        }
        $.ajax({
            type: "POST",
            url: ctx + "putVideo",
            data: {path: path},
            success: function (result) {
                if (result.code === 0) {
                    resetUrl(ctx + "getVideo?id=" + result.data)
                }
            },
            error: function () {
                alert("请求出错")
            }
        })
    }
</script>
</body>
</html>```


使用 JavaCV 实现 RTSP 转 HTTP-FLV 需要用到 FFmpeg 和 Netty 的相关功能。以下是实现步骤: 1. 引入 JavaCV、FFmpeg 和 Netty 相关依赖: ```xml <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv-platform</artifactId> <version>1.5.6</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.63.Final</version> </dependency> <dependency> <groupId>com.github.mhrimaz</groupId> <artifactId>netty-http-flv</artifactId> <version>1.0.1</version> </dependency> ``` 2. 创建 FFmpegFrameGrabber 和 FFmpegFrameRecorder,并设置相关参数: ```java FFmpegFrameGrabber grabber = new FFmpegFrameGrabber("rtsp://example.com/stream"); grabber.setOption("rtsp_transport", "tcp"); // 使用 TCP 协议传输数据 grabber.start(); FFmpegFrameRecorder recorder = new FFmpegFrameRecorder("http://localhost:8080/stream.flv", grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels()); recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); // 设置视频编码器为 H.264 recorder.setFormat("flv"); // 设置输出格式为 FLV recorder.start(); ``` 3. 创建 Netty 的 HTTP-FLV 服务器,并将 FFmpegFrameRecorder 中的数据转发给客户端: ```java EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new HttpObjectAggregator(65536)); pipeline.addLast(new HttpContentCompressor()); pipeline.addLast(new HttpFLVServerHandler(recorder)); } }); ChannelFuture future = bootstrap.bind(8080).sync(); ``` 4. 循环读取 RTSP 数据,将数据转换FLV 格式并写入 FFmpegFrameRecorder 中: ```java Frame frame = null; while ((frame = grabber.grab()) != null) { recorder.record(frame); } ``` 5. 释放资源: ```java grabber.stop(); grabber.release(); recorder.stop(); recorder.release(); future.channel().closeFuture().sync(); workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); ``` 以上是使用 JavaCV 实现 RTSP 转 HTTP-FLV 的基本步骤。需要注意的是,该方法需要使用 Netty 的 HTTP-FLV 服务器将数据转发给客户端,因此需要对 HTTP-FLV 相关技术有一定的了解。
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值