ZLMediaKit:实现RTSP/RTMP/HLS/HTTP协议的轻量级流媒体框架,支持大并发连接请求
一个基于C++11的高性能运营级流媒体服务框架,项目地址:ZLMediaKit
特点:
项目定位:
RTSP[S]
rtp over udp
rtp over tcp
rtp over http
rtp组播
四种RTP传输方式rtp over udp
rtp over tcp
方式)RTMP[S]
HLS
TS
fMP4
HTTP[S]与WebSocket
目录索引生成
,文件下载
,表单提交请求
文件下载器(支持断点续传)
,接口请求器
,文件上传器
GB28181与RTP推流
MP4点播与录制
WebRTC
SRT支持
其他
wvp-GB28181-pro: 开箱即用的28181协议视频平台
WEB VIDEO PLATFORM是一个基于GB28181-2016标准实现的开箱即用的网络视频平台,负责实现核心信令与设备管理后台部分,支持NAT穿透,支持海康、大华、宇视等品牌的IPC、NVR接入。支持国标级联,支持将不带国标功能的摄像机/直播流/直播推流转发到其他国标平台。
项目地址:wvp-GB28181-pro
使用文档:使用文档
应用场景:
基础特性:
新特性:
支持特性:
国标通道向上级联
WEB添加上级平台
注册
心跳保活
通道选择
通道推送
点播
云台控制
平台状态查询
平台信息查询
平台远程启动
每个级联平台可自定义的虚拟目录
目录订阅与通知
录像查看与播放
GPS订阅与通知(直播推流)
添加RTSP视频
添加接口鉴权
添加RTMP视频
云端录像(需要部署单独服务配合使用)
多流媒体节点,自动选择负载最低的节点使用。
WEB端支持播放H264与H265,音频支持G.711A/G.711U/AAC,覆盖国标常用编码格式。
支持电子地图。
支持接入WGS84和GCJ02两种坐标系。
我这里的部署方式是分开独立部署,你也可以根据 wvp-GB28181-pro
项目下,docker
目录下面的dockerFile
,一起部署。
建议去github,去查看该文件,因为gitee上面该文件已经很久没更新了。我这里打不开github就不截图了。
这里采用docker部署,因为需要c++环境以及一些依赖,这里建议docker直接部署。对于docker不了解的,可以查看我之前的docker系列博客,这里就不再多撰述了
docker系列地址:docker
启动容器成功,你需要将容器内的:/opt/media/conf/config.ini 拷贝出来,到你绑定的目录
#拉取镜像
docker pull zlmediakit/zlmediakit:master
#此镜像为github持续集成自动编译推送,跟代码(master分支)保持最新状态
docker run -d -p 1935:1935 -p 18180:80 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 30000-30500:30000-30500 -p 30000-30500:30000-30500/udp --name zlmediakit --restart=always --env MODE=standalone -e TZ="Asia/Shanghai" -v /home/docker/zlmediakit/media/conf:/opt/media/conf 12bb9d80c9a2 /bin/bash -c 'MediaServer /opt/media/conf/config.ini'
-d:后台运行
-p:绑定端口
-name: 容器名称
–restart : 自启
–env : 环境变量
-e TZ:时区
-v:绑定文件
-c : 指定文件启动
知识点:config.ini 文件,66行 mediaServerId 服务器id 是 wvp-pro 需要的,这里很重要
知识点:需要将你的防火墙把
ZLMediaKit
的端口全部开放,阿里云或者其他云的服务器记得把安全组的端口也放开,注意上面端口有UDP,TCP
linux查看开放端口列表:
[root@localhost local]# firewall-cmd --list-ports
linux开放指定端口:
[root@localhost local]# firewall-cmd --zone=public --add-port=9000/tcp --permanent
#关闭指定端口
[root@localhost local]# firewall-cmd --zone=public --remove-port=80/tcp --permanent
linux重启防火墙:
[root@localhost local]# firewall-cmd --reload
如下界面,即为部署成功
clone地址:https://gitee.com/pan648540858/wvp-GB28181-pro.git,将项目clone到本地
这里强烈建议去github,去clone wvp 代码,因为 gitee 上面的代码很久没有做更新了,有很多bug,我这边是自己把bug解决了,希望你在部署的时候,去 github上面去拉取在最新代码,少走一些弯路
将项目里面的mysql.sql里面的文件,创建数据库
修改 ProxyServletConfig.class,这里主要是将serverPort,zlmProxy,recordProxy提取出来做配置化
package com.genersoft.iot.vmp.conf;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.service.IMediaServerService;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.mitre.dsmiley.httpproxy.ProxyServlet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ObjectUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.net.ConnectException;
/**
* @author lin
*/
@SuppressWarnings(value = {"rawtypes", "unchecked"})
@Configuration
public class ProxyServletConfig {
private final static Logger logger = LoggerFactory.getLogger(ProxyServletConfig.class);
@Autowired
private IMediaServerService mediaServerService;
@Value("${server.port}")
private int serverPort;
@Value("${zlm.zlmproxy}")
private String zlmProxy;
@Value("${zlm.recordproxy}")
private String recordProxy;
@Bean
public ServletRegistrationBean zlmServletRegistrationBean(){
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new ZlmProxyServlet(),"/zlm/*");
servletRegistrationBean.setName("zlm_Proxy");
servletRegistrationBean.addInitParameter("targetUri", zlmProxy);
servletRegistrationBean.addUrlMappings();
if (logger.isDebugEnabled()) {
servletRegistrationBean.addInitParameter("log", "true");
}
return servletRegistrationBean;
}
class ZlmProxyServlet extends ProxyServlet{
@Override
protected String rewriteQueryStringFromRequest(HttpServletRequest servletRequest, String queryString) {
String queryStr = super.rewriteQueryStringFromRequest(servletRequest, queryString);
MediaServerItem mediaInfo = getMediaInfoByUri(servletRequest.getRequestURI());
if (mediaInfo != null) {
if (!ObjectUtils.isEmpty(queryStr)) {
queryStr += "&secret=" + mediaInfo.getSecret();
}else {
queryStr = "secret=" + mediaInfo.getSecret();
}
}
return queryStr;
}
/**
* 异常处理
*/
@Override
protected void handleRequestException(HttpRequest proxyRequest, HttpResponse proxyResonse, Exception e){
try {
super.handleRequestException(proxyRequest, proxyResonse, e);
} catch (ServletException servletException) {
logger.error("zlm 代理失败: ", e);
} catch (IOException ioException) {
if (ioException instanceof ConnectException) {
logger.error("zlm 连接失败");
} else if (ioException instanceof ClientAbortException) {
logger.error("zlm: 用户已中断连接,代理终止");
} else {
logger.error("zlm 代理失败: ", e);
}
} catch (RuntimeException exception){
logger.error("zlm 代理失败: ", e);
}
}
/**
* 对于为按照格式请求的可以直接返回404
*/
@Override
protected String getTargetUri(HttpServletRequest servletRequest) {
String requestURI = servletRequest.getRequestURI();
MediaServerItem mediaInfo = getMediaInfoByUri(requestURI);
String uri = null;
if (mediaInfo != null) {
// String realRequestURI = requestURI.substring(requestURI.indexOf(mediaInfo.getId())+ mediaInfo.getId().length());
uri = String.format("http://%s:%s", mediaInfo.getIp(), mediaInfo.getHttpPort());
}else {
uri = "http://127.0.0.1:" + serverPort +"/index/hook/null"; // 只是一个能返回404的请求而已, 其他的也可以
}
return uri;
}
/**
* 动态替换请求目标
*/
@Override
protected HttpHost getTargetHost(HttpServletRequest servletRequest) {
String requestURI = servletRequest.getRequestURI();
MediaServerItem mediaInfo = getMediaInfoByUri(requestURI);
HttpHost host;
if (mediaInfo != null) {
host = new HttpHost(mediaInfo.getIp(), mediaInfo.getHttpPort());
}else {
host = new HttpHost("127.0.0.1", serverPort);
}
return host;
}
/**
* 根据uri获取流媒体信息
*/
MediaServerItem getMediaInfoByUri(String uri){
String[] split = uri.split("/");
String mediaServerId = split[2];
if ("default".equalsIgnoreCase(mediaServerId)) {
return mediaServerService.getDefaultMediaServer();
}else {
return mediaServerService.getOne(mediaServerId);
}
}
/**
* 去掉url中的标志信息
*/
@Override
protected String rewriteUrlFromRequest(HttpServletRequest servletRequest) {
String requestURI = servletRequest.getRequestURI();
MediaServerItem mediaInfo = getMediaInfoByUri(requestURI);
String url = super.rewriteUrlFromRequest(servletRequest);
if (mediaInfo == null) {
logger.error("[ZLM服务访问代理],错误:处理url信息时未找到流媒体信息=>{}", requestURI);
return url;
}
if (!ObjectUtils.isEmpty(mediaInfo.getId())) {
url = url.replace(mediaInfo.getId() + "/", "");
}
return url.replace("default/", "");
}
}
@Bean
public ServletRegistrationBean recordServletRegistrationBean(){
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new RecordProxyServlet(),"/record_proxy/*");
servletRegistrationBean.setName("record_proxy");
servletRegistrationBean.addInitParameter("targetUri", recordProxy);
servletRegistrationBean.addUrlMappings();
if (logger.isDebugEnabled()) {
servletRegistrationBean.addInitParameter("log", "true");
}
return servletRegistrationBean;
}
class RecordProxyServlet extends ProxyServlet{
@Override
protected String rewriteQueryStringFromRequest(HttpServletRequest servletRequest, String queryString) {
String queryStr = super.rewriteQueryStringFromRequest(servletRequest, queryString);
MediaServerItem mediaInfo = getMediaInfoByUri(servletRequest.getRequestURI());
String remoteHost = String.format("http://%s:%s", mediaInfo.getIp(), mediaInfo.getHttpPort());
if (mediaInfo != null) {
if (!ObjectUtils.isEmpty(queryStr)) {
queryStr += "&remoteHost=" + remoteHost;
}else {
queryStr = "remoteHost=" + remoteHost;
}
}
return queryStr;
}
/**
* 异常处理
*/
@Override
protected void handleRequestException(HttpRequest proxyRequest, HttpResponse proxyResponse, Exception e){
try {
super.handleRequestException(proxyRequest, proxyResponse, e);
} catch (ServletException servletException) {
logger.error("录像服务 代理失败: ", e);
} catch (IOException ioException) {
if (ioException instanceof ConnectException) {
logger.error("录像服务 连接失败");
} else if (ioException instanceof ClientAbortException) {
logger.error("录像服务:用户已中断连接,代理终止");
} else {
logger.error("录像服务 代理失败: ", e);
}
} catch (RuntimeException exception){
logger.error("录像服务 代理失败: ", e);
}
}
/**
* 对于为按照格式请求的可以直接返回404
*/
@Override
protected String getTargetUri(HttpServletRequest servletRequest) {
String requestURI = servletRequest.getRequestURI();
MediaServerItem mediaInfo = getMediaInfoByUri(requestURI);
String uri = null;
if (mediaInfo != null) {
// String realRequestURI = requestURI.substring(requestURI.indexOf(mediaInfo.getId())+ mediaInfo.getId().length());
uri = String.format("http://%s:%s", mediaInfo.getIp(), mediaInfo.getRecordAssistPort());
}else {
uri = "http://127.0.0.1:" + serverPort +"/index/hook/null"; // 只是一个能返回404的请求而已, 其他的也可以
}
return uri;
}
/**
* 动态替换请求目标
*/
@Override
protected HttpHost getTargetHost(HttpServletRequest servletRequest) {
String requestURI = servletRequest.getRequestURI();
MediaServerItem mediaInfo = getMediaInfoByUri(requestURI);
HttpHost host;
if (mediaInfo != null) {
host = new HttpHost(mediaInfo.getIp(), mediaInfo.getRecordAssistPort());
}else {
host = new HttpHost("127.0.0.1", serverPort);
}
return host;
}
/**
* 根据uri获取流媒体信息
*/
MediaServerItem getMediaInfoByUri(String uri){
String[] split = uri.split("/");
String mediaServerId = split[2];
if ("default".equalsIgnoreCase(mediaServerId)) {
return mediaServerService.getDefaultMediaServer();
}else {
return mediaServerService.getOne(mediaServerId);
}
}
/**
* 去掉url中的标志信息
*/
@Override
protected String rewriteUrlFromRequest(HttpServletRequest servletRequest) {
String requestURI = servletRequest.getRequestURI();
MediaServerItem mediaInfo = getMediaInfoByUri(requestURI);
String url = super.rewriteUrlFromRequest(servletRequest);
if (mediaInfo == null) {
logger.error("[录像服务访问代理],错误:处理url信息时未找到流媒体信息=>{}", requestURI);
return url;
}
if (!ObjectUtils.isEmpty(mediaInfo.getId())) {
url = url.replace(mediaInfo.getId() + "/", "");
}
return url.replace("default/", "");
}
}
}
application.yml
spring:
# [可选]上传文件大小限制
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
# REDIS数据库配置
redis:
# [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1
host: 127.0.0.1
# [必须修改] 端口号
port: 6379
# [可选] 数据库 DB
database: 8
# [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接
password:
# [可选] 超时时间
timeout: 10000
# mysql数据源
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/wvp_db?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true
username: root
password: 123456
druid:
initialSize: 10 # 连接池初始化连接数
maxActive: 200 # 连接池最大连接数
minIdle: 5 # 连接池最小空闲连接数
maxWait: 60000 # 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
keepAlive: true # 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作。
validationQuery: select 1 # 检测连接是否有效sql,要求是查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
testWhileIdle: true # 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testOnBorrow: false # 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn: false # 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
poolPreparedStatements: false # 是否開啟PSCache,並且指定每個連線上PSCache的大小
timeBetweenEvictionRunsMillis: 60000 # 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連線,單位是毫秒
minEvictableIdleTimeMillis: 300000 # 配置一個連線在池中最小生存的時間,單位是毫秒
filters: stat,slf4j # 配置监控统计拦截的filters,监控统计用的filter:sta, 日志用的filter:log4j
useGlobalDataSourceStat: true # 合并多个DruidDataSource的监控数据
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=1000
#stat-view-servlet.url-pattern: /admin/druid/*
#[可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口
server:
port: 18080
# 作为28181服务器的配置
sip:
# [必须修改] 本机的IP
ip: 192.168.2.138
# [可选] 28181服务监听的端口
port: 5060
# 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007)
# 后两位为行业编码,定义参照附录D.3
# 3701020049标识山东济南历下区 信息行业接入
# [可选]
domain: 4401020049
# [可选]
id: 44010200492000000001
# [可选] 默认设备认证密码,后续扩展使用设备单独密码, 移除密码将不进行校验
password: admin123
#zlm 默认服务器配置
media:
id: PX5YGOUtvK56qhTq
# [必须修改] zlm服务器的内网IP
ip: 内网IP
# [必须修改] zlm服务器的http.port
http-port: 8180
# [可选] zlm服务器的hook.admin_params=secret
secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
# 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试
rtp:
# [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输
enable: true
# [可选] 在此范围内选择端口用于媒体流传输, 必须提前在zlm上配置该属性,不然自动配置此属性可能不成功
port-range: 30000,30500 # 端口范围
# [可选] 国标级联在此范围内选择端口发送媒体流,
send-port-range: 30000,30500 # 端口范围
# 录像辅助服务, 部署此服务可以实现zlm录像的管理与下载, 0 表示不使用
record-assist-port: 0
# [可选] 日志配置, 一般不需要改
logging:
config: classpath:logback-spring-local.xml
zlm:
zlmproxy: http://公网ip:6080
recordproxy: http://公网ip:18081
使用maven 打包为jar
mvn install
部署到服务器即可
web_src 打开到该目录下面,
#拉取依赖
npm install
#打包
npm run build
后面使用,nginx代理部署,将前端文件里面的 index.js 后端地址,改为你的服务地址即可
这里以海康为例,访问海康控制,以ip访问
在高级配置,配置,平台接入方式改为28181,其他配置对应之前的wvp-pro,application.yml, 服务器地址写你的服务器地址即可,记得开放端口
四:总结
不断去发现问题,去解决问题,你才有进步,多点细心和耐心。