前情提要:之前利用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>```