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



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。