java 实现监控rtsp流转flv,实现前端播放
偏执怪人。 2024-09-30 11:33:01 阅读 93
目录
注意后端代码jar包controller层config层factories层service层
前端代码
注意
先查看直接的视频流能不能播放,视频编解码必须是H264
video mediaplay官网 即(VLC)
下载、安装完VLC后,打开VLC 点击媒体 -> 打开网络串流,粘贴地址播放,不能播放可能地址有问题
查看编解码格式,右击视频选择工具->编解码器信息,格式为H264
后端代码
复制可直接使用
jar包
<code><dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
controller层
@RestController
@RequestMapping("/flv")
public class JyDeviceController extends BaseController { -- -->
@Autowired
private IFLVService service;
@RequestMapping()
public void open4(HttpServletResponse response, HttpServletRequest request) {
String rtsp = "rtsp://xxxxxxxxxx(自己的rtsp地址)";
service.open(rtsp, response, request);
}
}
config层
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* 使用多线程执行定时任务
*
* @author gc.x
*
*/
@Configuration
@EnableScheduling
public class SchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
// 线程池大小
scheduler.setPoolSize(3);
// 线程名字前缀
scheduler.setThreadNamePrefix("task-thread-");
return scheduler;
}
}
factories层
factories结构
<code>/**
* 转换器状态(初始化、打开、关闭、错误、运行)
*/
public enum ConverterState { -- -->
INITIAL, OPEN, CLOSE, ERROR, RUN
}
import javax.servlet.AsyncContext;
import java.io.IOException;
/**
* @Description Converter
* @Author admin
* @Date 2024/6/18 10:22
*/
public interface Converter {
/**
* 获取该转换的key
*/
public String getKey();
/**
* 获取该转换的url
*
* @return
*/
public String getUrl();
/**
* 添加一个流输出
*
* @param entity
*/
public void addOutputStreamEntity(String key, AsyncContext entity) throws IOException;
/**
* 退出转换
*/
public void exit();
/**
* 启动
*/
public void start();
}
import com.alibaba.fastjson2.util.IOUtils;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import javax.servlet.AsyncContext;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* javacv转包装<br/>
* 无须转码,更低的资源消耗,更低的延迟<br/>
* 确保流来源视频H264格式,音频AAC格式
*
* @author gc.x
*/
@Slf4j
public class ConverterFactories extends Thread implements Converter {
public volatile boolean runing = true;
/**
* 读流器
*/
private FFmpegFrameGrabber grabber;
/**
* 转码器
*/
private FFmpegFrameRecorder recorder;
/**
* 转FLV格式的头信息<br/>
* 如果有第二个客户端播放首先要返回头信息
*/
private byte[] headers;
/**
* 保存转换好的流
*/
private ByteArrayOutputStream stream;
/**
* 流地址,h264,aac
*/
private String url;
/**
* 流输出
*/
private List<AsyncContext> outEntitys;
/**
* key用于表示这个转换器
*/
private String key;
/**
* 转换队列
*/
private Map<String, Converter> factories;
public ConverterFactories(String url, String key, Map<String, Converter> factories, List<AsyncContext> outEntitys) {
this.url = url;
this.key = key;
this.factories = factories;
this.outEntitys = outEntitys;
}
@Override
public void run() {
boolean isCloseGrabberAndResponse = true;
try {
grabber = new FFmpegFrameGrabber(url);
if ("rtsp".equals(url.substring(0, 4))) {
grabber.setOption("rtsp_transport", "tcp");
grabber.setOption("stimeout", "5000000");
}
grabber.start();
if (avcodec.AV_CODEC_ID_H264 == grabber.getVideoCodec()
&& (grabber.getAudioChannels() == 0 || avcodec.AV_CODEC_ID_AAC == grabber.getAudioCodec())) {
log.info("this url:{} converterFactories start", url);
// 来源视频H264格式,音频AAC格式
// 无须转码,更低的资源消耗,更低的延迟
stream = new ByteArrayOutputStream();
recorder = new FFmpegFrameRecorder(stream, grabber.getImageWidth(), grabber.getImageHeight(),
grabber.getAudioChannels());
recorder.setInterleaved(true);
recorder.setVideoOption("preset", "ultrafast");
recorder.setVideoOption("tune", "zerolatency");
recorder.setVideoOption("crf", "25");
recorder.setFrameRate(grabber.getFrameRate());
recorder.setSampleRate(grabber.getSampleRate());
if (grabber.getAudioChannels() > 0) {
recorder.setAudioChannels(grabber.getAudioChannels());
// recorder.setAudioBitrate(grabber.getAudioBitrate());//转流后没有音频,不知道原因,注释掉就可以了
recorder.setAudioCodec(grabber.getAudioCodec());
}
recorder.setFormat("flv");
recorder.setVideoBitrate(grabber.getVideoBitrate());
recorder.setVideoCodec(grabber.getVideoCodec());
recorder.start(grabber.getFormatContext());
if (headers == null) {
headers = stream.toByteArray();
stream.reset();
writeResponse(headers);
}
int nullNumber = 0;
while (runing) {
AVPacket k = grabber.grabPacket();
if (k != null) {
try {
recorder.recordPacket(k);
} catch (Exception e) {
}
if (stream.size() > 0) {
byte[] b = stream.toByteArray();
stream.reset();
writeResponse(b);
if (outEntitys.isEmpty()) {
log.info("没有输出退出");
break;
}
}
avcodec.av_packet_unref(k);
} else {
nullNumber++;
if (nullNumber > 200) {
break;
}
}
Thread.sleep(5);
}
} else {
isCloseGrabberAndResponse = false;
// 需要转码为视频H264格式,音频AAC格式
ConverterTranFactories c = new ConverterTranFactories(url, key, factories, outEntitys, grabber);
factories.put(key, c);
c.start();
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
closeConverter(isCloseGrabberAndResponse);
completeResponse(isCloseGrabberAndResponse);
log.info("this url:{} converterFactories exit", url);
}
}
/**
* 输出FLV视频流
*
* @param b
*/
public void writeResponse(byte[] b) {
Iterator<AsyncContext> it = outEntitys.iterator();
while (it.hasNext()) {
AsyncContext o = it.next();
try {
o.getResponse().getOutputStream().write(b);
} catch (Exception e) {
log.info("移除一个输出");
it.remove();
}
}
}
/**
* 退出转换
*/
public void closeConverter(boolean isCloseGrabberAndResponse) {
if (isCloseGrabberAndResponse) {
IOUtils.close(grabber);
factories.remove(this.key);
}
IOUtils.close(recorder);
IOUtils.close(stream);
}
/**
* 关闭异步响应
*
* @param isCloseGrabberAndResponse
*/
public void completeResponse(boolean isCloseGrabberAndResponse) {
if (isCloseGrabberAndResponse) {
Iterator<AsyncContext> it = outEntitys.iterator();
while (it.hasNext()) {
AsyncContext o = it.next();
o.complete();
}
}
}
@Override
public String getKey() {
return this.key;
}
@Override
public String getUrl() {
return this.url;
}
@Override
public void addOutputStreamEntity(String key, AsyncContext entity) throws IOException {
if (headers == null) {
outEntitys.add(entity);
} else {
entity.getResponse().getOutputStream().write(headers);
entity.getResponse().getOutputStream().flush();
outEntitys.add(entity);
}
}
@Override
public void exit() {
this.runing = false;
try {
this.join();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
import com.alibaba.fastjson2.util.IOUtils;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import javax.servlet.AsyncContext;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* javacv转码<br/>
* 流来源不是视频H264格式,音频AAC格式 转码为视频H264格式,音频AAC格式
*
* @author gc.x
*/
@Slf4j
public class ConverterTranFactories extends Thread implements Converter {
public volatile boolean runing = true;
/**
* 读流器
*/
private FFmpegFrameGrabber grabber;
/**
* 转码器
*/
private FFmpegFrameRecorder recorder;
/**
* 转FLV格式的头信息<br/>
* 如果有第二个客户端播放首先要返回头信息
*/
private byte[] headers;
/**
* 保存转换好的流
*/
private ByteArrayOutputStream stream;
/**
* 流地址,h264,aac
*/
private String url;
/**
* 流输出
*/
private List<AsyncContext> outEntitys;
/**
* key用于表示这个转换器
*/
private String key;
/**
* 转换队列
*/
private Map<String, Converter> factories;
public ConverterTranFactories(String url, String key, Map<String, Converter> factories,
List<AsyncContext> outEntitys, FFmpegFrameGrabber grabber) {
this.url = url;
this.key = key;
this.factories = factories;
this.outEntitys = outEntitys;
this.grabber = grabber;
}
@Override
public void run() {
try {
log.info("this url:{} converterTranFactories start", url);
grabber.setFrameRate(25);
if (grabber.getImageWidth() > 1920) {
grabber.setImageWidth(1920);
}
if (grabber.getImageHeight() > 1080) {
grabber.setImageHeight(1080);
}
stream = new ByteArrayOutputStream();
recorder = new FFmpegFrameRecorder(stream, grabber.getImageWidth(), grabber.getImageHeight(),
grabber.getAudioChannels());
recorder.setInterleaved(true);
recorder.setVideoOption("preset", "ultrafast");
recorder.setVideoOption("tune", "zerolatency");
recorder.setVideoOption("crf", "25");
recorder.setGopSize(50);
recorder.setFrameRate(25);
recorder.setSampleRate(grabber.getSampleRate());
if (grabber.getAudioChannels() > 0) {
recorder.setAudioChannels(grabber.getAudioChannels());
// recorder.setAudioBitrate(grabber.getAudioBitrate());
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
}
recorder.setFormat("flv");
recorder.setVideoBitrate(grabber.getVideoBitrate());
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.start();
if (headers == null) {
headers = stream.toByteArray();
stream.reset();
writeResponse(headers);
}
int nullNumber = 0;
while (runing) {
// 抓取一帧
Frame f = grabber.grab();
if (f != null) {
try {
// 转码
recorder.record(f);
} catch (Exception e) {
}
if (stream.size() > 0) {
byte[] b = stream.toByteArray();
stream.reset();
writeResponse(b);
if (outEntitys.isEmpty()) {
log.info("没有输出退出");
break;
}
}
} else {
nullNumber++;
if (nullNumber > 200) {
break;
}
}
Thread.sleep(5);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
closeConverter();
completeResponse();
log.info("this url:{} converterTranFactories exit", url);
factories.remove(this.key);
}
}
/**
* 输出FLV视频流
*
* @param b
*/
public void writeResponse(byte[] b) {
Iterator<AsyncContext> it = outEntitys.iterator();
while (it.hasNext()) {
AsyncContext o = it.next();
try {
o.getResponse().getOutputStream().write(b);
} catch (Exception e) {
log.info("移除一个输出");
it.remove();
}
}
}
/**
* 退出转换
*/
public void closeConverter() {
IOUtils.close(grabber);
IOUtils.close(recorder);
IOUtils.close(stream);
}
/**
* 关闭异步响应
*/
public void completeResponse() {
Iterator<AsyncContext> it = outEntitys.iterator();
while (it.hasNext()) {
AsyncContext o = it.next();
o.complete();
}
}
@Override
public String getKey() {
return this.key;
}
@Override
public String getUrl() {
return this.url;
}
@Override
public void addOutputStreamEntity(String key, AsyncContext entity) throws IOException {
if (headers == null) {
outEntitys.add(entity);
} else {
entity.getResponse().getOutputStream().write(headers);
entity.getResponse().getOutputStream().flush();
outEntitys.add(entity);
}
}
@Override
public void exit() {
this.runing = false;
try {
this.join();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
service层
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Description IFLVService
* @Author admin
* @Date 2024/6/18 10:24
*/
public interface IFLVService {
/**
* 打开一个流地址
*
* @param url
* @param response
*/
public void open(String url, HttpServletResponse response, HttpServletRequest request);
}
import com.emergency.device.jy.factories.Converter;
import com.emergency.device.jy.factories.ConverterFactories;
import com.emergency.device.jy.service.IFLVService;
import org.apache.commons.compress.utils.Lists;
import org.springframework.stereotype.Service;
import javax.servlet.AsyncContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import static com.emergency.framework.datasource.DynamicDataSourceContextHolder.log;
/**
* @Description FLVService
* @Author admin
* @Date 2024/6/18 10:34
*/
@Service
public class FLVService implements IFLVService {
private ConcurrentHashMap<String, Converter> converters = new ConcurrentHashMap<>();
/**
* 打开一个流地址
*
* @param url
* @param response
* @param request
*/
@Override
public void open(String url, HttpServletResponse response, HttpServletRequest request) {
String key = md5(url);
AsyncContext async = request.startAsync();
async.setTimeout(0);
if (converters.containsKey(key)) {
Converter c = converters.get(key);
try {
c.addOutputStreamEntity(key, async);
} catch (IOException e) {
log.error(e.getMessage(), e);
throw new IllegalArgumentException(e.getMessage());
}
} else {
List<AsyncContext> outs = Lists.newArrayList();
outs.add(async);
ConverterFactories c = new ConverterFactories(url, key, converters, outs);
c.start();
converters.put(key, c);
}
response.setContentType("video/x-flv");
response.setHeader("Connection", "keep-alive");
response.setStatus(HttpServletResponse.SC_OK);
try {
response.flushBuffer();
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
public String md5(String plainText) {
StringBuilder buf = null;
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(plainText.getBytes());
byte b[] = md.digest();
int i;
buf = new StringBuilder("");
for (int offset = 0; offset < b.length; offset++) {
i = b[offset];
if (i < 0)
i += 256;
if (i < 16)
buf.append("0");
buf.append(Integer.toHexString(i));
}
} catch (NoSuchAlgorithmException e) {
log.error(e.getMessage(), e);
}
return buf.toString();
}
}
前端代码
<template>
<div>
// 浏览器不支持自动播放,需要在video标签中设置,controls是否显示播放器按钮
<video preload="auto" muted autoplay></video> code>
</div>
</template>
<script>
data() { -- -->
return {
flvPlayer: null,
};
},
mounted() {
this.$nextTick(() => {
this.playflv()
})
},
methods: {
//使用flv.js实现播放flv格式流,获取video节点
playflv() {
if (flvjs.isSupported()) {
var videoElement = document.getElementById('videoElement');
var flvPlayer = flvjs.createPlayer({
type: 'flv',
url: 'http://127.0.0.1:19677/flv',//后端controller层的接口地址
isLive: true, //数据源是否为直播流
hasAudio: true, //数据源是否包含有音频
hasVideo: true, //数据源是否包含有视频
enableStashBuffer: false //是否启用缓存区
},{
enableWorker: false, //不启用分离线程
enableStashBuffer: false, //关闭IO隐藏缓冲区
autoCleanupSourceBuffer: true //自动清除缓存
});
this.flvPlayer = flvPlayer
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
flvPlayer.play();
}
// 报错重连
this.flvPlayer.on(flvjs.Events.ERROR, (err, errdet) => {
// 参数 err 是一级异常,errdet 是二级异常
if (err == flvjs.ErrorTypes.MEDIA_ERROR) {
console.log('媒体错误')
if(errdet == flvjs.ErrorDetails.MEDIA_FORMAT_UNSUPPORTED) {
console.log('媒体格式不支持')
}
}
if (err == flvjs.ErrorTypes.NETWORK_ERROR) {
console.log('网络错误')
if(errdet == flvjs.ErrorDetails.NETWORK_STATUS_CODE_INVALID) {
console.log('http状态码异常')
}
}
if(err == flvjs.ErrorTypes.OTHER_ERROR) {
console.log('其他异常:', errdet)
}
if (this.flvPlayer) {
this.destoryVideo()
this.playflv()
}
})
},
//关闭视频流
destoryVideo() {
if (this.flvPlayer) {
this.flvPlayer.pause();// 暂停播放数据流
this.flvPlayer.unload();// 取消数据流加载
this.flvPlayer.detachMediaElement();// 将播放实例从节点中取出
this.flvPlayer.destroy(); // 销毁播放实例
this.flvPlayer = null;
}
},
},
beforeDestroy() {
this.destoryVideo();
},
</script>
参考地址
https://developer.aliyun.com/article/867004#slide-4
gitee地址
https://gitee.com/giteeClass/rtsp-converter-flv-spring-boot-starter
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。