目录
注意
先查看直接的视频流能不能播放,视频编解码必须是H264
video mediaplay官网 即(VLC)
下载、安装完VLC后,打开VLC 点击媒体 -> 打开网络串流,粘贴地址播放,不能播放可能地址有问题
查看编解码格式,右击视频选择工具->编解码器信息,格式为H264
后端代码
复制可直接使用
jar包
<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")publicclassJyDeviceControllerextendsBaseController{@AutowiredprivateIFLVService service;@RequestMapping()publicvoidopen4(HttpServletResponse response,HttpServletRequest request){String rtsp ="rtsp://xxxxxxxxxx(自己的rtsp地址)";
service.open(rtsp, response, request);}}
config层
importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.scheduling.TaskScheduler;importorg.springframework.scheduling.annotation.EnableScheduling;importorg.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;/**
* 使用多线程执行定时任务
*
* @author gc.x
*
*/@Configuration@EnableSchedulingpublicclassSchedulerConfig{@BeanpublicTaskSchedulertaskScheduler(){ThreadPoolTaskScheduler scheduler =newThreadPoolTaskScheduler();// 线程池大小
scheduler.setPoolSize(3);// 线程名字前缀
scheduler.setThreadNamePrefix("task-thread-");return scheduler;}}
factories层
factories结构
/**
* 转换器状态(初始化、打开、关闭、错误、运行)
*/publicenumConverterState{INITIAL,OPEN,CLOSE,ERROR,RUN}
importjavax.servlet.AsyncContext;importjava.io.IOException;/**
* @Description Converter
* @Author admin
* @Date 2024/6/18 10:22
*/publicinterfaceConverter{/**
* 获取该转换的key
*/publicStringgetKey();/**
* 获取该转换的url
*
* @return
*/publicStringgetUrl();/**
* 添加一个流输出
*
* @param entity
*/publicvoidaddOutputStreamEntity(String key,AsyncContext entity)throwsIOException;/**
* 退出转换
*/publicvoidexit();/**
* 启动
*/publicvoidstart();}
importcom.alibaba.fastjson2.util.IOUtils;importlombok.extern.slf4j.Slf4j;importorg.bytedeco.ffmpeg.avcodec.AVPacket;importorg.bytedeco.ffmpeg.global.avcodec;importorg.bytedeco.javacv.FFmpegFrameGrabber;importorg.bytedeco.javacv.FFmpegFrameRecorder;importjavax.servlet.AsyncContext;importjava.io.ByteArrayOutputStream;importjava.io.IOException;importjava.util.Iterator;importjava.util.List;importjava.util.Map;/**
* javacv转包装<br/>
* 无须转码,更低的资源消耗,更低的延迟<br/>
* 确保流来源视频H264格式,音频AAC格式
*
* @author gc.x
*/@Slf4jpublicclassConverterFactoriesextendsThreadimplementsConverter{publicvolatileboolean runing =true;/**
* 读流器
*/privateFFmpegFrameGrabber grabber;/**
* 转码器
*/privateFFmpegFrameRecorder recorder;/**
* 转FLV格式的头信息<br/>
* 如果有第二个客户端播放首先要返回头信息
*/privatebyte[] headers;/**
* 保存转换好的流
*/privateByteArrayOutputStream stream;/**
* 流地址,h264,aac
*/privateString url;/**
* 流输出
*/privateList<AsyncContext> outEntitys;/**
* key用于表示这个转换器
*/privateString key;/**
* 转换队列
*/privateMap<String,Converter> factories;publicConverterFactories(String url,String key,Map<String,Converter> factories,List<AsyncContext> outEntitys){this.url = url;this.key = key;this.factories = factories;this.outEntitys = outEntitys;}@Overridepublicvoidrun(){boolean isCloseGrabberAndResponse =true;try{
grabber =newFFmpegFrameGrabber(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 =newByteArrayOutputStream();
recorder =newFFmpegFrameRecorder(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 =newConverterTranFactories(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
*/publicvoidwriteResponse(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();}}}/**
* 退出转换
*/publicvoidcloseConverter(boolean isCloseGrabberAndResponse){if(isCloseGrabberAndResponse){IOUtils.close(grabber);
factories.remove(this.key);}IOUtils.close(recorder);IOUtils.close(stream);}/**
* 关闭异步响应
*
* @param isCloseGrabberAndResponse
*/publicvoidcompleteResponse(boolean isCloseGrabberAndResponse){if(isCloseGrabberAndResponse){Iterator<AsyncContext> it = outEntitys.iterator();while(it.hasNext()){AsyncContext o = it.next();
o.complete();}}}@OverridepublicStringgetKey(){returnthis.key;}@OverridepublicStringgetUrl(){returnthis.url;}@OverridepublicvoidaddOutputStreamEntity(String key,AsyncContext entity)throwsIOException{if(headers ==null){
outEntitys.add(entity);}else{
entity.getResponse().getOutputStream().write(headers);
entity.getResponse().getOutputStream().flush();
outEntitys.add(entity);}}@Overridepublicvoidexit(){this.runing =false;try{this.join();}catch(Exception e){
log.error(e.getMessage(), e);}}}
importcom.alibaba.fastjson2.util.IOUtils;importlombok.extern.slf4j.Slf4j;importorg.bytedeco.ffmpeg.global.avcodec;importorg.bytedeco.javacv.FFmpegFrameGrabber;importorg.bytedeco.javacv.FFmpegFrameRecorder;importorg.bytedeco.javacv.Frame;importjavax.servlet.AsyncContext;importjava.io.ByteArrayOutputStream;importjava.io.IOException;importjava.util.Iterator;importjava.util.List;importjava.util.Map;/**
* javacv转码<br/>
* 流来源不是视频H264格式,音频AAC格式 转码为视频H264格式,音频AAC格式
*
* @author gc.x
*/@Slf4jpublicclassConverterTranFactoriesextendsThreadimplementsConverter{publicvolatileboolean runing =true;/**
* 读流器
*/privateFFmpegFrameGrabber grabber;/**
* 转码器
*/privateFFmpegFrameRecorder recorder;/**
* 转FLV格式的头信息<br/>
* 如果有第二个客户端播放首先要返回头信息
*/privatebyte[] headers;/**
* 保存转换好的流
*/privateByteArrayOutputStream stream;/**
* 流地址,h264,aac
*/privateString url;/**
* 流输出
*/privateList<AsyncContext> outEntitys;/**
* key用于表示这个转换器
*/privateString key;/**
* 转换队列
*/privateMap<String,Converter> factories;publicConverterTranFactories(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;}@Overridepublicvoidrun(){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 =newByteArrayOutputStream();
recorder =newFFmpegFrameRecorder(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
*/publicvoidwriteResponse(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();}}}/**
* 退出转换
*/publicvoidcloseConverter(){IOUtils.close(grabber);IOUtils.close(recorder);IOUtils.close(stream);}/**
* 关闭异步响应
*/publicvoidcompleteResponse(){Iterator<AsyncContext> it = outEntitys.iterator();while(it.hasNext()){AsyncContext o = it.next();
o.complete();}}@OverridepublicStringgetKey(){returnthis.key;}@OverridepublicStringgetUrl(){returnthis.url;}@OverridepublicvoidaddOutputStreamEntity(String key,AsyncContext entity)throwsIOException{if(headers ==null){
outEntitys.add(entity);}else{
entity.getResponse().getOutputStream().write(headers);
entity.getResponse().getOutputStream().flush();
outEntitys.add(entity);}}@Overridepublicvoidexit(){this.runing =false;try{this.join();}catch(Exception e){
log.error(e.getMessage(), e);}}}
service层
importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;/**
* @Description IFLVService
* @Author admin
* @Date 2024/6/18 10:24
*/publicinterfaceIFLVService{/**
* 打开一个流地址
*
* @param url
* @param response
*/publicvoidopen(String url,HttpServletResponse response,HttpServletRequest request);}
importcom.emergency.device.jy.factories.Converter;importcom.emergency.device.jy.factories.ConverterFactories;importcom.emergency.device.jy.service.IFLVService;importorg.apache.commons.compress.utils.Lists;importorg.springframework.stereotype.Service;importjavax.servlet.AsyncContext;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;importjava.security.MessageDigest;importjava.security.NoSuchAlgorithmException;importjava.util.List;importjava.util.concurrent.ConcurrentHashMap;importstaticcom.emergency.framework.datasource.DynamicDataSourceContextHolder.log;/**
* @Description FLVService
* @Author admin
* @Date 2024/6/18 10:34
*/@ServicepublicclassFLVServiceimplementsIFLVService{privateConcurrentHashMap<String,Converter> converters =newConcurrentHashMap<>();/**
* 打开一个流地址
*
* @param url
* @param response
* @param request
*/@Overridepublicvoidopen(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);thrownewIllegalArgumentException(e.getMessage());}}else{List<AsyncContext> outs =Lists.newArrayList();
outs.add(async);ConverterFactories c =newConverterFactories(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);}}publicStringmd5(String plainText){StringBuilder buf =null;try{MessageDigest md =MessageDigest.getInstance("MD5");
md.update(plainText.getBytes());byte b[]= md.digest();int i;
buf =newStringBuilder("");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></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
版权归原作者 偏执怪人。 所有, 如有侵权,请联系我们删除。