前情提要:之前利用websocket解析过https://blog.csdn.net/IT_CREATE/article/details/105625858?spm=1001.2014.3001.5501,不过由于是处理图片帧的方式,导致前端不能播放声音,同时多开窗口分流后影响了图片的刷新率,所以改用当前方式进行解析,效率得到了提高,同时更加合理
展示效果:
码云地址:https://gitee.com/dxl96/video-service
1、首先我们需要引入相关的jar包,javacv相关
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.1.RELEASEversion>
<relativePath/>
parent>
<groupId>com.degroupId>
<artifactId>videoserviceartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>videoservicename>
<description>视频服务description>
<properties>
<java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<commons.io.version>2.5commons.io.version>
<commons.fileupload.version>1.3.3commons.fileupload.version>
<hutool.version>4.6.4hutool.version>
<fastjson.version>1.2.47fastjson.version>
<lang3.version>3.9lang3.version>
<jsckson.version>2.10.3jsckson.version>
<javacv.version>1.5.1javacv.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>commons-iogroupId>
<artifactId>commons-ioartifactId>
<version>${commons.io.version}version>
dependency>
<dependency>
<groupId>commons-fileuploadgroupId>
<artifactId>commons-fileuploadartifactId>
<version>${commons.fileupload.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>${fastjson.version}version>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>${hutool.version}version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>${lang3.version}version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>${jackson.version}version>
dependency>
<dependency>
<groupId>org.bytedecogroupId>
<artifactId>javacv-platformartifactId>
<version>${javacv.version}version>
<type>pomtype>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<source>${java.version}source>
<target>${java.version}target>
<encoding>${project.build.sourceEncoding}encoding>
configuration>
plugin>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-surefire-pluginartifactId>
<configuration>
<skipTests>trueskipTests>
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/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>```