JavaCV转封装rtsp到rtmp(无需转码,低资源消耗)

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

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

用到的技术: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地址就是根据这个配置文件来的。

待优化之处:
1.如果服务部署在Docker环境下,本机ip是动态的,并非固定为127.0.0.1,所以需要动态获取nginx域名解析为ip,rtmp推送地址才能生效,可以使用InetAddress.getByName(www.baidu.com).getHostAddress();这样的方式获取解析到的ip地址。
2.目前出现的一个bug尚未解决,如果传入的设备ip填写错误,在JavaCV的FFmpegFrameGrabber构造器在调用start()方法是会出现阻塞现象,导致构造器无法释放,后续推流工作无法继续。

上述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:[email protected]:554/h264/ch1/main/av_stream -vcodec h264 -f flv -an rtmp://localhost:1935/live/room
2.ffmpeg -rtsp_transport tcp -i rtsp://admin:[email protected]: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>
原创文章 23 获赞 11 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_40777510/article/details/103764198
今日推荐