各大監控視頻平臺廠商與外對接均是基於IE的OCX插件方式提供實時視頻查看、歷史視頻回放與歷史視頻下載。在h5已大行其道的當下,基於IE的OCX插件方式已知足不了廣大客戶的實際需求,所以須要一個兼容各大主流瀏覽器與手機瀏覽的監控視頻處理方案。html
red5是基於Flash的流媒體服務的一款基於Java的開源流媒體服務器。java
ffmpeg是一套能夠用來記錄、轉換數字音頻、視頻,並能將其轉化爲流的開源計算機程序。mysql
本方案利用Red5發佈RTMP流媒體服務器,向外提供實時、歷史的RTMP推流;利用FFmpeg實現RTSP看成源推送到RTMP服務器;基於jsplayer實現視頻展現。git
具體細節上代碼:github
安裝Red5,下載地址:https://github.com/Red5/red5-server,如不了具體安裝步驟請自行百度。web
安裝ffmpeg,下載地址:https://ffmpeg.zeranoe.com/builds/win64/static/ffmpeg-20180325-5b31dd1-win64-static.zip,如不了具體安裝步驟請自行百度。spring
構建基於Red5的Web項目sql
target runtime 選擇 new runtime
express
選擇Red5並next
api
選擇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不一樣能夠進行推流還能夠實現轉錄到本地,這樣歷史視頻查看功能也就實現了。
此方案還有不少能夠去優化的地方,你們能夠在評論區下進行探討,相同窗習提升。