基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案 背景 方案 实现

各大监控视频平台厂商与外对接均是基于IE的OCX插件方式提供实时视频查看、历史视频回放与历史视频下载。在h5已大行其道的当下,基于IE的OCX插件方式已满足不了广大客户的实际需求,因此需要一个兼容各大主流浏览器与手机浏览的监控视频处理方案。

方案

red5是基于Flash的流媒体服务的一款基于Java的开源流媒体服务器。

ffmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。

本方案利用Red5发布RTMP流媒体服务器,向外提供实时、历史的RTMP推流;利用FFmpeg实现RTSP当作源推送到RTMP服务器;基于jsplayer实现视频展示。

具体细节上代码:

安装Red5下载地址:https://github.com/Red5/red5-server,如不了具体安装步骤请自行百度。

安装ffmpeg,下载地址:https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-20180325-5b31dd1-win64-static.zip,如不了具体安装步骤请自行百度。

实现

构建基于Red5的Web项目

 基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

target runtime 选择 new runtime

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

选择Red5并next

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

选择jdk1.8 ,把red5目录指向,我们解压的red5 server文件夹

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

点击Finish

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

勾选red5 application generation

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

点击Finish,经过以上步骤基于Red5的Web项目已构建成功。项目结构如下:

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

搭建Red5服务器

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

右键New->Server

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

选择Red5,并Next

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

修改对应目录选择Red5并next,点击Finish,此时Red5服务器已搭建完成。

在WebContent目录下创建streams文件夹,streams目录下存放mp4或flv格式的视频文件,发布到Red5中即可实现历史视频的RTMP推送。

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

基于以上的项目修改为maven项目,新建maven项目名称为MyVideo并中添加上图的web.xml、red5-web.xml、red5-web.properties、Application.java并修改相应配置,具体见下图,

基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

其中web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	>


	<!-- The display-name element contains a short name that is intended to 
		be displayed by tools. The display name need not be unique. -->
	<display-name>MyVideo</display-name>

	<!-- The context-param element contains the declaration of a web application's 
		servlet context initialization parameters. -->
	<context-param>
		<param-name>webAppRootKey</param-name>
		<param-value>/MyVideo</param-value>
	</context-param>

	<listener>
		<listener-class>org.red5.logging.ContextLoggingListener</listener-class>
	</listener>

	<filter>
		<filter-name>LoggerContextFilter</filter-name>
		<filter-class>org.red5.logging.LoggerContextFilter</filter-class>
	</filter>

	<filter-mapping>
		<filter-name>LoggerContextFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

	<!-- remove the following servlet tags if you want to disable remoting for 
		this application -->
	<servlet>
		<servlet-name>gateway</servlet-name>
		<servlet-class>org.red5.server.net.servlet.AMFGatewayServlet</servlet-class>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<filter>
		<filter-name>encodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>encodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

	<!-- The servlet-mapping element defines a mapping between a servlet and 
		a url pattern -->
	<servlet-mapping>
		<servlet-name>gateway</servlet-name>
		<url-pattern>/gateway</url-pattern>
	</servlet-mapping>

	<!-- The security-constraint element is used to associate security constraints 
		with one or more web resource collections -->
	<security-constraint>
		<web-resource-collection>
			<web-resource-name>Forbidden</web-resource-name>
			<url-pattern>/streams/*</url-pattern>
		</web-resource-collection>
		<auth-constraint />
	</security-constraint>

	<!-- 防止spring内存溢出监听器 -->
	<listener>
		<listener-class>org.springframework.web.util.IntrospectorCleanupListener</listener-class>
	</listener>

	<servlet>
		<description>springMVC Servlet</description>
		<servlet-name>springmvc</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<!-- 此处配置的是SpringMVC的配置文件 -->
			<param-value>classpath:spring-mvc.xml</param-value>
		</init-param>
		<load-on-startup>2</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>springmvc</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>

</web-app>

red5-web.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd 
    http://www.springframework.org/schema/aop 
    http://www.springframework.org/schema/aop/spring-aop-3.0.xsd 
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-4.2.xsd">

	<!-- Defines a properties file for dereferencing variables -->
	<bean 
		class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="location" value="/WEB-INF/red5-web.properties" />
	</bean>

	<!-- Defines the web context -->
	<bean  />

	<!-- Defines the web scopes -->
	<bean 
		init-method="register">
		<property name="server" ref="red5.server" />
		<property name="parent" ref="global.scope" />
		<property name="context" ref="web.context" />
		<property name="handler" ref="web.handler" />
		<property name="contextPath" value="${webapp.contextPath}" />
		<property name="virtualHosts" value="${webapp.virtualHosts}" />
	</bean>

	<!-- Defines the web handler which acts as an applications endpoint -->
	<bean  />

	<!-- 开启自动扫包 -->
	<context:component-scan base-package="com.gm.service">
		<!--制定扫包规则,不扫描@Controller注解的JAVA类,其他的还是要扫描 -->
		<context:exclude-filter type="annotation"
			expression="org.springframework.stereotype.Controller" />
	</context:component-scan>

	<!-- 启动AOP支持 -->
	<aop:aspectj-autoproxy />

	<!-- Database connection pool bean -->
	<bean >
		<property name="driverClassName" value="${db.driver}" />
		<property name="url" value="${db.url}" />
		<property name="username" value="${db.username}" />
		<property name="password" value="${db.password}" />
	</bean>

	<!-- 配置Session工厂 -->
	<bean >
		<property name="dataSource" ref="dataSource" />
		<property name="mapperLocations" value="classpath:com/gm/mapper/*Mapper.xml" />
		<property name="configLocation" value="classpath:/mybatis-config.xml"></property>
	</bean>

	<!-- 自动扫描所有的Mapper接口与文件 -->
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="com.gm.mapper"></property>
	</bean>

	<!-- 配置事务管理器 -->
	<bean 
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"></property>
	</bean>

	<!-- 定义个通知,指定事务管理器 -->
	<tx:advice >
		<tx:attributes>
			<tx:method name="delete*" propagation="REQUIRED" read-only="false"
				rollback-for="java.lang.Exception" />
			<tx:method name="save*" propagation="REQUIRED" read-only="false"
				rollback-for="java.lang.Exception" />
			<tx:method name="insert*" propagation="REQUIRED" read-only="false"
				rollback-for="java.lang.Exception" />
			<tx:method name="update*" propagation="REQUIRED" read-only="false"
				rollback-for="java.lang.Exception" />
			<tx:method name="load*" propagation="SUPPORTS" read-only="true" />
			<tx:method name="find*" propagation="SUPPORTS" read-only="true" />
			<tx:method name="search*" propagation="SUPPORTS" read-only="true" />
			<tx:method name="select*" propagation="SUPPORTS" read-only="true" />
			<tx:method name="get*" propagation="SUPPORTS" read-only="true" />
		</tx:attributes>
	</tx:advice>

	<aop:config>
		<!-- 配置一个切入点 -->
		<aop:pointcut 
			expression="execution(* com.gm.service.impl.*ServiceImpl.*(..))" />
		<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods" />
	</aop:config>
	
	<bean class="com.gm.util.ApplicationContextHandle" lazy-init="false"/>
	
	<bean ></bean>
	
	<bean ></bean>
</beans>

这块多啰嗦一下,在SpringMvc项目中配置applicationContext.xml,在red5项目中则配置在red5-web.xml。

其中red5-web.properties

webapp.contextPath=/MyVideo
webapp.virtualHosts=*

db.driver=com.mysql.jdbc.Driver
db.url=jdbc:mysql://127.0.0.1:3306/actdemo1?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true
db.username=root
db.password=1qaz@wsx

其中spring-mvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
http://www.springframework.org/schema/beans/spring-beans-4.1.xsd 
http://www.springframework.org/schema/context 
http://www.springframework.org/schema/context/spring-context-4.1.xsd 
http://www.springframework.org/schema/mvc 
http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd">

	<!-- 自动扫描@Controller注入为bean -->
	<context:component-scan base-package="com.gm.controller" />

	<mvc:annotation-driven />
	<!--对静态资源文件的访问 -->
	<mvc:resources mapping="/static/**" location="/WEB-INF/static/" />
	<mvc:resources mapping="/static/jw_old/**" location="/WEB-INF/static/jw_old/" />
	<mvc:resources mapping="/static/jw_new/**" location="/WEB-INF/static/jw_new/" />
	<mvc:resources mapping="/7.10.4/**" location="/WEB-INF/static/jw_new/7.10.4/" />
	<mvc:resources mapping="/skins/**" location="/WEB-INF/static/jw_new/skins/" />

	<!-- 对模型视图名称的解析,即在模型视图名称添加前后缀 -->
	<bean
		class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<property name="viewClass"
			value="org.springframework.web.servlet.view.JstlView" />
		<property name="prefix" value="/WEB-INF/views/"></property>
		<property name="suffix" value=".jsp" />
	</bean>


	<bean 
		class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
		<!-- 上传文件大小上限,单位为字节(5GB) -->
		<property name="maxUploadSize">
			<value>5368709120</value>
		</property>
		<!-- 请求的编码格式,必须和jSP的pageEncoding属性一致,以便正确读取表单的内容,默认为ISO-8859-1 -->
		<property name="defaultEncoding">
			<value>UTF-8</value>
		</property>
	</bean>

</beans>

其中mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<!-- 全局参数 -->
	<settings>
		<!-- 设置但JDBC类型为空时,某些驱动程序 要指定值,default:OTHER,插入空值时不需要指定类型 -->
		<setting name="jdbcTypeForNull" value="NULL" />
	</settings>
	<!-- <plugins>
		<plugin interceptor="com.manager.util.MybatisInterceptor"></plugin>
	</plugins> -->

</configuration>

其中loadFFmpeg.properties

#ffmpeg执行路径,一般为ffmpeg的安装目录,该路径只能是目录,不能为具体文件路径,否则会报错
path=E:/ffmpeg-20180227-fa0c9d6-win64-static/bin/
#存放任务的默认Map的初始化大小
size=10
#是否输出debug消息
debug=true

部分业务代码:

其中Application.java,为了节省服务器资源在对应摄像头点击播放时触发ffmpeg进行RTMP推流。

package com;

import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.red5.server.adapter.MultiThreadedApplicationAdapter;
import org.red5.server.api.IClient;
import org.red5.server.api.IConnection;
import org.red5.server.api.scope.IScope;
import org.red5.server.api.stream.IBroadcastStream;
import org.red5.server.api.stream.ISubscriberStream;
import com.gm.FFmpegCommandManager.FFmpegManager;
import com.gm.FFmpegCommandManager.FFmpegManagerImpl;
import com.gm.FFmpegCommandManager.entity.TaskEntity;
import com.gm.entity.Camera;
import com.gm.service.CameraService;


/**
 * Red5业务处理核心
 *
 */
public class Application extends MultiThreadedApplicationAdapter {
	 
 
	public static Map<String,Integer> streamList = new HashMap<String,Integer>();
	
	
	@Override
	public boolean connect(IConnection conn) {
		System.out.println("connect");
		return super.connect(conn);
	}

	@Override
	public void disconnect(IConnection arg0, IScope arg1) {
		System.out.println("disconnect"); 
		super.disconnect(arg0, arg1);
	}
	/**
	 * 开始发布直播
	 */
	@Override
	public void streamPublishStart(IBroadcastStream stream) {
		System.out.println("[streamPublishStart]********** ");
		System.out.println("发布Key: " + stream.getPublishedName());
		
		System.out.println(
				"发布时间:" + new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date(stream.getCreationTime())));
		System.out.println("****************************** ");
	}

	/**
	 * 流结束
	 */
	@Override
	public void streamBroadcastClose(IBroadcastStream arg0) {
		
		super.streamBroadcastClose(arg0);
	}

	/**
	 * 用户断开播放
	 */
	@Override
	public void streamSubscriberClose(ISubscriberStream arg0) {
	
		super.streamSubscriberClose(arg0);
	}

	/**
	 * 链接rtmp服务器
	 */
	@Override
	public boolean appConnect(IConnection arg0, Object[] arg1) {
		// TODO Auto-generated method stub
		
		System.out.println("[appConnect]********** ");
		System.out.println("请求域:" + arg0.getScope().getContextPath());
		System.out.println("id:" + arg0.getClient().getId());
		System.out.println("name:" + arg0.getClient().getId());
		System.out.println("********************** ");
		return super.appConnect(arg0, arg1);
	}

	/**
	 * 加入了rtmp服务器
	 */
	@Override
	public boolean join(IClient arg0, IScope arg1) {
	      
		// TODO Auto-generated method stub
		System.out.println("[join]**************** ");
		System.out.println("id:"+arg0.getId());
		System.out.println("********************** ");
		return super.join(arg0, arg1);
	}

	/**
	 * 开始播放流
	 */
	@Override
	public void streamSubscriberStart(ISubscriberStream stream) {
		
		String streamScope = stream.getScope().getContextPath();
		String streamKey = stream.getBroadcastStreamPublishName();
		
		/**
		 * rtmp://172.19.12.240/MyVideo/stream/test ,其中/MyVideo/stream为请求域,test为播放key,stream和test都可作为参数
		 * 
		 'file': 'test',
    	 'streamer': 'rtmp://172.19.12.240/MyVideo/stream/',	
    		
    		
		 * rtmp://172.19.12.240/MyVideo/stream.test ,其中/MyVideo为请求域,stream.test为播放key,stream和test都可作为参数
		 * 
		 'file': 'stream.test',
    	 'streamer': 'rtmp://172.19.12.240/MyVideo/',	
		 */
		
		System.out.println("[streamSubscriberStart]********** ");
		System.out.println("播放域:" + streamScope);
		System.out.println("播放Key:" + stream.getBroadcastStreamPublishName());
		
		//streamKey示例:stream_1
		if (streamKey.contains("stream") && !streamKey.contains("HD")) {
			//判断摄像头ID还是物理文件,物理文件无需进行处理,摄像头需对其进行rtsp转rtmp,如遇多台机器访问同一摄像头实时,无需ffmpeg进行再次转码,streamList访问总是+1,如退出连接且streamList访问数为1时,管理转流进程
			stream.getScope().setAttribute("streamKey", streamKey);
			
			boolean flag = true;
			FFmpegManager manager = new FFmpegManagerImpl();
			Collection<TaskEntity> list = manager.queryAll();
			for (TaskEntity task : list) {
				if(task.getId().equals(streamKey)) {	
					flag = false;
					streamList.put(streamKey,streamList.get(streamKey)+1);
					System.out.println("streamKey="+streamKey+",当前客户端连接数:"+streamList.get(streamKey));
					break;
				}
			}
			
			if(flag) {
				CameraService cameraService  = (CameraService) scope.getContext().getBean("cameraService");
				Camera camera = cameraService.find(Integer.parseInt(streamKey.split("_")[1]));
				camera.setCameraId(streamKey);
				/*camera.setCameraRtsp("rtsp://184.72.239.149/vod/mp4://BigBuckBunny_175k.mov");*/
				camera.setCameraRtmp("rtmp://172.19.12.240/" + streamScope + "/");
									
				Map<String,String> map = new HashMap<String,String>();
				map.put("appName", camera.getCameraId());
				map.put("input", camera.getCameraRtsp());
				map.put("output", camera.getCameraRtmp());
				map.put("codec", "h264");
				map.put("fmt", "flv");
				map.put("fps", "25");
				map.put("rs", "640x360");
				map.put("twoPart", "0");//twoPart=2时,推出两个rtmp流,一个自定义码流与元码流
				
				// 执行任务,id就是appName,如果执行失败返回为null
				String id = manager.start(map);
				TaskEntity info = manager.query(id);
				streamList.put(streamKey, 1);
				System.out.println("streamKey="+streamKey+",当前客户端连接数:"+streamList.get(streamKey));
			}	
		}
		
		
		System.out.println("********************************* ");
		
		String sessionId = stream.getConnection().getSessionId();
		stream.getConnection().setAttribute(null, null); 
		
		super.streamSubscriberStart(stream);
	}

	/**
	 * 离开了rtmp服务器
	 */
	@Override
	public void leave(IClient arg0, IScope arg1) {
		System.out.println("[leave]**************************");
		
		FFmpegManager manager = new FFmpegManagerImpl();
		
		if (arg1.getAttribute("streamKey") != null) {	
			String streamKey = arg1.getAttribute("streamKey").toString();
			Collection<TaskEntity> list = manager.queryAll();
			
			System.out.println("ffmpeg在线执行数量:" + list.size());
			
			for (TaskEntity task : list) {
				if(task.getId().equals(streamKey)) {	
					if (streamList.get(streamKey) == 1) {
						manager.stop(streamKey);
						streamList.remove(streamKey);
						System.out.println("streamKey="+streamKey+",当前客户端连接数:0");

					} else {
						streamList.put(streamKey,streamList.get(streamKey)-1);
						System.out.println("streamKey="+streamKey+",当前客户端连接数:"+streamList.get(streamKey));
					}	
					break;
				}
			}
		}
		super.leave(arg0, arg1);
	}
 
}

部分业务相关代码在此就不贴,实现效果:模拟下类似插件式的四画面基于Red5与ffmpeg实现rtmp处理NVR或摄像头的监控视频处理方案
背景
方案
实现

可通过

 runtime.exec(command);

触发FFmpeg进行推流,推流命令:

ffmpeg -i rtsp://admin:Ab123456@172.19.12.113/h265/ch1/av_stream -f flv -r 25 -g 25 -s 640x360 -an rtmp://172.19.12.240/live/test123 -vcodec h264  -f flv -an rtmp://172.19.12.240/live/test123HD

ffmpeg常见命令参照我的另一篇博客地址

ffmpeg不同可以进行推流还可以实现转录到本地,这样历史视频查看功能也就实现了。

此方案还有很多可以去优化的地方,大家可以在评论区下进行探讨,相同学习提高。