精华内容
下载资源
问答
  • 实测可用,支持一下。https://www.cnblogs.com/hyzs25/p/3644917.html

    实测可用,支持一下。https://www.cnblogs.com/kawhileonardfans/p/13044468.html

    展开全文
  • 纯java代码实现各种视频flv格式,支持在线播放。不依赖第三方jar包,转码效率高,速度快。
  • java实现rstp格式转换使用ffmpeg实现linux命令第一步安装node.js和ffmpeg第二步搭建node.js启动websocket接收服务第三步java实现(启动并挂起转换rtsp流) ...var http = require('http'); var WebSocket

    第一步安装node.js和ffmpeg

    安装可以参考网上的这里不一一介绍了
    

    第二步搭建node.js启动websocket接收服务

    部分代码:(要完整代码请关注我并私信给我)

    var fs = require('fs');
    var http = require('http');
    var WebSocket = require('ws');
    
    if (process.argv.length < 3) {
    	console.log('输入正确参数');
    	process.exit();
    }
    
    var stream_secret = process.argv[2];//密码
    var stream_port = process.argv[3] || 8081;//ffpeng推送端口
    var websocket_port = process.argv[4] || 8082;//前端websocket端口 ,比如:8082
    var record_stream = false;
    var totalSize = 0;
    
    

    第三步java实现(启动并挂起转换rtsp流)

    首先我们新建一个java项目,可以是普通的java,我这里新建的是springboot看起来比较高大上。

    1. 新建springboot项目
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述next下一步到这里项目新建好了;
    2. 上代码
    public class FfmpegTest {
        //自带map缓存,防止同一个设备机器多次转换导致视频丢帧
        public static Map<String , Integer> map = new HashMap<>();
        //线程池,因为开启转换是持续的多线程开启保证主线程不被阻塞
        ExecutorService es3 = Executors.newCachedThreadPool();
        @RequestMapping(value = "/hello/{code}",method = RequestMethod.GET)
        public String test(@PathVariable String code, HttpServletRequest request){
            //获取IP地址
            String ipAddress = IpUtil.getIpAddr(request);
            System.out.println("客户端ip:"+ipAddress);
            //转换rtsp流
            String ffmpeg = ffmpeg(code);
            //获取pid
            String pid = getPid(code);
            System.out.println(ffmpeg+"进程号是:"+pid);
            return ffmpeg+"进程号是:"+pid;
        }
        @RequestMapping(value = "/kill/{code}",method = RequestMethod.GET)
        public String kill(@PathVariable String code){
            String pid = getPid(code);
            System.out.println(code+"视频设备进程号是:"+pid);
            String s = killPid(pid , code);
            return code+"视频设备进程号是:"+pid+"   "+s;
        }
        public  String ffmpeg(String code) {
            String returnstr="";
            String s="";
            List<String> commend = new ArrayList<String>();
            commend.add("ffmpeg");
            commend.add("-i");
            commend.add("rtsp://用户:摄像头密码@192.168.1."+code+":554/h264/ch1/sub/av_stream");
            commend.add("-q");
            commend.add("0");
            commend.add("-f mpegts -codec:v mpeg1video -s 800x600");
            commend.add("http://192.168.1.142:8081/supersecret/live"+code+"");
            StringBuffer test=new StringBuffer();
            for(int i=0;i<commend.size();i++)
                test.append(commend.get(i)+" ");
            System.out.println(test);
            try {
                if (map.get(code)!=null && map.get(code)>=1){
                    map.put(code,map.get(code)+1);
                    returnstr+=code+"设备视频已经在转换列表里了";
                    System.out.println("视频已经在转换列表里了");
                }else{
                    //采用多线程处理防止主线程阻塞,一直等待
                    startStream(test.toString(),code);
                    map.put(code,1);
                    System.out.println("视频转换成功:");
                    returnstr+=code+"设备视频转换成功";
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return returnstr;
        }
        /**
         * 使用多线程执行linux的ffmpeg命令防止主线程阻塞
         * @param test 命令
         * @param code 对应的摄像投设备标志
         */
        private  void startStream(String test,String code) {
            //处理buffer的线程
            es3.submit(new Runnable() {
                @Override
                public void run() {
                    String line = null;
                    try {
                        Runtime rt = Runtime.getRuntime();
                        //执行linux命令
                        Process proc = rt.exec(test.toString());
                        //添加缓存防止多次转换
                        map.put(code,1);
                        //启用多线程消费正常日志防止内存小导致线程阻塞
                        clearStream(proc.getInputStream());
                        //启用多线程消费错误日志防止内存小导致线程阻塞
                        clearStream(proc.getErrorStream());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        /**
         * 开启多线程消费日志,防止转换缓冲区内存溢出
         * @param stream 输出流
         */
        private  void clearStream (InputStream stream) {
            //处理buffer的线程
            es3.submit(new Runnable() {
                @Override
                public void run() {
                    String line = null;
                    try (BufferedReader in = new BufferedReader(new InputStreamReader(stream));) {
                        while ((line = in.readLine()) != null) {
                            System.out.println(line);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        /**
         * 根据pid关闭进程
         * @param pid
         * @param code
         * @return
         */
        public static String killPid(String pid , String code) {
            BufferedReader reader = null;
            String pidStr="";
            try {
                // 显示所有进程
                Process process = Runtime.getRuntime().exec("kill -9 " + pid);
                pidStr+="成功关闭进程:"+pid;
                map.put(code,0);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException e) {
                    }
                }
            }
            return pidStr;
        }
        /**
         *根据名称获取linux启动的进程pid
         * @param pid 摄像头唯一标志开启进程时包含的标志
         * @return
         */
        public static String getPid(String pid) {
            BufferedReader reader = null;
            try {
                // 显示所有进程
                Process process = Runtime.getRuntime().exec("ps -ef");
                reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
                String line = null;
                while ((line = reader.readLine()) != null) {
                    //根据设备标志获取需要的线程pid
                    if (line.contains("rtsp://用户名:摄像头密码@192.168.1."+pid+":554/h264/ch1/sub")) {
                        //拿到第一个pid
                        String[] strs = line.split("\\s+");
                        return strs[1];
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (reader != null) {
                    try {
                        reader.close();
                    } catch (IOException e) {
                    }
                }
            }
            return null;
        }
        public static void main(String[] args) {
        }
    }
    
    1. 启动项目;
      启动springboot项目,然后浏览器访问项目我这里路径是: http://localhost:8097/hello/27此时已经把服务开启并且把流推送到websocket项目
      浏览器访问在这里插入图片描述
      文件代码如下
    <!DOCTYPE html>
    <html>
    <head>
    	<title>JSMpeg Stream Client</title>
    	<style type="text/css">
    		html, body {
    			text-align: center;
    		}
    	</style>
    
    </head>
    <body>
    	<canvas id="video-canvas1"></canvas>
    
    	<canvas id="video-canvas2"></canvas>
    
    	<canvas id="video-canvas3"></canvas>
    
    	<canvas id="video-canvas4"></canvas>
    
    	<canvas id="video-canvas5"></canvas>
    
    	<canvas id="video-canvas6"></canvas>
    
    	<canvas id="video-canvas7"></canvas>
    
    	<canvas id="video-canvas8"></canvas>
    
    	<canvas id="video-canvas9"></canvas>
    
    	<canvas id="canvas"></canvas>
    
    	<span id="jietu">截图</span>
    
    	<script type="text/javascript" src="jsmpeg.min.js"></script>
    
    	<script type="text/javascript" src="html2canvas.js"></script>
    
    	<script type="text/javascript" src="canvas2Image.js"></script>
    	<script type="text/javascript">
    		let canvas1 = document.getElementById('video-canvas1');
    		let url1 = 'ws://192.168.1.142:8082/live26';
    		let player1 = new JSMpeg.Player(url1, {canvas: canvas1});
    
    		let canvas2 = document.getElementById('video-canvas2');
    		let url2 = 'ws://192.168.1.142:8082/live24';
    		let player2 = new JSMpeg.Player(url2, {canvas: canvas2});
    
    		let canvas3 = document.getElementById('video-canvas3');
    		let url3 = 'ws://192.168.1.142:8082/live27';
    		let player3 = new JSMpeg.Player(url3, {canvas: canvas3});
    
    		let canvas4 = document.getElementById('video-canvas4');
    		let url4 = 'ws://192.168.1.142:8082/live33';
    		let player4 = new JSMpeg.Player(url4, {canvas: canvas4});
    
    		let canvas5 = document.getElementById('video-canvas5');
    		let url5 = 'ws://192.168.1.142:8082/live23';
    		let player5 = new JSMpeg.Player(url5, {canvas: canvas5});
    
    		let canvas6 = document.getElementById('video-canvas6');
    		let url6 = 'ws://192.168.1.142:8082/live30';
    		let player6 = new JSMpeg.Player(url6, {canvas: canvas6});
    		let jietu = document.getElementById("jietu");
    
    		jietu.addEventListener("click", function(){
    			/*var cntElem = document.getElementById("video-canvas1");
    			var shareContent = cntElem;//需要截图的包裹的(原生的)DOM 对象
    			var width = shareContent.offsetWidth; //获取dom 宽度
    			var height = shareContent.offsetHeight; //获取dom 高度
    			var canvas = document.getElementById("canvas"); //创建一个canvas节点
    			var scale = 2; //定义任意放大倍数 支持小数
    			canvas.width = width * scale; //定义canvas 宽度 * 缩放
    			canvas.height = height * scale; //定义canvas高度 *缩放
    			canvas.getContext("2d").scale(scale, scale); //获取context,设置scale
    			var opts = {
    				scale: scale, // 添加的scale 参数
    				canvas: canvas, //自定义 canvas
    				// logging: true, //日志开关,便于查看html2canvas的内部执行流程
    				width: width, //dom 原始宽度
    				height: height,
    				useCORS: true // 【重要】开启跨域配置
    			};
    			html2canvas(shareContent, opts).then(function (canvas) {
    				var context = canvas.getContext('2d');
    				// 【重要】关闭抗锯齿
    				context.mozImageSmoothingEnabled = false;
    				context.webkitImageSmoothingEnabled = false;
    				context.msImageSmoothingEnabled = false;
    				context.imageSmoothingEnabled = false;
    				// 【重要】默认转化的格式为png,也可设置为其他格式
    				var img = Canvas2Image.saveAsPNG(canvas, canvas.width, canvas.height);
    				document.body.appendChild(img);
    				img.css = {
    					"width": canvas.width / 2 + "px",
    					"height": canvas.height / 2 + "px",
    					"position":"fixed",
    					"top":"0",
    					"left":"0",
    					"opacity":"1",
    					"z-index":222
    				}
    			});*/
    
    			downloadFile('download', document.getElementById("video-canvas1").toDataURL());
    		});
    
    		function downloadFile(fileName, content) {
    			let aLink = document.createElement('a');
    			let blob = this.base64ToBlob(content); //new Blob([content]);
    
    			let evt = document.createEvent("HTMLEvents");
    			evt.initEvent("click", true, true);//initEvent 不加后两个参数在FF下会报错  事件类型,是否冒泡,是否阻止浏览器的默认行为
    			aLink.download = fileName;
    			aLink.href = URL.createObjectURL(blob);
    
    			// aLink.dispatchEvent(evt);
    			aLink.click()
    		}
    		function base64ToBlob(code) {
    			let parts = code.split(';base64,');
    			let contentType = parts[0].split(':')[1];
    			let raw = window.atob(parts[1]);
    			let rawLength = raw.length;
    
    			let uInt8Array = new Uint8Array(rawLength);
    
    			for (let i = 0; i < rawLength; ++i) {
    				uInt8Array[i] = raw.charCodeAt(i);
    			}
    			return new Blob([uInt8Array], { type: contentType });
    		}
    
    		/*for (let i = 1; i <= 9; i++){
    			let canvas = document.getElementById('video-canvas' + i);
    			let url = 'ws://127.0.0.1:8082/live' + i;
    			let player = new JSMpeg.Player(url, {canvas: canvas});
    		}*/
    
    	</script>
    </body>
    </html>
    
    
    1. 浏览器就可以实时看到监控了;
    2. 注意事项和细节代码注释。觉得还行关注和打赏一下。有需要可以添加我的qq:794129243
      本文是原创未经允许不得用于商业用途涉及法律效应,一概不负责。转载请标明出处。
    展开全文
  • FFmpeg封装rtsp到rtmp(无需转码,低资源消耗)目录结构添加依赖,编写配置文件创建Bean创建缓存Cache修改启动类拉流、推流、封装定时任务Timer线程池管理编写controller1.开启视频流接口(POST)2.关闭视频流...

    项目码云(Gitee)地址:https://gitee.com/banmajio/RTSPtoRTMP
    项目github地址:https://github.com/banmajio/RTSPtoRTMP
    个人博客:banmajio’s blog

    浏览器不支持flash插件之后,h5播放rtmp直播流的解决方案

    参考:javaCV开发详解之8:转封装在rtsp转rtmp流中的应用(无须转码,更低的资源消耗)

    【注意】该项目只能用来实现直播的rtsp转rtmp,无法满足回放需求。对于海康设备来说,rtsp指令带参数进行回放,会发生报:带宽不足的问题。所以需要回放功能的请对海康sdk进行二次开发,手动捕获码流数据转封装为rtmp流。实现思路请参考:海康sdk捕获码流数据通过JavaCV推成rtmp流的实现思路(PS流转封装RTMP) 海康sdk二次开发推rtmp流的项目已经开发优化完成,但暂不考虑开源,有需要的请联系q:1402325991 有偿!! 介意勿扰!!

    用到的技术:FFmpeg、JavaCV、ngingx
    项目背景:将海康摄像头的rtsp流转为rtmp流,配合video.js实现web端播放。
    [注]:该项目中的一些处理是为了满足公司项目需求添加完善的,如果需要改造扩展只需要在原来的基础上进行扩充或者剥离即可。最基本的核心操作在CameraPush.java这个类中,或者参考上述链接原作者的代码。

    该项目需要搭配使用的nginx服务器下载地址:http://cdn.banmajio.com/nginx.rar
    下载后解压该文件,点击nginx.exe(闪退是正常的,可以通过任务管理器查看是否存在nginx进程,存在则说明启动成功了)启动nginx服务。
    nginx的配置文件存放在conf目录下的nginx.conf,根据需要修改。项目中的rtmp地址就是根据这个配置文件来的。

    上述bug优化1:JavaCV中FFmpegFrameGrabber调用start()方法时出现阻塞的解决办法

    项目github地址:https://github.com/banmajio/RTSPtoRTMP
    个人博客:banmajio’s blog

    目录结构

    目录结构

    1.com.junction包里的类为SpringBoot项目启动类。
    2.com.junction.cache包里的类为保存推流信息的缓存类。
    3.com.junction.controller包里的类为项目controller API接口。
    4.com.junction.pojo包里的类为相机信息和配置文件映射的bean。
    5.com,junction.thread包里的类为线程池管理类。
    6.com.junction.util包里的类为拉流推流业务处理类和定时任务Timer类。
    7.application.yml为项目配置文件。

    添加依赖,编写配置文件

    1.添加依赖,引入javacpp和ffmpeg的jar包。

    		<!-- javacv1.5.1 -->
    		<dependency>
    			<groupId>org.bytedeco</groupId>
    			<artifactId>javacv</artifactId>
    			<version>1.5.1</version>
    		</dependency>
    		<dependency>
    			<groupId>org.bytedeco</groupId>
    			<artifactId>ffmpeg-platform</artifactId>
    			<version>4.1.3-1.5.1</version>
    		</dependency>
    		<!-- 支持 @ConfigurationProperties 注解 -->
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-configuration-processor</artifactId>
    			<optional>true</optional>
    		</dependency>
    

    2.pom中引入的spring-boot-configuration-processor是为了将配置文件映射为bean,方便项目中使用配置文件中的值

    server:
      port: 8082
      servlet:
       context-path: /camera  
      
    config:
    #直播流保活时间(分钟)
      keepalive: 5
    #nginx推送地址
      push_ip: 127.0.0.1
    #nginx推送端口
      push_port: 1935
    

    创建Bean

    1.CameraPojo(相机信息)

    	private String username;// 摄像头账号
    	private String password;// 摄像头密码
    	private String ip;// 摄像头ip
    	private String channel;// 摄像头通道号
    	private String stream;// 摄像头码流(main为主码流、sub为子码流)
    	private String rtsp;// rtsp地址
    	private String rtmp;// rtmp地址
    	private String startTime;// 回放开始时间
    	private String endTime;// 回放结束时间
    	private String openTime;// 打开时间
    	private int count = 0;// 使用人数
    	private String token;//唯一标识token
    

    2.Config(读取配置文件的bean)

    package com.junction.pojo;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.stereotype.Component;
    
    /**
     * @Title ConfigPojo.java
     * @description 读取配置文件的bean
     * @time 2019年12月25日 下午5:11:21
     * @author wuguodong
     **/
    @Component
    //读取application.yml中config层级下的配置项
    @ConfigurationProperties(prefix = "config")
    public class Config {
    	private String keepalive;//保活时长(分钟)
    	private String push_ip;//推送地址
    	private String push_port;//推送端口
    	
    	public String getKeepalive() {
    		return keepalive;
    	}
    	public void setKeepalive(String keepalive) {
    		this.keepalive = keepalive;
    	}
    	public String getPush_ip() {
    		return push_ip;
    	}
    	public void setPush_ip(String push_ip) {
    		this.push_ip = push_ip;
    	}
    	public String getPush_port() {
    		return push_port;
    	}
    	public void setPush_port(String push_port) {
    		this.push_port = push_port;
    	}
    	@Override
    	public String toString() {
    		return "Config [keepalive=" + keepalive + ", push_ip=" + push_ip + ", push_port=" + push_port + "]";
    	}	
    }
    

    创建缓存Cache

    保存推流信息,与服务启动的时间。

    /**
     * @Title CacheUtil.java
     * @description 推流缓存信息
     * @time 2019年12月17日 下午3:12:45
     * @author wuguodong
     **/
    public final class CacheUtil {
    	/*
    	 * 保存已经开始推的流
    	 */
    	public static Map<String, CameraPojo> STREAMMAP = new ConcurrentHashMap<String, CameraPojo>();
    
    	/*
    	 * 保存服务启动时间
    	 */
    	public static long STARTTIME;
    }
    

    修改启动类

    项目启动时,将启动时间存入缓存中;项目结束时,销毁线程池和定时器,释放资源。

    package com.junction;
    
    import java.util.Date;
    
    import javax.annotation.PreDestroy;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    import com.junction.cache.CacheUtil;
    import com.junction.thread.CameraThread;
    import com.junction.util.TimerUtil;
    
    @SpringBootApplication
    public class CameraServerApplication {
    
    	public static void main(String[] args) {
    		//将服务启动时间存入缓存
    		CacheUtil.STARTTIME = new Date().getTime();
    		SpringApplication.run(CameraServerApplication.class, args);
    	}
    
    	@PreDestroy
    	public void destory() {
    		System.err.println("释放空间...");
    		// 关闭线程池
    		CameraThread.MyRunnable.es.shutdownNow();
    		// 销毁定时器
    		TimerUtil.timer.cancel();
    	}
    }
    
    

    拉流、推流、转封装

    1.两个重要构造器FFmpegFrameGrabberFFmpegFrameRecorder
    2.转封装不涉及转码,所以资源占用很低。

    什么是转封装?为什么转封装比转码消耗更少?为什么转封装无法改动视频尺寸?
    先举个栗子:假设视频格式(mp4,flv,avi等)是盒子,里面的视频编码数据(h264,hevc)是苹果,我们把这个苹果从盒子里取出来放到另一个盒子里,盒子是变了,苹果是没有变动的,因此视频相关的尺寸数据是没有改动的,这个就是转封装的概念。
    有了上面这个例子,我们可以把“转码”理解为:把这个盒子里的苹果(hevc)拿出来削皮切块后再加工成樱桃(h264)后再装到另一个盒子里,多了一步对苹果(hevc)转换为樱桃(h264)的操作,自然比直接把苹果拿到另一个盒子(转封装)要消耗更多机器性能。

    
    import static org.bytedeco.ffmpeg.global.avcodec.av_packet_unref;
    
    import org.bytedeco.ffmpeg.avcodec.AVPacket;
    import org.bytedeco.ffmpeg.avformat.AVFormatContext;
    import org.bytedeco.javacv.FFmpegFrameGrabber;
    import org.bytedeco.javacv.FFmpegFrameRecorder;
    
    import com.junction.pojo.CameraPojo;
    
    /**
     * @Title CameraPush.java
     * @description 拉流推流
     * @time 2019年12月16日 上午9:34:41
     * @author wuguodong
     **/
    public class CameraPush {
    	protected FFmpegFrameGrabber grabber = null;// 解码器
    	protected FFmpegFrameRecorder record = null;// 编码器
    	int width;// 视频像素宽
    	int height;// 视频像素高
    
    	// 视频参数
    	protected int audiocodecid;
    	protected int codecid;
    	protected double framerate;// 帧率
    	protected int bitrate;// 比特率
    
    	// 音频参数
    	// 想要录制音频,这三个参数必须有:audioChannels > 0 && audioBitrate > 0 && sampleRate > 0
    	private int audioChannels;
    	private int audioBitrate;
    	private int sampleRate;
    
    	// 设备信息
    	private CameraPojo cameraPojo;
    
    	public CameraPush(CameraPojo cameraPojo) {
    		this.cameraPojo = cameraPojo;
    	}
    	/**
    	 * 选择视频源
    	 * 
    	 * @author wuguodong
    	 * @throws Exception
    	 */
    	public CameraPush from() throws Exception {
    		// 采集/抓取器
    		System.out.println(cameraPojo.getRtsp());
    		grabber = new FFmpegFrameGrabber(cameraPojo.getRtsp());
    		if (cameraPojo.getRtsp().indexOf("rtsp") >= 0) {
    			grabber.setOption("rtsp_transport", "tcp");// tcp用于解决丢包问题
    		}
    		// 设置采集器构造超时时间
    		grabber.setOption("stimeout", "2000000");
    		grabber.start();// 开始之后ffmpeg会采集视频信息,之后就可以获取音视频信息
    		width = grabber.getImageWidth();
    		height = grabber.getImageHeight();
    		// 若视频像素值为0,说明采集器构造超时,程序结束
    		if (width == 0 && height == 0) {
    			System.err.println("[ERROR]   拉流超时...");
    			return null;
    		}
    		// 视频参数
    		audiocodecid = grabber.getAudioCodec();
    		System.err.println("音频编码:" + audiocodecid);
    		codecid = grabber.getVideoCodec();
    		framerate = grabber.getVideoFrameRate();// 帧率
    		bitrate = grabber.getVideoBitrate();// 比特率
    		// 音频参数
    		// 想要录制音频,这三个参数必须有:audioChannels > 0 && audioBitrate > 0 && sampleRate > 0
    		audioChannels = grabber.getAudioChannels();
    		audioBitrate = grabber.getAudioBitrate();
    		if (audioBitrate < 1) {
    			audioBitrate = 128 * 1000;// 默认音频比特率
    		}
    		return this;
    	}
    	/**
    	 * 选择输出
    	 * 
    	 * @author wuguodong
    	 * @throws Exception
    	 */
    	public CameraPush to() throws Exception {
    		// 录制/推流器
    		record = new FFmpegFrameRecorder(cameraPojo.getRtmp(), width, height);
    		record.setVideoOption("crf", "28");// 画面质量参数,0~51;18~28是一个合理范围
    		record.setGopSize(2);
    		record.setFrameRate(framerate);
    		record.setVideoBitrate(bitrate);
    
    		record.setAudioChannels(audioChannels);
    		record.setAudioBitrate(audioBitrate);
    		record.setSampleRate(sampleRate);
    		AVFormatContext fc = null;
    		if (cameraPojo.getRtmp().indexOf("rtmp") >= 0 || cameraPojo.getRtmp().indexOf("flv") > 0) {
    			// 封装格式flv
    			record.setFormat("flv");
    			record.setAudioCodecName("aac");
    			record.setVideoCodec(codecid);
    			fc = grabber.getFormatContext();
    		}
    		record.start(fc);
    		return this;
    	}
    
    	/**
    	 * 转封装
    	 * 
    	 * @author wuguodong
    	 * @throws org.bytedeco.javacv.FrameGrabber.Exception
    	 * @throws org.bytedeco.javacv.FrameRecorder.Exception
    	 * @throws InterruptedException
    	 */
    	public CameraPush go(Thread nowThread)
    			throws org.bytedeco.javacv.FrameGrabber.Exception, org.bytedeco.javacv.FrameRecorder.Exception {
    		long err_index = 0;// 采集或推流导致的错误次数
    		// 连续五次没有采集到帧则认为视频采集结束,程序错误次数超过5次即中断程序
    		//将探测时留下的数据帧释放掉,以免因为dts,pts的问题对推流造成影响
    		grabber.flush();
    		for (int no_frame_index = 0; no_frame_index < 5 || err_index < 5;) {
    			try {
    				// 用于中断线程时,结束该循环
    				nowThread.sleep(1);
    				AVPacket pkt = null;
    				// 获取没有解码的音视频帧
    				pkt = grabber.grabPacket();
    				if (pkt == null || pkt.size() <= 0 || pkt.data() == null) {
    					// 空包记录次数跳过
    					no_frame_index++;
    					err_index++;
    					continue;
    				}
    				// 不需要编码直接把音视频帧推出去
    				err_index += (record.recordPacket(pkt) ? 0 : 1);
    				av_packet_unref(pkt);
    			} catch (InterruptedException e) {
    				// 当需要结束推流时,调用线程中断方法,中断推流的线程。当前线程for循环执行到
    				// nowThread.sleep(1);这行代码时,因为线程已经不存在了,所以会捕获异常,结束for循环
    				// 销毁构造器
    				grabber.close();
    				record.close();
    				System.err.println("设备中断推流成功...");
    				break;
    			} catch (org.bytedeco.javacv.FrameGrabber.Exception e) {
    				err_index++;
    			} catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
    				err_index++;
    			}
    		}
    		// 程序正常结束销毁构造器
    		grabber.close();
    		record.close();
    		System.err.println("设备推流完毕...");
    		return this;
    	}
    }
    

    定时任务Timer

    定时任务用来执行两部分操作:
    1.定时检查正在推流的通道使用人数,如果该通道当前使用人数为0,则中断线程,结束该路视频推流并清除缓存。
    2.定时检查正在推流的通道最后打开请求时间,如果与当前时间超过配置的保活时间时,则结束推流,并清除缓存。
    当前设置的定时任务执行间隔为1分钟,可自行修改。

    package com.junction.util;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.Set;
    import java.util.Timer;
    import java.util.TimerTask;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.stereotype.Component;
    
    import com.junction.cache.CacheUtil;
    import com.junction.controller.CameraController;
    import com.junction.pojo.Config;
    
    /**
     * @Title TimerUtil.java
     * @description 定时任务
     * @time 2019年12月16日 下午3:10:08
     * @author wuguodong
     **/
    @Component
    public class TimerUtil implements CommandLineRunner {
    
    	@Autowired
    	private Config config;// 配置文件bean
    
    	public static Timer timer;
    
    	@Override
    	public void run(String... args) throws Exception {
    		// 超过5分钟,结束推流
    		timer = new Timer("timeTimer");
    		timer.schedule(new TimerTask() {
    			@Override
    			public void run() {
    				System.err.println("开始执行定时任务...");
    				// 管理缓存
    				if (null != CacheUtil.STREAMMAP && 0 != CacheUtil.STREAMMAP.size()) {
    					Set<String> keys = CacheUtil.STREAMMAP.keySet();
    					for (String key : keys) {
    						try {
    							// 最后打开时间
    							long openTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    									.parse(CacheUtil.STREAMMAP.get(key).getOpenTime()).getTime();
    							// 当前系统时间
    							long newTime = new Date().getTime();
    							// 如果通道使用人数为0,则关闭推流
    							if (CacheUtil.STREAMMAP.get(key).getCount() == 0) {
    								// 结束线程
    								CameraController.jobMap.get(key).setInterrupted();
    								// 清除缓存
    								CacheUtil.STREAMMAP.remove(key);
    								CameraController.jobMap.remove(key);
    							} else if ((newTime - openTime) / 1000 / 60 > Integer.valueOf(config.getKeepalive())) {
    								CameraController.jobMap.get(key).setInterrupted();
    								CameraController.jobMap.remove(key);
    								CacheUtil.STREAMMAP.remove(key);
    								System.err.println("[定时任务]  关闭" + key + "摄像头...");
    							}
    						} catch (ParseException e) {
    							e.printStackTrace();
    						}
    					}
    				}
    				System.err.println("定时任务执行完毕...");
    			}
    		}, 1, 1000 * 60);
    	}
    }
    

    线程池管理

    package com.junction.thread;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    import com.junction.cache.CacheUtil;
    import com.junction.controller.CameraController;
    import com.junction.pojo.CameraPojo;
    import com.junction.util.CameraPush;
    
    /**
     * @Title CameraThread.java
     * @description TODO
     * @time 2019年12月16日 上午9:32:43
     * @author wuguodong
     **/
    public class CameraThread {
    	public static class MyRunnable implements Runnable {
    		// 创建线程池
    		public static ExecutorService es = Executors.newCachedThreadPool();
    
    		private CameraPojo cameraPojo;
    		private Thread nowThread;
    
    		public MyRunnable(CameraPojo cameraPojo) {
    			this.cameraPojo = cameraPojo;
    		}
    
    		// 中断线程
    		public void setInterrupted() {
    			nowThread.interrupt();
    		}
    
    		@Override
    		public void run() {
    			// 直播流
    			try {
    				// 获取当前线程存入缓存
    				nowThread = Thread.currentThread();
    				CacheUtil.STREAMMAP.put(cameraPojo.getToken(), cameraPojo);
    				// 执行转流推流任务
    				CameraPush push = new CameraPush(cameraPojo).from();
    				if (push != null) {
    					push.to().go(nowThread);
    				}
    				// 清除缓存
    				CacheUtil.STREAMMAP.remove(cameraPojo.getToken());
    				CameraController.jobMap.remove(cameraPojo.getToken());
    			} catch (Exception e) {
    				System.err.println(
    						"当前线程:" + Thread.currentThread().getName() + " 当前任务:" + cameraPojo.getRtsp() + "停止...");
    				CacheUtil.STREAMMAP.remove(cameraPojo.getToken());
    				CameraController.jobMap.remove(cameraPojo.getToken());
    				e.printStackTrace();
    			}
    		}
    	}
    }
    

    编写controller

    controller提供了五个接口,使用RESTful风格,故使用postman等软件测试时,选择相应的类型。
    1.获取视频服务配置信息及服务运行时间
    api: http://127.0.0.1:8082/camera/status (GET)
    2.获取正在推送的所有视频流信息
    api: http://127.0.0.1:8082/camera/cameras (GET)
    3.开启视频流(直播or回放)
    api: http://127.0.0.1:8082/camera/cameras (POST)
    params: ip;username;password;channel;stream;starttime;endtime
    4.关闭视频流
    api: http://127.0.0.1:8082/camera/cameras/:tokens (DELETE)
    5.视频流保活
    api: http://127.0.0.1:8082/camera/cameras/:tokens (PUT)

    1.开启视频流接口(POST)

    先校验参数,然后判断缓存是否为空(如果为空说明目前没有推流任务,否则遍历缓存,通过参数判断当前通道是否在推流。如果找到,则该路视频的bean内人数count+1,反之调用openStream()方法进行推流)。
    

    openStream()方法内先判断是否存在starttime参数,如果有则说明该流为历史流;在判断是否存在endtime,若无endtime则使用starttime前后各加一分钟作为历史流的开始时间和结束时间。若无starttime则视为该流为直播流。ffmpeg在拉取rtsp直播流和历史流时的命令不相同,所以需要上述判断!!

    通过openStream()组装rtsp命令和rtmp命令以及UUID生成的token和其他参数,set进cameraPojo中。提交当前任务到线程池,并将当前任务线程存入jobMap(存放推流线程任务的缓存)中。

    		// 执行任务
    		CameraThread.MyRunnable job = new CameraThread.MyRunnable(cameraPojo);
    		CameraThread.MyRunnable.es.execute(job);
    		jobMap.put(token, job);
    

    ffmpeg直播流与历史流命令格式:
    1.ffmpeg -rtsp_transport tcp -i rtsp://admin:abc12345@192.168.1.8:554/h264/ch1/main/av_stream -vcodec h264 -f flv -an rtmp://localhost:1935/live/room
    2.ffmpeg -rtsp_transport tcp -i rtsp://admin:abc12345@192.168.1.222:554/Streaming/tracks/101?starttime=20191227t084400z’&'endtime=20191227t084600z -vcodec copy -acodec copy -f flv rtmp://localhost:1935/history/room

    /**
    	 * @Title: openCamera
    	 * @Description: 开启视频流
    	 * @param ip
    	 * @param username
    	 * @param password
    	 * @param channel   通道
    	 * @param stream    码流
    	 * @param starttime
    	 * @param endtime
    	 * @return Map<String,String>
    	 **/
    	@RequestMapping(value = "/cameras", method = RequestMethod.POST)
    	public Map<String, String> openCamera(String ip, String username, String password, String channel, String stream,
    			String starttime, String endtime) {
    		// 返回结果
    		Map<String, String> map = new HashMap<String, String>();
    		// 校验参数
    		if (null != ip && "" != ip && null != username && "" != username && null != password && "" != password
    				&& null != channel && "" != channel) {
    			CameraPojo cameraPojo = new CameraPojo();
    			// 获取当前时间
    			String openTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date().getTime());
    			Set<String> keys = CacheUtil.STREAMMAP.keySet();
    			// 缓存是否为空
    			if (0 == keys.size()) {
    				// 开始推流
    				cameraPojo = openStream(ip, username, password, channel, stream, starttime, endtime, openTime);
    				map.put("token", cameraPojo.getToken());
    				map.put("url", cameraPojo.getRtmp());
    			} else {
    				// 是否存在的标志;0:不存在;1:存在
    				int sign = 0;
    				for (String key : keys) {
    					// 是否已经在推流
    					if (ip.equals(CacheUtil.STREAMMAP.get(key).getIp())
    							&& channel.equals(CacheUtil.STREAMMAP.get(key).getChannel())) {
    						cameraPojo = CacheUtil.STREAMMAP.get(key);
    						sign = 1;
    						break;
    					}
    				}
    				if (sign == 1) {
    					cameraPojo.setCount(cameraPojo.getCount() + 1);
    					cameraPojo.setOpenTime(openTime);
    				} else {
    					// 开始推流
    					cameraPojo = openStream(ip, username, password, channel, stream, starttime, endtime, openTime);
    				}
    				map.put("token", cameraPojo.getToken());
    				map.put("url", cameraPojo.getRtmp());
    			}
    		}
    
    		return map;
    	}
    
    	/**
    	 * @Title: openStream
    	 * @Description: 推流器
    	 * @param ip
    	 * @param username
    	 * @param password
    	 * @param channel
    	 * @param stream
    	 * @param starttime
    	 * @param endtime
    	 * @param openTime
    	 * @return
    	 * @return CameraPojo
    	 **/
    	private CameraPojo openStream(String ip, String username, String password, String channel, String stream,
    			String starttime, String endtime, String openTime) {
    		CameraPojo cameraPojo = new CameraPojo();
    		// 生成token
    		String token = UUID.randomUUID().toString();
    		String rtsp = "";
    		String rtmp = "";
    		// 历史流
    		if (null != starttime && "" != starttime) {
    			if (null != endtime && "" != endtime) {
    				rtsp = "rtsp://" + username + ":" + password + "@" + ip + ":554/Streaming/tracks/" + channel
    						+ "01?starttime=" + starttime.substring(0, 8) + "t" + starttime.substring(8) + "z'&'endtime="
    						+ endtime.substring(0, 8) + "t" + endtime.substring(8) + "z";
    			} else {
    				try {
    					SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
    					String startTime = df.format(df.parse(starttime).getTime() - 60 * 1000);
    					String endTime = df.format(df.parse(starttime).getTime() + 60 * 1000);
    					rtsp = "rtsp://" + username + ":" + password + "@" + ip + ":554/Streaming/tracks/" + channel
    							+ "01?starttime=" + startTime.substring(0, 8) + "t" + startTime.substring(8)
    							+ "z'&'endtime=" + endTime.substring(0, 8) + "t" + endTime.substring(8) + "z";
    				} catch (ParseException e) {
    					e.printStackTrace();
    				}
    			}
    			rtmp = "rtmp://" + config.getPush_ip() + ":" + config.getPush_port() + "/history/" + token;
    		} else {// 直播流
    			rtsp = "rtsp://" + username + ":" + password + "@" + ip + ":554/h264/ch" + channel + "/" + stream
    					+ "/av_stream";
    			rtmp = "rtmp://" + config.getPush_ip() + ":" + config.getPush_port() + "/live/" + token;
    		}
    
    		cameraPojo.setUsername(username);
    		cameraPojo.setPassword(password);
    		cameraPojo.setIp(ip);
    		cameraPojo.setChannel(channel);
    		cameraPojo.setStream(stream);
    		cameraPojo.setRtsp(rtsp);
    		cameraPojo.setRtmp(rtmp);
    		cameraPojo.setOpenTime(openTime);
    		cameraPojo.setCount(1);
    		cameraPojo.setToken(token);
    
    		// 执行任务
    		CameraThread.MyRunnable job = new CameraThread.MyRunnable(cameraPojo);
    		CameraThread.MyRunnable.es.execute(job);
    		jobMap.put(token, job);
    
    		return cameraPojo;
    	}
    

    2.关闭视频流接口(DELETE)

    传入参数为tokens,通过,分隔,可以同时关闭多路视频。通过token查找缓存判断是否存在,如果存在,则人数count-1。不直接调用结束线程的方法是为了满足如果多个客户端同时观看该路视频,一人关闭会影响其他人使用。故调用该接口只是使该路视频的使用人数-1,最终结束线程的操作交由定时任务处理,如果定时器查询到视频使用人数的count为0,则结束该路视频的推流操作,并清除缓存。

    /**
    	 * @Title: closeCamera
    	 * @Description:关闭视频流
    	 * @param tokens
    	 * @return void
    	 **/
    	@RequestMapping(value = "/cameras/{tokens}", method = RequestMethod.DELETE)
    	public void closeCamera(@PathVariable("tokens") String tokens) {
    		if (null != tokens && "" != tokens) {
    			String[] tokenArr = tokens.split(",");
    			for (String token : tokenArr) {
    				if (jobMap.containsKey(token) && CacheUtil.STREAMMAP.containsKey(token)) {
    					if (0 < CacheUtil.STREAMMAP.get(token).getCount()) {
    						// 人数-1
    						CacheUtil.STREAMMAP.get(token).setCount(CacheUtil.STREAMMAP.get(token).getCount() - 1);
    					}
    				}
    			}
    		}
    	}
    

    3.获取视频流(GET)

    获取当前进行的推流任务。

    /**
    	 * @Title: getCameras
    	 * @Description:获取视频流
    	 * @return Map<String, CameraPojo>
    	 **/
    	@RequestMapping(value = "/cameras", method = RequestMethod.GET)
    	public Map<String, CameraPojo> getCameras() {
    		return CacheUtil.STREAMMAP;
    	}
    

    4.视频流保活(PUT)

    视频流保活的作用是为了应付以下场景:
    如果客户端比如浏览器直接关闭掉,并不会通知服务客户已经不再观看视频了,这是服务还在进行推流。所以添加保活机制,如果客户端没有触发保活机制,定时任务执行时,如果该路视频的最后打开时间距当前时间超过配置的保活时间时,关闭该路视频的推流任务。如果客户端触发保活机制时,更新该路视频的最后打开时间(opentime)为当前系统时间。

    /**
    	 * @Title: keepAlive
    	 * @Description:视频流保活
    	 * @param tokens
    	 * @return void
    	 **/
    	@RequestMapping(value = "/cameras/{tokens}", method = RequestMethod.PUT)
    	public void keepAlive(@PathVariable("tokens") String tokens) {
    		// 校验参数
    		if (null != tokens && "" != tokens) {
    			String[] tokenArr = tokens.split(",");
    			for (String token : tokenArr) {
    				CameraPojo cameraPojo = new CameraPojo();
    				// 直播流token
    				if (null != CacheUtil.STREAMMAP.get(token)) {
    					cameraPojo = CacheUtil.STREAMMAP.get(token);
    					// 更新当前系统时间
    					cameraPojo.setOpenTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date().getTime()));
    				}
    			}
    		}
    	}
    

    5.获取服务信息(GET)

    通过该接口获取服务运行时间,以及配置文件的配置

    	/**
    	 * @Title: getConfig
    	 * @Description: 获取服务信息
    	 * @return Map<String, Object>
    	 **/
    	@RequestMapping(value = "/status", method = RequestMethod.GET)
    	public Map<String, Object> getConfig() {
    		// 获取当前时间
    		long nowTime = new Date().getTime();
    		String upTime = (nowTime - CacheUtil.STARTTIME) / (1000 * 60 * 60) + "时"
    				+ (nowTime - CacheUtil.STARTTIME) % (1000 * 60 * 60) / (1000 * 60) + "分";
    		Map<String, Object> status = new HashMap<String, Object>();
    		status.put("config", config);
    		status.put("uptime", upTime);
    		return status;
    	}
    

    6.video.js

    测试需要的video.js。video.js用来播放rtmp的视频。注意chrome需要先允许加载flash插件(百度一下很简单的)。使用以下代码,在src处添加推流成功的rtmp地址。

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <title>Video.js | HTML5 Video Player</title>
    <link href="http://vjs.zencdn.net/5.20.1/video-js.css" rel="stylesheet">
    <script src="http://vjs.zencdn.net/5.20.1/videojs-ie8.min.js"></script>
    </head>
    <body width="640px" height="360px">
    
    	<video id="example_video_1" class="video-js vjs-default-skin" controls
    		preload="auto" width="640px" height="360px" data-setup="{}"
    		style="float: left">
    		<source src="此处填入rtmp地址" type="rtmp/flv">
    		<p class="vjs-no-js">
    			To view this video please enable JavaScript, and consider upgrading
    			to a web browser that <a
    				href="http://videojs.com/html5-video-support/" target="_blank">supports
    				HTML5 video</a>
    		</p>
    	</video>
    </body>
    </html>
    
    展开全文
  • 我们现在已经用java实现了rtsp/rtp over Tcp、Udp 了,现在要实现over http,请教java服务如何实现over http
  • java RTP-RTSP

    2019-04-16 11:23:39
    java RTP RTSP 库 没有实测过 下载请谨慎!!! 更多RTP库查看博客: https://blog.csdn.net/qq_41054313/article/details/88716995 RTSP库暂时没有...

空空如也

空空如也

1
收藏数 6
精华内容 2
关键字:

javartsp转http

java 订阅