背景
各大監控視頻平台廠商與外對接均是基於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不同可以進行推流還可以實現轉錄到本地,這樣歷史視頻查看功能也就實現了。
此方案還有很多可以去優化的地方,大家可以在評論區下進行探討,相同學習提高。