• 基于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项目

     

    target runtime 选择 new runtime


    选择Red5并next



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


    点击Finish


    勾选red5 application generation


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





    搭建Red5服务器


    右键New->Server


    选择Red5,并Next


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

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


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


    其中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"
    	id="WebApp_ID" version="2.5">
    
    
    	<!-- 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 id="placeholderConfig"
    		class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    		<property name="location" value="/WEB-INF/red5-web.properties" />
    	</bean>
    
    	<!-- Defines the web context -->
    	<bean id="web.context" class="org.red5.server.Context" autowire="byType" />
    
    	<!-- Defines the web scopes -->
    	<bean id="web.scope" class="org.red5.server.scope.WebScope"
    		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 id="web.handler" class="com.Application" />
    
    	<!-- 开启自动扫包 -->
    	<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 id="dataSource " class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    		<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 id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    		<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 id="txManager"
    		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    		<property name="dataSource" ref="dataSource"></property>
    	</bean>
    
    	<!-- 定义个通知,指定事务管理器 -->
    	<tx:advice id="txAdvice" transaction-manager="txManager">
    		<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 id="serviceMethods"
    			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 id="cameraService" class="com.gm.service.impl.CameraServiceImpl"></bean>
    	
    	<bean id="fileService" class="com.gm.service.impl.FileServiceImpl"></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 id="multipartResolver"
    		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);
    	}
     
    }

    部分业务相关代码在此就不贴,实现效果:模拟下类似插件式的四画面

    可通过

     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不同可以进行推流还可以实现转录到本地,这样历史视频查看功能也就实现了。

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

  • 相关阅读:
    MySQL 5.7 Invalid default value for 'CREATE_TIME'报错的解决方法
    浅析mysql中exists 与 in 的使用
    mysql 索引原理
    内存溢出与内存泄漏
    java 内部类详解
    JAVA中重写equals()方法的同时要重写hashcode()方法
    Java中volatile关键字解析
    JDK1.8 HashMap源码分析
    mysql 行转列 列转行
    Java多线程(十)——线程优先级和守护线程
  • 原文地址:https://www.cnblogs.com/gmhappy/p/9472366.html
Copyright © 2020-2023  润新知