客户有海康和大华的监控设备,没有买各类安防平台,国标方式需要预留给其他需要接入的系统,得兼容高版本chrome,询问了大华的客服人员,记录下曲折的过程。延迟大约10秒的样子,应该还能通过设置参数在优化,CPU占用率较高,不适合高并发,小项目用的少可以。如果高并发场景高的还是别用,可以使用ZLMediaKit开源(尝试了下ffmpeg占用率是要低很多,好东西太优秀的国产开源),或者SRS等一些开源的去改吧改吧用。
海康大华摄像机NVR接口方式在高版本chrome浏览器预览的解决方案
rtsp+nginx转m3u8播放
rtsp+nginx转flv播放
ZLMediaKit+wvp拉流
rtsp转flv(简单方式)
https://developer.aliyun.com/article/867004?scm=20140722.ID_community@@article@@867004.P_121.MO_938-ST_5186-V_1-ID_community@@article@@867004-OR_rec
方案一(目前使用方式,记录下整个过程):
一、ffmpeg+nginx搭建过程支持h264和h265
#前置安装一些后面需要的
yum -y install git
yum install -y bzip2
yum install -y cmake
yum install unzip -y
yum install gcc-c++ -y
yum install pcre pcre-devel -y
yum install -y libarchive
yum install zlib zlib-devel -y
yum install openssl openssl-devel -y
#sudo yum -y install cmake #3.0版本以上
yum install wget -y
cd /usr/local/
sudo wget https://cmake.org/files/v3.22/cmake-3.22.0-rc1-linux-x86_64.tar.gz
tar -zxvf cmake-3.22.0-rc1-linux-x86_64.tar.gz
sudo mv cmake-3.22.0-rc1-linux-x86_64 /opt/cmake-3.22.0
sudo ln -sf /opt/cmake-3.22.0/bin/* /usr/bin/
cmake --version
#下载安装nginx http://nginx.org/en/download.html
cd /usr/local/
tar -zxvf nginx-1.23.3.tar.gz
cd nginx-1.23.3
linux 安装flv模块已包含了rtmp
wget https://github.com/winshining/nginx-http-flv-module/archive/master.zip
unzip master.zip
./configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_stub_status_module --add-module=/usr/local/nginx-1.23.3/nginx-http-flv-module-master
make && make install
#nginx的配置,修改完重启下nginx
#nginx配置rtmp详解参考
#需要通过nginx简单负载均衡参考
https://blog.csdn.net/weixin_37530941/article/details/128702616
#直播相关的配置的详细介绍https://blog.51cto.com/eguid/5100113
二、对外网nginx的配置如下
user root root;
worker_processes 2;#设置为内核数*2
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
client_max_body_size 100m;
#gzip on;
server {
listen 8099;
#配置安装服务器的Ip地址,与下面rtmp的配置一致
server_name 172.16.121.75;
#charset koi8-r;
#access_log logs/host.access.log main;
# location / {
# index index.html index.htm;
# proxy_pass http://webservers;
# add_header Access-Control-Allow-Origin *;
# add_header Access-Control-Allow-Credentials true;
# add_header Access-Control-Allow-Headers Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,X-Requested-With;
# add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
# try_files $uri $uri/ @router;
# }
# ffmpeg直播地址
location /live {
flv_live on;
chunked_transfer_encoding on; #open 'Transfer-Encoding: chunked' response
add_header 'Access-Control-Allow-Credentials' 'true'; #add additional HTTP header
add_header 'Access-Control-Allow-Origin' '*'; #add additional HTTP header
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
add_header 'Cache-Control' 'no-cache';
}
# This URL provides RTMP statistics in XML
location /stat {
rtmp_stat all;
# Use this stylesheet to view XML as web page
# in browser
rtmp_stat_stylesheet stat.xsl;
}
location /stat.xsl {
# XML stylesheet to view RTMP stats.
# Copy stat.xsl wherever you want
# and put the full directory path here
root /path/to/stat.xsl/;
}
#使用转发
location /hls {
# Serve HLS fragments
types {
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
}
root html;
add_header Cache-Control no-cache;
}
location /dash {
# Serve DASH fragments
root /tmp;
add_header Cache-Control no-cache;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
error_page 404 403 /404.html;
location = /404.html {
root /usr/local/nginx/html;
}
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /500.html;
location = /500.html {
root /usr/local/nginx/html;
}
}
}
#rtmp设置
rtmp {
out_queue 4096;
out_cork 8;
max_streams 128;
timeout 15s;
drop_idle_publisher 15s;
log_interval 5s;
log_size 1m;
server {
listen 1935; #监听的端口号
server_name 172.16.121.75; #与上面监听的8099端口的server名字一致
application live { #自定义的名字
live on;
}
#直播hls配置
application hls {
live on;
hls on;
hls_path /usr/local/nginx/html/hls;#直播缓存路径window环境的配置为绝对地址,参考上传的
hls_fragment 2s;#设置HLS片段长度。 默认为5秒。
hls_playlist_length 5s;#设置HLS播放列表长度。 默认为30秒。
hls_continuous on; #连续模式。
hls_nested on; #嵌套模式就是允许如localhost:8099/hts/目录1/目录2/文件名.m3u8;
hls_cleanup off; # 切换HLS清理。 默认情况下,该功能处于打开状态。 在这种模式下,nginx缓存管理器进程从HLS目录中删除旧的HLS片段和播放列表,必须关闭才能按目录去删除,如果访问的url没有层级要求,把m3u8的名字取好点就可以没有必要按层级去删除目录或者建目录
}
}
}
#下载包甩到/home/village/ffmpeg 下根据实际需自己放,或者放到/usr/local下
三、linux环境程序部署相关下载包分享和安装过程
#链接:https://pan.baidu.com/s/1gTB0Hjxa7mqnBjPMKe-YQw
#提取码:0824
#大致的安装过程
cd /home/village/ffmpeg
tar jxvf nasm-2.15.tar.bz2
#安装h264 和 h265时候所需
cd nasm-2.15/
./configure --prefix=/usr/local
make && make install
nasm --version # 查看版本号 如果提示命令找不到 配下环境变量
cd /home/village/ffmpeg
tar -zxvf yasm-1.3.0.tar.gz
cd yasm-1.3.0
./configure
make && make install
#安装pkg-config
cd /home/village/ffmpeg
#wget https://pkg-config.freedesktop.org/releases/pkg-config-0.29.2.tar.gz
sudo tar -zxvf pkg-config-0.29.2.tar.gz
cd pkg-config-0.29.2/
sudo ./configure --with-internal-glib
make
make check
make install
export PATH=/usr/local/lib/pkgconfig:$PATH
#查看版本
pkg-config --version
#安装h264
cd /home/village/ffmpeg
tar -jxvf x264-master.tar.bz2
cd x264-master
mkdir /usr/local/x264
./configure --prefix=/usr/local/x264 --enable-shared --enable-static
make && make install
#添加环境变量
vim /etc/profile
#在文件末尾添加
export PATH=/usr/local/x264/bin:$PATH
export PATH=/usr/local/x264/include:$PATH
export PATH=/usr/local/x264/lib:$PATH
source /etc/profile
安装h265
cd /home/village/ffmpeg
tar -zxvf x265_3.2.tar.gz
cd x265_3.2/build/linux
./make-Makefiles.bash
#选择时候将选择ENABLE_HDR10_PLUS和HIGH_BIT_DEPTH通过回车设置ON,然后q键退出
vi /home/village/ffmpeg/x265_3.2/build/linux/x265.pc
#找到Libs.private: -lstdc++ -lm -lrt -ldl 末尾添加-lpthread
make && make install
pkg-config --list-all # 查看是否有x265 x264
#没有vim /etc/profile
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH
export PKG_CONFIG_PATH=/usr/local/x264/lib/pkgconfig:$PKG_CONFIG_PATH
export PKG_CONFIG_PATH=/usr/local/lib:$PKG_CONFIG_PATH
source /etc/profile
pkg-config --list-all # 查看是否有x265 x264
#https://blog.csdn.net/angl129/article/details/122339796
# 支持10bit
cd /home/village/ffmpeg/x265_3.2/source
vim CMakeLists.txt
#/HIGH_BIT_DEPTH 修改option(HIGH_BIT_DEPTH "Store pixel samples as 16bit values (Main10/Main12)" OFF) OFF改为ON
cd /home/village/ffmpeg/x265_3.2/build/linux
cmake -G "Unix Makefiles" -DCMAKE_INSTALL_PREFIX=/usr/local -DENABLE_SHARED=OFF ../../source
#安装
make
make install
#ffmpeg
cd /home/village/ffmpeg
tar -xzvf ffmpeg-4.1.tar.gz
cd ffmpeg-4.1
./configure --prefix=/usr/local/ffmpeg --enable-shared --enable-yasm --enable-libx264 --enable-libx265 --enable-gpl --enable-pthreads --extra-cflags=-I/usr/local/x264/include --extra-ldflags=-L/usr/local/x264/lib --disable-x86asm --pkg-config="pkg-config --static"
# vim /etc/ld.so.conf
#在文件末尾加上
/usr/local/ffmpeg/lib
/usr/local/lib
/usr/local/x264/lib
#让配置生效
sudo ldconfig
make && make install
cp /usr/local/ffmpeg/bin/* /usr/bin/
vi /etc/profile
#末尾加入
export PATH=/usr/local/ffmpeg/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/ffmpeg/lib:$LD_LIBRARY_PATH
#生效
source /etc/profile
ffmpeg -version
#处理大华rtsp到m3u8测试
#linux测试
ffmpeg -rtsp_transport tcp -i "rtsp://admin:admin123@IP:554/cam/realmonitor?channel=1&subtype=0" -strict -2 -c:v libx264 -vsync 2 -c:a aac -f hls -hls_time 4 -hls_list_size 3 -hls_wrap 10 -y /usr/local/nginx/html/hls/channel1.m3u8
#多级目录测试,目录结构/hls/用户名/自定的视频表ID/自定的视频表ID.m3u8
#简单处理就是hls_cleanup开启,定义好m3u8的文件名,通过文件名解析去关掉相应的ffmpeg
ffmpeg -rtsp_transport tcp -i "rtsp://admin:admin123@IP:554/cam/realmonitor?channel=1&subtype=0" -strict -2 -c:v libx264 -vsync 2 -c:a aac -f hls -hls_time 4 -hls_list_size 3 -hls_wrap 10 -y /usr/local/nginx/html/hls/ea6f791673c640998e31dd29082621f1/3E4DC4ECEA2847ED8E5D88BB2BA3BFCC/3E4DC4ECEA2847ED8E5D88BB2BA3BFCC.m3u8
#window下测试
ffmpeg -rtsp_transport tcp -i "rtsp://admin:admin123@IP:554/cam/realmonitor?channel=1&subtype=0" -strict -2 -c:v libx264 -vsync 2 -c:a aac -f hls -hls_time 4 -hls_list_size 3 -hls_wrap 10 -y D:\tools\nginx\vedioCache\hls\ea6f791673c640998e31dd29082621f1\3E4DC4ECEA2847ED8E5D88BB2BA3BFCC\3E4DC4ECEA2847ED8E5D88BB2BA3BFCC.m3u8
#http m3u8格式访问地址
http://localhost:8099/hls/ea6f791673c640998e31dd29082621f1/3E4DC4ECEA2847ED8E5D88BB2BA3BFCC/3E4DC4ECEA2847ED8E5D88BB2BA3BFCC.m3u8
http://172.16.121.75:8099/hls/ea6f791673c640998e31dd29082621f1/3E4DC4ECEA2847ED8E5D88BB2BA3BFCC/3E4DC4ECEA2847ED8E5D88BB2BA3BFCC.m3u8
#vlc播发器串流播放测试
#本地window测试环境就需要在本地安装ffmpeg和nginx
#window 下测试用,已编译好的安装了rtmp的nginx共享地址
链接:https://pan.baidu.com/s/1F9QKRXpubngbd7X1xypkwA
提取码:0824
#java通过接口调用方式调用ffmpeg参考以下博文进行改造,按海康和大华rtsp的格式设计和维护一张表,也可以通过集成官方的SDK去获取相应的设备列表信息
https://blog.csdn.net/weixin_43288858/article/details/128253490?spm=1001.2014.3001.5502
https://www.freesion.com/article/5775913700/
四、代码改造
ffmpeg java调用上面涉及的一些代码下载地址
http://github.com/eguid/FFCH4J
#代码和配置实现接口调用预览和关闭预览
#POM引入
org.bytedeco
javacv
1.5.4
org.bytedeco
ffmpeg-platform
4.3.1-1.5.4
org.springframework.boot
spring-boot-starter-data-redis
#yml配置加入
#ffmpeg相关自定义的设置
ffmpeg:
#macOs系统转流文件缓存地址
macOsTempPath: /Users/jiangsha/Documents/upload
#linux系统转流文件缓存地址,rtmp如何配置需要一致
linuxTempPath: /usr/local/nginx/html
#本地系统转流文件缓存地址,rtmp直播缓存地址
windowTempPath: D:\tools\nginx\vedioCache
# 映射出来的流媒体所在端口
livePort: 8099
# 映射出来的流媒体所在服务,nginx配置的/hls用于转发流的
liveApp: hls
#httpHeader http或者https
urlHead: http
#流媒体所在服务器
liveServiceUrl: localhost
#一次在线观看的有效时间,系统资源有限,无法一直推流消耗内存,默认给定30分钟
expireMinutes: 30
#延迟关闭最大时间 20 关闭后20秒内重新打开不重复调用
expireMinutesDelay: 20
#配置文件中找到 notify-keyspace-events "" 将其改成 notify-keyspace-events "Ex" 用于回调
spring:
redis:
host: redis的IP
password: 密码
database: 4
port: 6379
#redis配置类和回调类
@Configuration
public class RedisConfig {
@Bean
//标红别管他
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
@Component
@Slf4j
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Autowired
CameraSecretInfoService cameraSecretInfoService;
/**
* key过期触发的事件
*/
@SneakyThrows
@Override
public void onMessage(Message message, byte[] pattern) {
String channel = new String(message.getChannel(), StandardCharsets.UTF_8);
String key = new String(message.getBody(), StandardCharsets.UTF_8);
boolean contains = key.contains(Constants.FFMPEG_USER_KEY);
if (contains) {
log.info("redis key 过期:pattern={},channel={},key={}", new String(pattern), channel, key);
// key 形式 bladeFile:userId:pkId
String[] params = key.split(":");
cameraSecretInfoService.removeExpiredVideo(params[1], params[2]);
}
}
}
#定时关闭意外未关闭的ffmpeg
@Slf4j
@Component
public class FfmpegSchedule {
@Resource
CommandManager manager;
/**
* 每天晚上2点清理一次ffmpeg
*/
@Scheduled(cron = "0 00 2 * * ?")
private void configureTasks() {
log.info("开始定时清理未正常关闭的ffmpeg进程");
manager.stopAll();
}
}
#ffmpeg自定义配置类
@Data
@Configuration
@ConfigurationProperties(value = "ffmpeg")
public class FfmpegConfig {
/**
* 本地测试流文件路径缓存地址
*/
private String macOsTempPath;
/**
* window流文件路径存地址,配置在yml文件里面
*/
private String windowTempPath;
/**
* linux流文件路径存地址,配置在yml文件里面
*/
private String linuxTempPath;
/**
* 映射出来的流媒体所在端口
*/
private String livePort;
/**
* 映射出来的流媒体所在服务,nginx配置的勇于转发流的
*/
private String liveApp;
/**
* httpHeader
*/
private String urlHead;
/**
* 流媒体服务地址,设置为流媒体所在地址,也就是服务所地址
*/
private String liveServiceUrl;
/**
* 一次观看系统监控的时间,过期后会自动释放,单位分钟
*/
private int expireMinutes;
/**
* 延迟关闭的10秒
*/
private int expireMinutesDelay;
/**
* 默认命令行执行根路径
*/
private String path;
/**
* 是否开启debug模式
*/
private boolean debug;
/**
* 任务池大小
*/
private Integer size;
/**
* 回调通知地址
*/
private String callback;
/**
* 是否开启保活
*/
private boolean keepalive;
/**
* Description 获取返回3um8的地址 如http://221.178.132.15:8099/live/userId/vedioId/vedioId.m3u8
*
* @param userId
* userId
* @param vedioId
* vedioId
* @return java.lang.String
* @author
* @date 19:33 2023/1/7
**/
public String getRealLiveUrl(String userId, String vedioId) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(this.urlHead);
stringBuilder.append("://");
stringBuilder.append(this.liveServiceUrl);
stringBuilder.append(":");
stringBuilder.append(this.livePort + "/");
stringBuilder.append(this.liveApp + "/");
stringBuilder.append(userId + "/");
stringBuilder.append(vedioId + "/");
stringBuilder.append(vedioId);
stringBuilder.append(".m3u8");
return stringBuilder.toString();
}
public String getRealLiveUrl(String vedioId) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(this.urlHead);
stringBuilder.append("://");
stringBuilder.append(this.liveServiceUrl);
stringBuilder.append(":");
stringBuilder.append(this.livePort + "/");
stringBuilder.append(this.liveApp + "/");
stringBuilder.append(vedioId + "/");
stringBuilder.append(vedioId);
stringBuilder.append(".m3u8");
return stringBuilder.toString();
}
/**
* Description ffmpeg服务存储m3u8的地址
*
* @param userId
* userId
* @param vedioId
* vedioId
* @return java.lang.String
* @author
* @date 20:03 2023/1/7
**/
public String getSaveTempFileName(String userId, String vedioId) {
return FilePathUtil.getFfmpegTmpPath() + File.separator + this.getLiveApp() + File.separator + userId + File.separator + vedioId + File.separator + vedioId + ".m3u8";
}
/**
* Description ffmpeg服务存储m3u8的地址
*
* @param vedioId
* vedioId
* @return java.lang.String
* @author
* @date 20:03 2023/1/7
**/
public String getSaveTempFileName(String vedioId) {
return FilePathUtil.getFfmpegTmpPath() + File.separator + this.getLiveApp() + File.separator + vedioId + File.separator + vedioId + ".m3u8";
}
/**
* Description ffmpeg服务存储m3u8的地址
*
* @param userId
* userId
* @param vedioId
* vedioId
* @return java.lang.String
* @author
* @date 20:03 2023/1/7
**/
public String getBaseSavePath(String userId, String vedioId) {
return FilePathUtil.getFfmpegTmpPath() + File.separator + this.getLiveApp() + File.separator + userId + File.separator + vedioId + File.separator;
}
/**
* Description ffmpeg服务存储m3u8的地址
*
* @param vedioId
* vedioId
* @return java.lang.String
* @author
* @date 20:03 2023/1/7
**/
public String getBaseSavePath(String vedioId) {
return FilePathUtil.getFfmpegTmpPath() + File.separator + this.getLiveApp() + File.separator + vedioId + File.separator;
}
}
#文件路径工具类兼容各系统
public class FilePathUtil {
private FilePathUtil() {}
private static final FfmpegConfig ffmpegConfig = SpringUtils.getBean(FfmpegConfig.class);
public static String getFfmpegTmpPath() {
String tempPath = "";
if (SystemUtils.isMacOs()) {
tempPath = ffmpegConfig.getMacOsTempPath();
} else if (SystemUtils.isWindows()) {
tempPath = ffmpegConfig.getWindowTempPath();
FileUtil.makeDir(tempPath);
} else {
tempPath = ffmpegConfig.getLinuxTempPath();
}
return tempPath;
}
public static String getFilePath(String fileName) {
return getFilePath() + File.separator + fileName;
}
}
#API接口和DTO
@Api(value = "大华监控视频预览API", tags = "大华监控视频预览API")
@RequestMapping("/village/live")
@Validated
public interface CameraSecretInfoApi {
/**
* Description 预览大华视频
*
* @param villageMonitorDTO
* villageMonitorDTO
* @return com.gsww.village.common.base.OpenResponse
* @author
* @date 15:55 2023/1/9
**/
@ApiOperation(value = "预览某个视频返回m3u8地址", notes = "预览某个视频返回m3u8地址")
@PostMapping("/ffmpegOpen")
@BusinessOperateLog(operateModule = "通过ffmpeg预览某个视频", operateType = OperateType.QUERY, operateDesc = "预览摄像头")
OpenResponse
ffmpegOpen(@Validated(ValidatedGroup.CreateGroup.class) @RequestBody VillageMonitorDTO villageMonitorDTO);
/**
* Description 关闭预览
*
* @param villageMonitorParamDTO
* villageMonitorParamDTO
* @return com.gsww.village.common.base.OpenResponse
* @author
* @date 15:55 2023/1/9
**/
@ApiOperation(value = "关闭某个视频预览", notes = "关闭某个视频预览")
@PostMapping("/ffmpegClose")
@BusinessOperateLog(operateModule = "通过ffmpeg关闭某个视频预览", operateType = OperateType.QUERY, operateDesc = "关闭预览摄像头")
OpenResponse ffmpegOff(@RequestBody VillageMonitorParamDTO villageMonitorParamDTO);
}
#DTO类自己定义的表接各类摄像头数据进来方便前端拼接展示,CURD工程师最爱的
@Data
@ApiModel(description = "VillageMonitorDTO")
public class VillageMonitorDTO {
/**
* 网络摄像机地址
*/
@ApiModelProperty(value = "网络摄像机地址")
@Trimmed
@Length(max = 25, message = "网络摄像机地址不能超过25个字符",
groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
@NotBlank(message = "网络摄像机地址不能为空")
private String vedioAdress;
/**
* 端口
*/
@ApiModelProperty(value = "端口")
@Trimmed
@Length(max = 10, message = "端口不能超过10个字符",
groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
@NotEmpty(message = "端口不能为空")
private String vedioPort;
/**
* 摄像机类型, 预留后面去实现 1大华0海康
*/
@ApiModelProperty(value = "摄像机类型")
private String vedioType;
/**
* 通道号
*/
@ApiModelProperty(value = "通道号")
@Trimmed
@Length(max = 2, message = "通道号不能超过2个字符",
groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
@NotEmpty(message = "通道号不能为空")
private String channelNo;
/**
* 通道名称或摄像头名称
*/
@ApiModelProperty(value = "通道名称或摄像头名称")
@Trimmed
@Length(max = 20, message = "通道名称或摄像头名称不能超过20个字符",
groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
@NotEmpty(message = "通道名称或摄像头名称不能为空")
private String channelName;
/**
* 码流类型 大华0 主流 1辅流 海康1主流0辅流
*/
@ApiModelProperty(value = "码流类型")
@Trimmed
@Length(max = 10, message = "码流类型不能超过10个字符",
groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
@NotEmpty(message = "码流类型不能为空")
private String subType;
/**
* 账户名称
*/
@ApiModelProperty(value = "账户名称")
@Trimmed
@Length(max = 20, message = "账户名称不能超过20个字符",
groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
@NotEmpty(message = "账户名称不能为空")
private String accountName;
/**
* 账户密码
*/
@ApiModelProperty(value = "账户密码")
@Trimmed
@Length(max = 100, message = "账户密码不能超过100个字符",
groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
@NotEmpty(message = "账户密码不能为空")
private String accountPass;
/**
* 数据所属区划代码
*/
@ApiModelProperty(value = "数据所属区划代码")
@Trimmed
@Length(max = 20, message = "数据所属区划代码不能超过20个字符",
groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
private String areaCode;
/**
* 数据所属区划名称
*/
@ApiModelProperty(value = "数据所属区划名称")
@Trimmed
@Length(max = 20, message = "数据所属区划名称不能超过20个字符",
groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
private String areaName;
/**
* 经度
*/
@ApiModelProperty(value = "经度")
@Trimmed
private Double longitude;
/**
* 纬度
*/
@ApiModelProperty(value = "纬度")
@Trimmed
private Double latitude;
/**
* 备注
*/
@ApiModelProperty(value = "备注")
@Trimmed
@Length(max = 20, message = "备注不能超过20个字符",
groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
private String note;
/**
* m3u8Url 返回的地址
*/
private String m3u8Url;
@ApiModelProperty(value = "主键")
private String pkId;
/**
* 创建人id
*/
@ApiModelProperty(value = "创建人id")
private String createUserId;
/**
* 标识位
*/
private String flagEemp;
/**
* 创建人
*/
@ApiModelProperty(value = "创建人")
private String createUserName;
/**
* 更新人id
*/
@ApiModelProperty(value = "更新人id")
private String updateUserId;
/**
* 更新人
*/
@ApiModelProperty(value = "更新人")
private String updateUserName;
/**
* 创建时间
*/
@ApiModelProperty(value = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 更新时间
*/
@ApiModelProperty(value = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/**
* 1、新增,2、修改,3、删除
*/
@ApiModelProperty(value = "操作标志")
private String statusEemp;
/**
* 逻辑删除标志位
*/
@ApiModelProperty(value = "逻辑删除0未删除1已删除")
private String deleted;
}
#接视频的存储在自己系统的业务表设计
DROP TABLE IF EXISTS `t_village_monitor`;
CREATE TABLE `t_village_monitor` (
`PK_ID` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
`VEDIO_ADRESS` varchar(25) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '网络摄像机IP地址',
`VEDIO_PORT` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'RTSP端口',
`CHANNEL_NO` varchar(2) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '通道号',
`CHANNEL_NAME` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '通道名称',
`VEDIO_TYPE` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '设备类型',
`SUB_TYPE` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '码流类型',
`ACCOUNT_NAME` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '账户名称',
`ACCOUNT_PASS` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '账户密码',
`AREA_CODE` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '数据所属区划代码',
`AREA_NAME` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '数据所属区划名称',
`LONGITUDE` decimal(10, 6) NULL DEFAULT NULL COMMENT '经度',
`LATITUDE` decimal(10, 6) NULL DEFAULT NULL COMMENT '纬度',
`NOTE` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
`FLAG_EEMP` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '标识位',
`CREATE_USER_ID` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建用户ID',
`CREATE_USER_NAME` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建用户名称',
`UPDATE_USER_ID` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改用户ID',
`UPDATE_USER_NAME` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改用户姓名',
`STATUS_EEMP` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '逻辑标识位:(1:新增,2:修改,3:删除)',
`CREATE_TIME` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`UPDATE_TIME` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间',
`DELETED` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '删除标识(0:否,1:是)',
PRIMARY KEY (`PK_ID`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '视频监控表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_village_monitor
-- ----------------------------
INSERT INTO `t_village_monitor` VALUES ('3E4DC4ECEA2847ED8E5D88BB2BA3BFCC', '61.171.182.292', '554', '8', '高清球机17', '1', '0', 'admin', 'qUReS8rEJLdHkqqRRg8uOg==', '340323202000', '仲兴镇', 117.195874, 33.204568, NULL, NULL, 'ea6f791673c640998e31dd29082621f1', 'ljp', 'ea6f791673c640998e31dd29082621f1', 'ljp', '2', '2023-01-10 10:50:50', '2023-01-10 11:38:59', '0');
INSERT INTO `t_village_monitor` VALUES ('3E4DC4ECEA2847ED8E5D88BB2BA3BFC2', '62.172.182.291', '554', '6', '高清球机15', '1', '0', 'admin', 'qUReS8rEJLdHkqqRRg8uOg==', '340323103000', '连城镇', 117.368578, 33.292745, NULL, NULL, 'ea6f791673c640998e31dd29082621f1', 'ljp', 'ea6f791673c640998e31dd29082621f1', 'ljp', '2', '2023-01-10 10:50:32', '2023-01-10 11:38:41', '0');
#Controller
@RestController
public class CameraSecretInfoController implements CameraSecretInfoApi {
@Autowired
CameraSecretInfoService cmeraSecretInfoService;
@Override
public OpenResponse ffmpegOpen(VillageMonitorDTO villageMonitorDTO) {
if (StrUtil.isEmpty(villageMonitorDTO.getPkId())) {
throw new BusinessException("摄像ID不能为空");
}
villageMonitorDTO.setAccountPass(AesEncryptUtil.desEncrypt(villageMonitorDTO.getAccountPass()));
VillageMonitorDTO returnDto = cmeraSecretInfoService.ffmpegOpen(villageMonitorDTO);
// 休息3秒钟先加载一下视频,体验好些就不加
if (!returnDto.getReplayFlag()) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return OpenResponse.success(returnDto);
}
@Override
public OpenResponse ffmpegOff(VillageMonitorParamDTO villageMonitorParamDTO) {
if (StrUtil.isEmpty(villageMonitorParamDTO.getPkId())) {
throw new BusinessException("摄像ID不能为空");
}
cmeraSecretInfoService.ffmpegOff(villageMonitorParamDTO.getPkId());
return OpenResponse.success();
}
@Override
public OpenResponse> ffmpegOpenList(VillageMonitorListParamDTO villageMonitorListParamDTO) {
List list = new ArrayList<>();
List booleanList = new ArrayList<>();
int size = villageMonitorListParamDTO.getListVideos().size();
villageMonitorListParamDTO.getListVideos().stream().forEach(dto -> {
dto.setAccountPass(AesEncryptUtil.desEncrypt(dto.getAccountPass()));
VillageMonitorDTO returnDto = cmeraSecretInfoService.ffmpegOpen(dto);
returnDto.setAccountPass("");
list.add(returnDto);
if (returnDto.getReplayFlag()) {
booleanList.add(returnDto.getReplayFlag());
}
});
// 休息3秒钟先加载一下视频,体验好些就不加
if (!(booleanList.size() == size)) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return OpenResponse.success(list);
}
@Override
public OpenResponse ffmpegOffList(IdsDTO ids) {
ids.getIds().stream().forEach(id -> cmeraSecretInfoService.ffmpegOff(id));
return OpenResponse.success();
}
}
#service接口和实现类
public interface CameraSecretInfoService {
/**
* Description 返回M3u8地址
*
* @param villageMonitorDTO
* villageMonitorParamDTO
* @return java.lang.String
* @author
* @date 16:04 2023/1/7
**/
VillageMonitorDTO ffmpegOpen(VillageMonitorDTO villageMonitorDTO);
/**
* Description 关闭预览
*
* @param pkId
* pkId
* @author
* @date 16:04 2023/1/7
**/
void ffmpegOff(String pkId);
/**
* Description 关闭预览
*
* @param pkId
* pkId
* @author
* @date 16:04 2023/1/7
**/
void stopBackVideoDelay(String pkId);
/**
* Description 停止并删除过期的预览
*
* @param userId
* token1
* @return void
* @author
* @date 15:56 2023/1/7
**/
void removeExpiredVideo(String userId, String vedioId) throws IOException;
/**
* Description 删除文件夹下面所有文件
*
* @param basePath
* basePath
* @return void
* @author
* @date 15:57 2023/1/7
**/
void deleteDir(String basePath) throws IOException;
void stopBackVideo(String vedioId) throws IOException;
}
#// 过期删除关闭视频流的KEY用于redis的回调
public static final String FFMPEG_USER_KEY = "bladeFile:";
#视频预览实现
@Service
@Slf4j
public class CameraSecretInfoServiceImpl implements CameraSecretInfoService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Autowired
FfmpegConfig ffmpegConfig;
@Autowired
CommandManager manager;
/**
* Description 开启预览,启动ffmpeg的key规则为用户ID+视频监控表ID,用户失效的时候按用户全部删除
*
* @param villageMonitorDTO
* @return java.lang.String
* @author
* @date 17:44 2023/1/7
**/
@Override
public VillageMonitorDTO ffmpegOpen(VillageMonitorDTO villageMonitorDTO) {
// 当前用户ID
String userId = AbsExtDomainUtil.getUserInfo().getUserId();
// 存放的m3u8文件夹地址 root/appUrl/userId /pkId/pkId.m3u8
FileUtil.makeDir(ffmpegConfig.getBaseSavePath(userId, villageMonitorDTO.getPkId()));
String codeId = userId + ":" + villageMonitorDTO.getPkId();
if (ObjectUtil.isEmpty(manager.query(codeId))
&& Boolean.FALSE.equals(stringRedisTemplate.hasKey(Constants.FFMPEG_USER_KEY + codeId))) {
manager.stop(codeId); // 先停止视频
manager.start(codeId,
CommandBuidlerFactory.createBuidler().add("ffmpeg").add("-rtsp_transport", "tcp")
.add("-i", getRtspUrl(villageMonitorDTO)) // 取videoUrl
.add("-strict", "-2")
//转成h264国标
.add("-vcodec", "libx264").add("-vsync", "2")
.add("-preset:v").add("ultrafast").add("-tune:v").add("zerolatency")
// 不要声音
//.add("-an")
// 音频
.add("-c:a", "aac")
.add("-hls_time", "3").add("-hls_list_size", "2").add("-hls_wrap", "3").add("-y")
//如果使用http-flv把下面的地址换成类似rtmp://172.16.121.75:1935/live/test
.add(ffmpegConfig.getSaveTempFileName(userId, villageMonitorDTO.getPkId())));
CommandTasker info = manager.query(codeId);
villageMonitorDTO.setReplayFlag(false);
log.info("启动ffmpeg:" + info.toString());
} else if (ObjectUtil.isEmpty(manager.query(codeId))
&& Boolean.TRUE.equals(stringRedisTemplate.hasKey(Constants.FFMPEG_USER_KEY + codeId))) {
try {
removeDir(userId, villageMonitorDTO.getPkId());
} catch (IOException e) {
e.printStackTrace();
}
manager.stop(codeId); // 先停止视频
manager.start(codeId,
CommandBuidlerFactory.createBuidler().add("ffmpeg").add("-rtsp_transport", "tcp")
.add("-i", getRtspUrl(villageMonitorDTO)) // 取videoUrl
.add("-strict", "-2")
// 转成h264
.add("-vcodec", "libx264").add("-vsync", "2").add("-preset:v").add("ultrafast").add("-tune:v")
.add("zerolatency")
// 不要声音
// .add("-an")
// 音频
.add("-c:a", "aac").add("-hls_time", "5").add("-hls_list_size", "2").add("-hls_wrap", "3").add("-y")
.add(ffmpegConfig.getSaveTempFileName(userId, villageMonitorDTO.getPkId())));
CommandTasker info = manager.query(codeId);
villageMonitorDTO.setReplayFlag(false);
log.info("启动ffmpeg:" + info.toString());
} else {
villageMonitorDTO.setReplayFlag(true);
}
// 设置ffmpeg视频失效时间,避免长时间占用资源,造成内存溢出
stringRedisTemplate.opsForValue().set(Constants.FFMPEG_USER_KEY + codeId, codeId,
ffmpegConfig.getExpireMinutes(), TimeUnit.MINUTES);
// 返回路径根据ffmpeg存放视频路径+nginx代理灵活配置
//如果使用http-flv的话实际返回地址换成类似http://172.16.121.75:8099/live?port=1935&app=live&stream=test
villageMonitorDTO.setM3u8Url(ffmpegConfig.getRealLiveUrl(userId, villageMonitorDTO.getPkId()));
return villageMonitorDTO;
}
@Override
public void ffmpegOff(String pkId) {
stopBackVideoDelay(pkId);
}
@Override
public void removeExpiredVideo(String userId, String vedioId) throws IOException {
String basePathAll = ffmpegConfig.getBaseSavePath(userId, vedioId);
File fileExist = new File(basePathAll);
// 文件夹文件夹存在,则停止后删除
if (fileExist.exists()) {
stopBackVideo(userId, vedioId);
if (fileExist.exists()) {
deleteDir(basePathAll);
}
}
}
@Override
public void deleteDir(String basePath) throws IOException {
Path path = Paths.get(basePath);
Files.walkFileTree(path, new SimpleFileVisitor() {
// 先去遍历删除文件
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
log.warn("文件被删除 : %s%n", file);
return FileVisitResult.CONTINUE;
}
// 再去遍历删除目录
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
log.warn("文件夹被删除: %s%n", dir);
return FileVisitResult.CONTINUE;
}
});
}
@Override
public void stopBackVideo(String vedioId) throws IOException {
String userId = AbsExtDomainUtil.getUserInfo().getUserId();
String basePath = ffmpegConfig.getBaseSavePath(userId, vedioId);
File fileExist = new File(basePath);
String codeId = userId + ":" + vedioId;
// 停止ffmpeg转码
manager.stop(codeId);
manager.start(codeId, CommandBuidlerFactory.createBuidler().add("rm -rf", basePath));
// 对文件夹进行删除操作
if (fileExist.exists()) {
deleteDir(basePath);
}
// 清除相应的redis
stringRedisTemplate.delete(Constants.FFMPEG_USER_KEY + codeId);
}
@Override
public void stopBackVideoDelay(String vedioId) {
String userId = AbsExtDomainUtil.getUserInfo().getUserId();
String codeId = userId + ":" + vedioId;
// 延迟10秒关闭,免得重复拉
log.info(codeId + " 将在" + ffmpegConfig.getExpireMinutesDelay() + "秒后关闭流");
stringRedisTemplate.opsForValue().set(Constants.FFMPEG_USER_KEY + codeId, codeId,
ffmpegConfig.getExpireMinutesDelay(), TimeUnit.SECONDS);
}
public void stopBackVideo(String userId, String vedioId) throws IOException {
// 解析jwt的token值,拿到最后面一截,这个也是不会重复
String basePath = FilePathUtil.getFfmpegTmpPath() + File.separator + ffmpegConfig.getLiveApp() + File.separator
+ userId + File.separator + vedioId + File.separator;
File fileExist = new File(basePath);
String codeId = userId + ":" + vedioId;
// 停止ffmpeg转码
manager.stop(codeId);
manager.start(codeId, CommandBuidlerFactory.createBuidler().add("rm -rf", basePath));
// 对文件夹进行删除操作
if (fileExist.exists()) {
deleteDir(basePath);
}
// 清除相应的redis
stringRedisTemplate.delete(Constants.FFMPEG_USER_KEY + codeId);
}
/**
* 得到文件名称
*
* @param file
* 文件
* @param fileNames
* 文件名
* @return {@link List}<{@link String}>
*/
private List getFileNames(File file, List fileNames) {
File[] files = file.listFiles();
for (File f : files) {
if (f.isDirectory()) {
fileNames.add(f.getName());
}
}
return fileNames;
}
/**
* Description 获取到设备的rtsp流地址
*
* @param villageMonitorDTO
* villageMonitorDTO
* @return java.lang.String
* @author
* @date 13:45 2023/1/10
**/
private String getRtspUrl(VillageMonitorDTO villageMonitorDTO) {
StringBuilder stringBuild = new StringBuilder("rtsp://");
stringBuild.append(villageMonitorDTO.getAccountName() + ":");
stringBuild.append(villageMonitorDTO.getAccountPass() + "@");
stringBuild.append(villageMonitorDTO.getVedioAdress() + ":");
stringBuild.append(villageMonitorDTO.getVedioPort());
// 默认大华
if (StrUtil.isEmpty(villageMonitorDTO.getVedioType())
|| Constants.ONE.equals(villageMonitorDTO.getVedioType())) {
stringBuild.append("/cam/realmonitor?");
stringBuild.append("channel=" + villageMonitorDTO.getChannelNo());
stringBuild.append("&subtype=" + villageMonitorDTO.getSubType());
} else {
stringBuild.append("/Streaming/Channels/");
stringBuild.append(villageMonitorDTO.getChannelNo() + villageMonitorDTO.getSubType());
}
// stringBuild.append("\\\"");
return stringBuild.toString();
}
}
#开源保活处理下避免内存溢出造成应用程序奔溃
public class KeepAliveHandler extends Thread{
/**待处理队列*/
private static Queue queue=null;
public int err_index=0;//错误计数 5次调用后自动停止
public volatile int stop_index=0;//安全停止线程标记
/** 任务持久化器*/
private TaskDao taskDao = null;
public KeepAliveHandler(TaskDao taskDao) {
super();
this.taskDao=taskDao;
queue=new ConcurrentLinkedQueue<>();
}
public static void add(String id ) {
if(queue!=null) {
queue.offer(id);
}
}
public boolean stop(Process process) {
if (process != null) {
process.destroy();
return true;
}
return false;
}
@Override
public void run() {
for(;stop_index==0;) {
if(queue==null) {
continue;
}
String id=null;
CommandTasker task=null;
try {
while(queue.peek() != null) {
System.err.println("准备重启任务:"+queue);
id=queue.poll();
task=taskDao.get(id);
//重启任务
ExecUtil.restart(task);
}
}catch(Exception e) {
//重启任务失败,5次后自动停止避免内存溢出
err_index++;
System.err.println(id+" 任务重启失败" + err_index + "次,详情:"+task);
if (err_index >=5) {
stop_index=1;
System.err.println(id+" 任务重启失败,保活关闭");
}
}
}
}
@Override
public void interrupt() {
stop_index=1;
}
}
#开源代码配置工具类linux路径问题修改
PropertiesUtil类修改
/**
* 获取对应文件路径下的文件流 兼容linux打包读取路径
*
* @param path
* @return
* @throws FileNotFoundException
*/
public static InputStream getInputStream(String path) throws IOException {
return new ClassPathResource(path).getInputStream();
}
接口测试
#前端测试html ,src修改为输出的m3u8地址
#前端部分代码
链接:https://pan.baidu.com/s/1Ev-1cuA5WRSC_vbCSKA-Wg
提取码:0824
前端播放m3u8格式视频
切换视频
方案二
在方案一的代码中把ffmpeg组装的地址换成
ffmpeg -re -rtsp_transport tcp -i "rtsp://admin:admin123@IP:554/cam/realmonitor?channel=3&subtype=0" -f flv -vcodec h264 -vprofile baseline -acodec aac -ar 44100 -strict -2 -ac 1 -f flv -s 640*360 -q 10 "rtmp://172.16.121.75(nginxIP):1935(rtmp端口)/live(服务名称)/test"
通过vlc打开
http://172.16.121.75:8099/live?port=1939&app=live&stream=test
前端可以通过flv.js访问,vue的代码自行改造,以下代码保存为html直接打开
播放http-flv
效果如下
参考博文https://blog.csdn.net/Candyz7/article/details/126741970
方案三:
通过ZLM免费开源国标平台搭建接入的示例,看看拉流方式的实现,适合并发较高情况,不过需要搭建个流媒体服务,有支持http-flv可以开箱就用:
参考https://notemi.cn/wvp---zlmedia-kit---mediaserverui-to-realize-streaming-playback-and-recording-of-camera-gb28181.html
前端如果想分离出来简单调整下代码实现前后端分离
如果想支持拉流拉成m3u8格式可以简单调整下如下,没有深入改会有很多隐患
/home/village/ZLMediaKit/release/linux/Debug/config.ini
cmd添加flv和m3u8格式支持
代码调整下
if ("ffmpeg".equals(param.getType()) && "ffmpeg.cmd2".equals(param.getFfmpeg_cmd_key())) {
dstUrl = String.format("/usr/local/nginx/html/hls/%s-%s%s", param.getApp(),
param.getStream(),".m3u8");
}
配置文件更改
前端分离需要更改的地方如下
index.js更改替换下build
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: './',
proxyTable: {
'/zlm': {
target:'http://172.16.121.75:18080', //请求的目标地址的BaseURL
ws:true,
changeOrigin:true, //是否开启跨域
pathRewrite:{
'^/zlm':'' //重新路径,把EzaYun开头的,替换成 ''
}
},
'/static/snap': {
target: 'http://127.16.121.75:18080',
changeOrigin: true,
// pathRewrite: {
// '^/static/snap': '/static/snap'
// }
}
},
这样前端打包完成后就和一般vue打包一样生成个dist,这样通过nginx就可以简单部署前端代码
配置调整下
nginx加上流媒体服务访问m3u8
#流媒体前端代理
location /zlm-server/ {
alias /home/village/wvp/web/dist/;
index index.html index.htm;
}
#流媒体接口代理
location /zlm/api/ {
proxy_pass http://172.16.121.75:18080/api/;
}
location /zlm/api/user/login {
proxy_pass http://172.16.121.75:18080/api/user/login;
}
#前端获取流媒体
location /zlmhls {
# Serve HLS fragments
types {
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
}
#root html;
alias /usr/local/nginx/html/hls;
}
拉流占用情况