diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..eef220eb1
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+docker/volumes
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 000000000..5e41eeb5f
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,75 @@
+name: release-ubuntu
+
+on:
+ push:
+ tags:
+ - "v*.*.*" # 触发条件是推送标签 如git tag v2.7.4 git push origin v2.7.4
+
+jobs:
+ build-ubuntu:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ arch: [amd64]
+ max-parallel: 1 # 最大并行数
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4 # github action运行环境
+
+ - name: Create release # 创建文件夹
+ run: |
+ rm -rf release
+ mkdir release
+ echo ${{ github.sha }} > Release.txt
+ cp Release.txt LICENSE release/
+ cat Release.txt
+
+ - name: Set up JDK 1.8
+ uses: actions/setup-java@v4
+ with:
+ # Eclipse基金会维护的开源Java发行版 因为github action参考java的用这个 所以用这个
+ # 还有microsoft(微软维护的openjdk发行版) oracle(商用SDK)等
+ distribution: 'temurin'
+ java-version: '8'
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20.x' # Node.js版本 20系列的最新稳定版
+
+ - name: Compile backend
+ run: |
+ mvn package
+ mvn package -P war
+
+ - name: Compile frontend
+ run: |
+ cd ./web
+ npm install
+ npm run build:prod
+ cd ../
+
+ - name: Package Files
+ run: |
+ cp -r ./src/main/resources/static release/ # 复制前端文件
+ cp ./target/*.jar release/ # 复制 JAR 文件
+ cp ./src/main/resources/application-dev.yml release/application.yml
+
+ BRANCH=${{ github.event.base_ref }}
+ BRANCH_NAME=$(echo "$BRANCH" | grep -oP 'refs/heads/\K.*')
+ echo "BRANCH_NAME= ${BRANCH_NAME}"
+ # 如果无法获取,使用默认分支
+ if [[ -z "BRANCH_NAME" ]]; then
+ BRANCH_NAME="${{ github.event.repository.default_branch }}"
+ fi
+
+ TAG_NAME="${GITHUB_REF#refs/tags/}"
+ ZIP_FILE_NAME="${BRANCH_NAME}-${TAG_NAME}.zip"
+ zip -r "$ZIP_FILE_NAME" release
+ echo "ZIP_FILE_NAME=$ZIP_FILE_NAME" >> $GITHUB_ENV
+
+ - name: Release
+ uses: softprops/action-gh-release@v2
+ if: startsWith(github.ref, 'refs/tags/')
+ with:
+ files: ${{ env.ZIP_FILE_NAME }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 776ebe10e..c937a5c05 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,3 +28,5 @@ hs_err_pid*
/src/main/resources/static/
certificates
+/.vs
+/docker/volumes
diff --git a/README.md b/README.md
index c8b5bca57..4f1369a71 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@

-# 开箱即用的28181协议视频平台
+# 开箱即用的国标28181和部标808+1078协议视频平台
[](https://travis-ci.org/xia-chu/ZLMediaKit)
[](https://github.com/xia-chu/ZLMediaKit/blob/master/LICENSE)
@@ -8,7 +8,7 @@
[](https://github.com/xia-chu/ZLMediaKit/pulls)
-WEB VIDEO PLATFORM是一个基于GB28181-2016标准实现的开箱即用的网络视频平台,负责实现核心信令与设备管理后台部分,支持NAT穿透,支持海康、大华、宇视等品牌的IPC、NVR接入。支持国标级联,支持将不带国标功能的摄像机/直播流/直播推流转发到其他国标平台。
+WEB VIDEO PLATFORM是一个基于GB28181-2016、部标808、部标1078标准实现的开箱即用的网络视频平台,负责实现核心信令与设备管理后台部分,支持NAT穿透,支持海康、大华、宇视等品牌的IPC、NVR接入。支持国标级联,支持将不带国标功能的摄像机/直播流/直播推流转发到其他国标平台。
流媒体服务基于@夏楚 ZLMediaKit [https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit)
播放器使用@dexter jessibuca [https://github.com/langhuihui/jessibuca/tree/v3](https://github.com/langhuihui/jessibuca/tree/v3)
@@ -27,15 +27,6 @@ WEB VIDEO PLATFORM是一个基于GB28181-2016标准实现的开箱即用的网
wvp使用文档 [https://doc.wvp-pro.cn](https://doc.wvp-pro.cn)
ZLM使用文档 [https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit)
-# 付费社群
-[](https://t.zsxq.com/0d8VAD3Dm)
-> 收费是为了提供更好的服务,也是对作者更大的激励。加入星球的用户三天后可以私信我留下微信号,我会拉大家入群。
-> 加入三天内不满意可以直接自行推出,星球会直接退款给大家。需要发票可以在星球app中直接咨询星球客服获取。
-
-> 星球还提供了包括闭源的全功能试用包, 会随时更新。
-
-> 付费社群即可以对作者提供支持,也可以为大家更加快速的解决问题。如果暂时无法加入,给项目点个星也是极大的鼓励。
-
# gitee仓库
https://gitee.com/pan648540858/wvp-GB28181-pro.git
@@ -129,13 +120,38 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
- [X] 支持Mysql,Postgresql,金仓等数据库
- [X] 支持录制计划, 根据设定的时间对通道进行录制. 暂不支持将录制的内容转发到国标上级
- [X] 支持国标信令集群
+- [X] 新增支持部标808和部标1078,大量新特性不一一列表了。支持作为网关被国标上级调用部标设备
# 闭源内容
-- [X] 支持ONVIF协议,设备检索,支持点播,云台控制,国标级联点播,自动点播等。
-- [X] 支持部标1078+808协议,支持点播,云台控制,录像回放,位置上报,自动点播等。
-- [X] 支持国标28181-2022协议,支持巡航轨迹查询,PTZ精准控制,存储卡格式化,设备软件升级,OSD配置,h265+aac,支持辅码流,录像倒放等。
-- [X] 支持国网B接口协议。支持注册,获取资源,预览, 云台控制,预置位控制等,可免费定制支持语音对讲、录像回放和抓拍图像。
+- [X] 国标增强版: 支持国标28181-2022协议,支持巡航轨迹查询,PTZ精准控制,存储卡格式化,设备软件升级,OSD配置,h265+aac,支持辅码流,录像倒放等。
+- [X] 全功能版:
+ - [X] 支持开源所有功能
+ - [X] ONVIF协议
+ - 设备检索
+ - 实时图像预览
+ - 录像回放、回放倍速控制
+ - 云台控制、预置位控制、云台绝对定位、看守位
+ - 聚焦控制
+ - 设备重启
+ - 设备时间设置以及跟系统时间的差值比较
+ - 恢复出厂设置
+ - 自动获取设备品牌等信息、支持展示DNS信息、支持协议的展示
+ - 国标级联点播、自动点播等。
+ - [X] 国网B接口协议
+ - 设备注册
+ - 资源获取
+ - 预览
+ - 云台控制
+ - 预置位控制等,
+ - 可免费定制支持语音对讲、录像回放和抓拍图像。
+ - [X] 支持按权限分配可以使用的通道
+ - [X] 支持电子地图。支持展示通道位置,支持在地图上修改通道位置。可扩展接入高德地图API,支持搜索位置,附近设备。
+ - [X] 支持表格导出
+ - [X] 拉流代理支持按照品牌拼接url。
+ - [X] 播放鉴权,更加安全。
+ - [X] 此版本后续开发功能支持直接更新提供,无需二次付费。
+ - [X] 提供源码不限制部署次数和支持路数。
# 授权协议
@@ -143,6 +159,17 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
# 技术支持
+# 付费社群
+
+
+> 收费是为了提供更好的服务,也是对作者更大的激励。加入星球的用户三天后可以私信我留下微信号,我会拉大家入群。
+> 加入三天内不满意可以直接自行推出,星球会直接退款给大家。需要发票可以在星球app中直接咨询星球客服获取。
+
+> 星球还提供了包括闭源的全功能试用包, 会随时更新。
+
+> 付费社群即可以对作者提供支持,也可以为大家更加快速的解决问题。如果暂时无法加入,给项目点个星也是极大的鼓励。
+
+
[知识星球](https://t.zsxq.com/0d8VAD3Dm)专栏列表:,
- [使用入门系列一:WVP-PRO能做什么](https://t.zsxq.com/0dLguVoSp)
diff --git a/doc/_content/ability/push.md b/doc/_content/ability/push.md
index cc568b3c8..8998a4b75 100644
--- a/doc/_content/ability/push.md
+++ b/doc/_content/ability/push.md
@@ -18,6 +18,9 @@ WVP支持三种图像输入方式,直播,[拉流代理](_content/ability/pro
1. 默认情况下WVP收到推流信息后,列表中出现这条推流信息,如果你需要共享推流信息到其他国标平台,那么你需要编辑/国标通道配置,配置国标编码.
2. WVP也支持推流前导入大量通道直接推送给上级,点击“下载模板”按钮,根据示例修改模板后,点击“通道导入”按钮导入通道数据.
+## 生成推流地址
+可以在推流列表里点击‘生成推流地址’按钮,得到新地址后直接复制到推流设备。
+
## 推拉流鉴权规则
为了保护服务器的WVP默认开启推流鉴权(目前不支持关闭此功能)
diff --git a/docker/.env b/docker/.env
new file mode 100644
index 000000000..0de472237
--- /dev/null
+++ b/docker/.env
@@ -0,0 +1,19 @@
+MediaRtmp=10001
+MediaRtsp=10002
+MediaRtp=10003
+
+WebHttp=8080
+WebHttps=8081
+
+Stream_IP=127.0.0.1
+SDP_IP=127.0.0.1
+
+SIP_ShowIP=127.0.0.1
+SIP_Port=8160
+SIP_Domain=3502000000
+SIP_Id=35020000002000000001
+SIP_Password=wvp_sip_password
+
+
+RecordSip=true
+RecordPushLive=
\ No newline at end of file
diff --git a/docker/README.md b/docker/README.md
new file mode 100644
index 000000000..a7d0bcd3f
--- /dev/null
+++ b/docker/README.md
@@ -0,0 +1,10 @@
+可以在当前目录下:
+使用`docker compose up -d`直接运行。
+使用`docker compose up -d -build -force-recreate`强制重新构建所有服务的镜像并删除旧容器重新运行
+
+`.env`用来配置环境变量,在这里配好之后,其它的配置会自动联动的。
+
+`build.sh`用来以日期为tag构建镜像,推送到指定的容器注册表内(Windows下可以使用`Git Bash`运行)
+
+
+其它的文件的作用暂不明确
\ No newline at end of file
diff --git a/docker/build.sh b/docker/build.sh
index 5b1016fc3..5dd3e8e53 100755
--- a/docker/build.sh
+++ b/docker/build.sh
@@ -1,45 +1,79 @@
-#/bin/bash
-set -e
+#!/bin/bash
-version=2.7.3
+# 获取当前日期作为标签(格式:YYYYMMDD)
+date_tag=$(date +%Y%m%d)
-git clone https://gitee.com/pan648540858/wvp-GB28181-pro.git
-cd wvp-GB28181-pro/web_src && \
- npm install && \
- npm run build
+# 切换到脚本所在目录的上一级目录作为工作目录
+cd "$(dirname "$0")/.." || {
+ echo "错误:无法切换到上级目录"
+ exit 1
+}
+echo "已切换工作目录到:$(pwd)"
+
+# 检查私有仓库环境变量
+if [ -z "$DOCKER_REGISTRY" ]; then
+ echo "未设置DOCKER_REGISTRY环境变量"
+ read -p "请输入私有Docker注册库地址(如不推送请留空): " input_registry
+ docker_registry="$input_registry"
+else
+ docker_registry="$DOCKER_REGISTRY"
+fi
+
+# 定义要构建的镜像和对应的Dockerfile路径(相对当前工作目录)
+images=(
+ "wvp-service:docker/wvp/Dockerfile"
+ "wvp-nginx:docker/nginx/Dockerfile"
+)
+
+# 构建镜像的函数
+build_image() {
+ local image_name="$1"
+ local dockerfile_path="$2"
-cd ../../
-mkdir -p ./nginx/dist
-cp -r wvp-GB28181-pro/src/main/resources/static/* ./nginx/dist
+ # 检查Dockerfile是否存在
+ if [ ! -f "$dockerfile_path" ]; then
+ echo "错误:未找到Dockerfile - \"$dockerfile_path\",跳过构建"
+ return 1
+ fi
+
+ # 构建镜像
+ local full_image_name="${image_name}:${date_tag}"
+ echo
+ echo "=============================================="
+ echo "开始构建镜像:${full_image_name}"
+ echo "Dockerfile路径:${dockerfile_path}"
+
+ docker build -t "${full_image_name}" -f "${dockerfile_path}" .
+ if [ $? -ne 0 ]; then
+ echo "镜像${full_image_name}构建失败"
+ return 1
+ fi
+
+ # 推送镜像(如果设置了仓库地址)
+ if [ -n "$docker_registry" ]; then
+ local registry_image="${docker_registry}/${full_image_name}"
+ echo "给镜像打标签:${registry_image}"
+ docker tag "${full_image_name}" "${registry_image}"
+
+ echo "推送镜像到注册库"
+ docker push "${registry_image}"
+ if [ $? -eq 0 ]; then
+ echo "镜像${registry_image}推送成功"
+ else
+ echo "镜像${registry_image}推送失败"
+ fi
+ else
+ echo "未提供注册库地址,不执行推送"
+ fi
+ echo "=============================================="
+ echo
+}
-echo "构建ZLM容器"
-cd ./media/
-chmod +x ./build.sh
-./build.sh
-cd ../
+# 循环构建所有镜像
+for item in "${images[@]}"; do
+ IFS=':' read -r image_name dockerfile_path <<< "$item"
+ build_image "$image_name" "$dockerfile_path"
+done
-echo "构建数据库容器"
-cd ./mysql/
-chmod +x ./build.sh
-./build.sh
-cd ../
-
-echo "构建Redis容器"
-cd ./redis/
-chmod +x ./build.sh
-./build.sh
-cd ../
-
-echo "构建WVP容器"
-cd ./wvp/
-chmod +x ./build.sh
-./build.sh
-cd ../
-
-echo "构建Nginx容器"
-cd ./nginx/
-chmod +x ./build.sh
-./build.sh
-cd ../
-
-./push.sh
+echo "所有镜像处理完成"
+exit 0
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 8ad454822..9758161c0 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -1,7 +1,7 @@
version: '3'
services:
polaris-redis:
- image: polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-redis:latest
+ image: redis:latest # 使用官方Redis镜像
restart: unless-stopped
healthcheck:
test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ]
@@ -11,8 +11,8 @@ services:
start_period: 10s
networks:
- media-net
- ports:
- - 6379:6379
+ # ports:
+ # - 6379:6379
volumes:
- ./redis/conf/redis.conf:/opt/polaris/redis/redis.conf
- ./volumes/redis/data/:/data
@@ -21,7 +21,7 @@ services:
command: redis-server /opt/polaris/redis/redis.conf --appendonly yes
polaris-mysql:
- image: polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-mysql:latest
+ image: mysql:8 # 使用官方MySQL 8镜像
restart: unless-stopped
healthcheck:
test: [ "CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/3306" ]
@@ -34,18 +34,18 @@ services:
environment:
MYSQL_DATABASE: wvp
MYSQL_ROOT_PASSWORD: root
- MYSQL_USER: root
- MYSQL_PASSWORD: root
+ MYSQL_USER: wvp_user
+ MYSQL_PASSWORD: wvp_password
TZ: Asia/Shanghai
- ports:
- - 3306:3306
+ # ports:
+ # - 3306:3306
volumes:
- ./mysql/conf:/etc/mysql/conf.d
- ./logs/mysql:/logs
- ./volumes/mysql/data:/var/lib/mysql
+ - ../数据库/2.7.4/初始化-mysql-2.7.4.sql:/docker-entrypoint-initdb.d/init.sql # 初始化SQL脚本目录
command: [
- 'mysqld',
- '--default-authentication-plugin=mysql_native_password',
+ # '--default-authentication-plugin=mysql_native_password',
'--innodb-buffer-pool-size=80M',
'--character-set-server=utf8mb4',
'--collation-server=utf8mb4_general_ci',
@@ -54,69 +54,99 @@ services:
]
polaris-media:
- image: polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-media:latest
+ image: zlmediakit/zlmediakit:master # 替换为官方镜像
restart: always
networks:
- media-net
ports:
- - "10935:10935"
- - "5540:5540"
- - "6080:6080"
+ #- "6080:80/tcp" # [播流]HTTP 安全考虑-非测试阶段需要注释掉,改为由nginx代理播流地址
+ #- "4443:443/tcp" # [播流]HTTPS 安全考虑-非测试阶段需要注释掉,改为由nginx代理播流地址
+ - "${MediaRtmp:-10935}:${MediaRtmp:-10935}/tcp" # [收流]RTMP
+ - "${MediaRtmp:-10935}:${MediaRtmp:-10935}/udp" # [收流]RTMP
+ #- "41935:41935/tcp" # [收流]RTMPS 无效
+ - "${MediaRtsp:-5540}:${MediaRtsp:-5540}/tcp" # [收流]RTSP
+ - "${MediaRtsp:-5540}:${MediaRtsp:-5540}/udp" # [收流]RTSP
+ #- "45540:45540/tcp" # [收流]RTSPS 无效
+ - "${MediaRtp:-10000}:${MediaRtp:-10000}/tcp" # [收流]RTP
+ - "${MediaRtp:-10000}:${MediaRtp:-10000}/udp" # [收流]RTP
volumes:
- - ./volumes/video:/opt/media/www/record/
+ - ./volumes/video:/opt/media/bin/www/record/
- ./logs/media:/opt/media/log/
+ - ./media/config.ini:/conf/config.ini
+ command: [
+ 'MediaServer',
+ '-c', '/conf/config.ini',
+ '-l', '0'
+ ]
polaris-wvp:
- image: polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-wvp:latest
+ # 显式指定构建上下文和Dockerfile路径
+ build:
+ context: .. # 构建上下文的根路径
+ dockerfile: ./docker/wvp/Dockerfile # 相对于上下文路径的Dockerfile位置
restart: always
networks:
- media-net
ports:
- "18978:18978"
- - "8116:8116/udp"
- - "8116:8116/tcp"
+ - "${SIP_Port:-8116}:${SIP_Port:-8116}/udp"
+ - "${SIP_Port:-8116}:${SIP_Port:-8116}/tcp"
depends_on:
- polaris-redis
- polaris-mysql
- polaris-media
- links:
- - polaris-redis
- - polaris-mysql
- - polaris-media
volumes:
- - ./wvp/wvp/:/opt/wvp/wvp/
+ - ./wvp/wvp/:/opt/ylcx/wvp/
- ./logs/wvp:/opt/wvp/logs/
environment:
TZ: "Asia/Shanghai"
- # 本机的IP
- SIP_HOST: 127.0.0.1
- STREAM_HOST: 127.0.0.1
+ # 流链接的IP
+ Stream_IP: ${Stream_IP}
+ # SDP里的IP
+ SDP_IP: ${SDP_IP}
+ # [可选] zlm服务器访问WVP所使用的IP, 默认使用127.0.0.1,zlm和wvp没有部署在同一台服务器时必须配置
+ ZLM_HOOK_HOST: polaris-wvp
ZLM_HOST: polaris-media
- ZLM_PORT: 6080
ZLM_SERCERT: su6TiedN2rVAmBbIDX0aa0QTiBJLBdcf
+
+ MediaHttp: ${WebHttp:-8080}
+ #MediaHttps: ${WebHttps:-8081}
+ MediaRtmp: ${MediaRtmp:-10935}
+ MediaRtsp: ${MediaRtsp:-5540}
+ MediaRtp: ${MediaRtp:-10000}
+
REDIS_HOST: polaris-redis
REDIS_PORT: 6379
+
DATABASE_HOST: polaris-mysql
DATABASE_PORT: 3306
- DATABASE_USER: wvp
- DATABASE_PASSWORD: wvp
- # 前端跨域配置,nginx容器所在物理机IP
- NGINX_HOST: http://127.0.0.1:8080
+ DATABASE_USER: wvp_user
+ DATABASE_PASSWORD: wvp_password
+
+ SIP_ShowIP: ${SIP_ShowIP}
+ SIP_Port: ${SIP_Port:-8116}
+ SIP_Domain: ${SIP_Domain}
+ SIP_Id: ${SIP_Id}
+ SIP_Password: ${SIP_Password}
+
+ RecordSip: ${RecordSip}
+ RecordPushLive: ${RecordPushLive}
polaris-nginx:
- image: polaris-tian-docker.pkg.coding.net/qt/polaris/polaris-nginx:latest
+ # 显式指定构建上下文和Dockerfile路径
+ build:
+ context: .. # 构建上下文的根路径
+ dockerfile: ./docker/nginx/Dockerfile # 相对于上下文路径的Dockerfile位置
ports:
- - "8080:8080"
+ - "${WebHttp:-8080}:8080"
depends_on:
- polaris-wvp
- links:
- - polaris-wvp
- environment:
- WVP_HOST: polaris-wvp
- WVP_PORT: 18978
volumes:
- - ./nginx/conf/nginx.conf:/etc/nginx/nginx.conf
+ - ./nginx/templates/:/etc/nginx/templates
- ./logs/nginx:/var/log/nginx
+ environment:
+ # 流链接的IP
+ Stream_IP: ${Stream_IP}
networks:
- media-net
diff --git a/docker/media/config.ini b/docker/media/config.ini
index 2098e8d49..cc74281aa 100644
--- a/docker/media/config.ini
+++ b/docker/media/config.ini
@@ -52,21 +52,21 @@ alive_interval=10.0
enable=1
on_flow_report=
on_http_access=
-on_play=
-on_publish=
-on_record_mp4=
+on_play=http://polaris-wvp:18978/index/hook/on_play
+on_publish=http://polaris-wvp:18978/index/hook/on_publish
+on_record_mp4=http://polaris-wvp:18978/index/hook/on_record_mp4
on_record_ts=
-on_rtp_server_timeout=
+on_rtp_server_timeout=http://polaris-wvp:18978/index/hook/on_rtp_server_timeout
on_rtsp_auth=
on_rtsp_realm=
-on_send_rtp_stopped=
+on_send_rtp_stopped=http://polaris-wvp:18978/index/hook/on_send_rtp_stopped
on_server_exited=
-on_server_keepalive=
-on_server_started=
+on_server_keepalive=http://polaris-wvp:18978/index/hook/on_server_keepalive
+on_server_started=http://polaris-wvp:18978/index/hook/on_server_started
on_shell_login=
-on_stream_changed=
-on_stream_none_reader=
-on_stream_not_found=
+on_stream_changed=http://polaris-wvp:18978/index/hook/on_stream_changed
+on_stream_none_reader=http://polaris-wvp:18978/index/hook/on_stream_none_reader
+on_stream_not_found=http://polaris-wvp:18978/index/hook/on_stream_not_found
retry=1
retry_delay=3.0
stream_changed_schemas=rtsp/rtmp/fmp4/ts/hls/hls.fmp4
@@ -82,10 +82,10 @@ forwarded_ip_header=
keepAliveSecond=30
maxReqSize=40960
notFound=
404 Not Found您访问的资源不存在!
ZLMediaKit(git hash:8ccb4e9/%aI,branch:master,build time:2024-11-07T10:34:19)
-port=6080
+port=80
rootPath=./www
sendBufSize=65536
-sslport=4443
+sslport=443
virtualPath=
[multicast]
@@ -99,7 +99,7 @@ auto_close=0
continue_push_ms=3000
enable_audio=1
enable_fmp4=1
-enable_hls=1
+enable_hls=0
enable_hls_fmp4=0
enable_mp4=0
enable_rtmp=1
@@ -111,7 +111,7 @@ hls_save_path=./www
modify_stamp=2
mp4_as_player=0
mp4_max_second=3600
-mp4_save_path=/home
+mp4_save_path=/opt/media/bin/www
paced_sender_ms=0
rtmp_demand=0
rtsp_demand=0
@@ -119,13 +119,14 @@ ts_demand=0
[record]
appName=record
-enableFmp4=0
+enableFmp4=1
fastStart=0
fileBufSize=65536
fileRepeat=0
sampleMS=500
[rtc]
+bfilter=0
datachannel_echo=0
externIP=
maxRtpCacheMS=5000
@@ -150,7 +151,7 @@ directProxy=1
enhanced=0
handshakeSecond=15
keepAliveSecond=15
-port=10935
+port=10001
sslport=0
[rtp]
@@ -165,8 +166,9 @@ dumpDir=
gop_cache=1
h264_pt=98
h265_pt=99
+merge_frame=1
opus_pt=100
-port=10000
+port=10003
port_range=30000-30500
ps_pt=96
rtp_g711_dur_ms=100
@@ -179,7 +181,7 @@ directProxy=1
handshakeSecond=15
keepAliveSecond=15
lowLatency=0
-port=5540
+port=10002
rtpTransportType=-1
sslport=0
@@ -189,6 +191,7 @@ port=0
[srt]
latencyMul=4
+passPhrase=
pktBufSize=8192
port=9000
timeoutSec=5
diff --git a/docker/nginx/Dockerfile b/docker/nginx/Dockerfile
index 91403f8e7..5e57aab51 100644
--- a/docker/nginx/Dockerfile
+++ b/docker/nginx/Dockerfile
@@ -1,19 +1,28 @@
+FROM ubuntu:24.04 AS builder
+
+
+RUN apt-get update && \
+ apt-get install -y nodejs npm && \
+ rm -rf /var/lib/apt/lists/*
+
+COPY ./web /build
+WORKDIR /build
+
+RUN npm --registry=https://registry.npmmirror.com install
+RUN npm run build:prod
+
+WORKDIR /src/main/resources
+RUN ls
+
+WORKDIR /src/main/resources/static
+RUN ls
+
FROM nginx:alpine
-RUN apk add --no-cache bash
-
ARG TZ=Asia/Shanghai
-RUN \
- sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
- apk update && \
- apk add tzdata
-RUN ln -sf /usr/share/zoneinfo/${TZ} /etc/localtime && \
- echo '${TZ}' > /etc/timezone
-RUN rm -rf /etc/nginx/conf.d/*
-RUN mkdir /opt/dist
-COPY ./dist /opt/dist
-COPY ./conf/nginx.conf /etc/nginx/conf.d
+
+COPY --from=builder /src/main/resources/static /opt/dist
CMD ["nginx","-g","daemon off;"]
diff --git a/docker/nginx/conf/nginx.conf b/docker/nginx/conf/nginx.conf
deleted file mode 100644
index ede96ec67..000000000
--- a/docker/nginx/conf/nginx.conf
+++ /dev/null
@@ -1,55 +0,0 @@
-#user nobody;
-worker_processes 1;
-
-#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;
-
- sendfile on;
- #tcp_nopush on;
-
- #keepalive_timeout 0;
- keepalive_timeout 65;
-
- #gzip on;
-
- server {
- listen 8080;
- server_name localhost;
-
- location / {
- root /opt/dist;
- index index.html index.htm;
- }
- location /record_proxy/{
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header REMOTE-HOST $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_pass http://polaris-wvp:18978/;
- }
- location /api/ {
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header REMOTE-HOST $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_pass http://polaris-wvp:18978;
- }
- error_page 500 502 503 504 /50x.html;
- location = /50x.html {
- root html;
- }
- }
-}
diff --git a/docker/nginx/templates/nginx.conf.template b/docker/nginx/templates/nginx.conf.template
new file mode 100644
index 000000000..cf0de139e
--- /dev/null
+++ b/docker/nginx/templates/nginx.conf.template
@@ -0,0 +1,110 @@
+server {
+ listen 8080;
+ server_name localhost;
+
+ location / {
+ root /opt/dist;
+ index index.html index.htm;
+ }
+ location /record_proxy/{
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header REMOTE-HOST $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_pass http://polaris-wvp:18978/;
+ }
+ location /api/ {
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header REMOTE-HOST $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_pass http://polaris-wvp:18978;
+
+
+ # 从环境变量获取原始主机地址(x.x.x.x)
+ set $original_host ${Stream_IP};
+
+ # 执行字符串替换
+ # 将媒体资源文件替换为Nginx输出的相对地址
+ sub_filter "http://$original_host/index/api/downloadFile" "mediaserver/api/downloadFile";
+ sub_filter "http://$original_host:80/index/api/downloadFile" "mediaserver/api/downloadFile";
+ sub_filter "https://$original_host/index/api/downloadFile" "mediaserver/api/downloadFile";
+ sub_filter "https://$original_host:443/index/api/downloadFile" "mediaserver/api/downloadFile";
+ sub_filter "http://$original_host/mp4_record" "mp4_record";
+ sub_filter "http://$original_host:80/mp4_record" "mp4_record";
+ sub_filter "https://$original_host/mp4_record" "mp4_record";
+ sub_filter "https://$original_host:443/mp4_record" "mp4_record";
+
+ # 设置为off表示替换所有匹配项,而不仅仅是第一个
+ sub_filter_once off;
+
+ # 确保响应被正确处理
+ sub_filter_types application/json; # 只对JSON响应进行处理
+ }
+
+ # 将mediaserver/record转发到目标地址
+ location /mediaserver/api/downloadFile {
+ # 目标服务器地址
+ proxy_pass http://polaris-media:80/index/api/downloadFile;
+
+ # 以下是常用的反向代理设置
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # 超时设置,根据需要调整
+ proxy_connect_timeout 300s;
+ proxy_send_timeout 300s;
+ proxy_read_timeout 300s;
+ }
+
+ # 仅允许代理/rtp/开头的路径
+ location ^~ /rtp/ {
+ # 代理到ZLMediakit服务
+ proxy_pass http://polaris-media:80;
+
+ # 基础HTTP代理配置
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # WebSocket支持配置
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ # 超时设置,根据实际需求调整
+ proxy_connect_timeout 60s;
+ proxy_read_timeout 3600s;
+ proxy_send_timeout 60s;
+ }
+
+ # 仅允许代理/rtp/开头的路径
+ location ^~ /mp4_record/ {
+ # 代理到ZLMediakit服务
+ proxy_pass http://polaris-media:80;
+
+ # 基础HTTP代理配置
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # WebSocket支持配置
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+
+ # 超时设置,根据实际需求调整
+ proxy_connect_timeout 60s;
+ proxy_read_timeout 3600s;
+ proxy_send_timeout 60s;
+ }
+
+ error_page 500 502 503 504 /50x.html;
+ location = /50x.html {
+ root html;
+ }
+}
diff --git a/docker/wvp/Dockerfile b/docker/wvp/Dockerfile
index 67286b19f..e66b4cda4 100644
--- a/docker/wvp/Dockerfile
+++ b/docker/wvp/Dockerfile
@@ -1,64 +1,84 @@
-FROM ubuntu:20.04 AS build
-ARG Platfrom=amd64
-ARG JDK_NAME
+FROM ringcentral/jdk:11 AS builder
EXPOSE 18978/tcp
EXPOSE 8116/tcp
EXPOSE 8116/udp
EXPOSE 8080/tcp
-RUN apt-get update && \
- DEBIAN_FRONTEND="noninteractive" \
- apt-get install -y --no-install-recommends \
- wget \
- cmake \
- maven \
- git \
- ca-certificates \
- tzdata \
- curl \
- libpcre3 \
- libpcre3-dev \
- zlib1g-dev \
- openssl \
- libssl-dev \
- gdb && \
- apt-get autoremove -y && \
- apt-get clean -y && \
- rm -rf /var/lib/apt/lists/*
+#RUN apt-get update && \
+ #DEBIAN_FRONTEND="noninteractive" \
+ #apt-get install -y --no-install-recommends \
+ #wget \
+ #cmake \
+ #maven \
+ #git \
+ #ca-certificates \
+ #tzdata \
+ #curl \
+ #libpcre3 \
+ #libpcre3-dev \
+ #zlib1g-dev \
+ #openssl \
+ #libssl-dev \
+ #gdb && \
+ #apt-get autoremove -y && \
+ #apt-get clean -y && \
+ #rm -rf /var/lib/apt/lists/*
-# install jdk1.8
-RUN mkdir -p /opt/download
-WORKDIR /opt/download
-RUN if [ "$Platfrom" = "arm64" ]; \
- then \
- wget https://polaris-tian-generic.pkg.coding.net/qt/autopliot/jdk-8u411-linux-aarch64.tar.gz?version=latest --no-check-certificate -O jdk-8.tar.gz && \
- tar -zxvf /opt/download/jdk-8.tar.gz -C /usr/local/ --transform 's/jdk1.8.0_411/java/' && \
- rm /opt/download/jdk-8.tar.gz; \
- else \
- wget https://polaris-tian-generic.pkg.coding.net/qt/autopliot/jdk-8u202-linux-x64.tar.gz?version=latest --no-check-certificate -O jdk-8.tar.gz && \
- tar -zxvf /opt/download/jdk-8.tar.gz -C /usr/local/ --transform 's/jdk1.8.0_202/java/' && \
- rm /opt/download/jdk-8.tar.gz; \
- fi
+## install jdk1.8
+#RUN mkdir -p /opt/download
+#WORKDIR /opt/download
+#RUN if [ "$Platfrom" = "arm64" ]; \
+ #then \
+ #wget https://polaris-tian-generic.pkg.coding.net/qt/autopliot/jdk-8u411-linux-aarch64.tar.gz?version=latest --no-check-certificate -O jdk-8.tar.gz && \
+ #tar -zxvf /opt/download/jdk-8.tar.gz -C /usr/local/ --transform 's/jdk1.8.0_411/java/' && \
+ #rm /opt/download/jdk-8.tar.gz; \
+ #else \
+ #wget https://polaris-tian-generic.pkg.coding.net/qt/autopliot/jdk-8u202-linux-x64.tar.gz?version=latest --no-check-certificate -O jdk-8.tar.gz && \
+ #tar -zxvf /opt/download/jdk-8.tar.gz -C /usr/local/ --transform 's/jdk1.8.0_202/java/' && \
+ #rm /opt/download/jdk-8.tar.gz; \
+ #fi
-ENV JAVA_HOME /usr/local/java/
-ENV JRE_HOME ${JAVA_HOME}/jre
-ENV CLASSPATH .:${JAVA_HOME}/lib:${JRE_HOME}/lib
-ENV PATH ${JAVA_HOME}/bin:$PATH
+#ENV JAVA_HOME /usr/local/java/
+#ENV JRE_HOME ${JAVA_HOME}/jre
+#ENV CLASSPATH .:${JAVA_HOME}/lib:${JRE_HOME}/lib
+#ENV PATH ${JAVA_HOME}/bin:$PATH
RUN java -version && javac -version
+#RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \
+RUN apt-get update && \
+ apt-get install -y maven && \
+ rm -rf /var/lib/apt/lists/*
+
+
+COPY . /build
+WORKDIR /build
+RUN ls && mvn clean package -Dmaven.test.skip=true
+WORKDIR /build/target
+RUN mv wvp-pro-*.jar wvp.jar
+
+
+FROM ringcentral/jdk:11
RUN mkdir -p /opt/wvp
WORKDIR /opt/wvp
-COPY ./wvp /opt/wvp
+COPY --from=builder /build/target /opt/wvp
+COPY ./docker/wvp/wvp /opt/wvp
+ENTRYPOINT ["java", "-Xms512m", "-Xmx1024m", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/opt/ylcx/", "-jar", "wvp.jar", "--spring.config.location=/opt/ylcx/wvp/application.yml"]
-WORKDIR /home
-RUN cd /home && \
- git clone https://gitee.com/pan648540858/wvp-GB28181-pro.git
-RUN cd /home/wvp-GB28181-pro && \
- mvn clean package -Dmaven.test.skip=true && \
- cp /home/wvp-GB28181-pro/target/*.jar /opt/wvp/wvp.jar
-WORKDIR /opt/wvp
-ENTRYPOINT ["java", "-Xms512m", "-Xmx1024m", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/opt/ylcx/", "-jar", "wvp.jar", "--spring.config.location=/opt/ylcx/wvp/application.yml"]
\ No newline at end of file
+#RUN mkdir -p /opt/wvp
+#WORKDIR /opt/wvp
+#COPY ./wvp /opt/wvp
+#
+#WORKDIR /home
+#RUN cd /home && \
+ #git clone https://gitee.com/pan648540858/wvp-GB28181-pro.git
+#
+#RUN cd /home/wvp-GB28181-pro && \
+ #mvn clean package -Dmaven.test.skip=true && \
+ #cp /home/wvp-GB28181-pro/target/*.jar /opt/wvp/wvp.jar
+#
+#WORKDIR /opt/wvp
+#ENTRYPOINT ["java", "-Xms512m", "-Xmx1024m", "-XX:+HeapDumpOnOutOfMemoryError", "-XX:HeapDumpPath=/opt/ylcx/", "-jar", "wvp.jar", "--spring.config.location=/opt/ylcx/wvp/application.yml"]
\ No newline at end of file
diff --git a/docker/wvp/wvp/application-docker.yml b/docker/wvp/wvp/application-docker.yml
index 68c411e7b..04eeff8df 100644
--- a/docker/wvp/wvp/application-docker.yml
+++ b/docker/wvp/wvp/application-docker.yml
@@ -1,73 +1,107 @@
spring:
- # 设置接口超时时间
- mvc:
- async:
- request-timeout: 20000
- thymeleaf:
- cache: false
- # [可选]上传文件大小限制
- servlet:
- multipart:
- max-file-size: 10MB
- max-request-size: 100MB
- # REDIS数据库配置
- redis:
- # [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1
- host: ${REDIS_HOST:127.0.0.1}
- # [必须修改] 端口号
- port: ${REDIS_PORT:6379}
- # [可选] 数据库 DB
- database: 1
- # [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接
- password:
- # [可选] 超时时间
- timeout: 30000
- # mysql数据源
- datasource:
- type: com.zaxxer.hikari.HikariDataSource
- driver-class-name: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://${DATABASE_HOST:127.0.0.1}:${DATABASE_PORT:3306}/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true&allowPublicKeyRetrieval=true
- username: ${DATABASE_USER:root}
- password: ${DATABASE_PASSWORD:root}
+ cache:
+ type: redis
+ thymeleaf:
+ cache: false
+ # 设置接口超时时间
+ mvc:
+ async:
+ request-timeout: 20000
+ # [可选]上传文件大小限制
+ servlet:
+ multipart:
+ max-file-size: 10MB
+ max-request-size: 100MB
+ # REDIS数据库配置
+ redis:
+ # [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1
+ host: ${REDIS_HOST:127.0.0.1}
+ # [必须修改] 端口号
+ port: ${REDIS_PORT:6379}
+ # [可选] 数据库 DB
+ database: 1
+ # [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接
+ password:
+ # [可选] 超时时间
+ timeout: 10000
+ ## [可选] 一个pool最多可分配多少个jedis实例
+ #poolMaxTotal: 1000
+ ## [可选] 一个pool最多有多少个状态为idle(空闲)的jedis实例
+ #poolMaxIdle: 500
+ ## [可选] 最大的等待时间(秒)
+ #poolMaxWait: 5
+ # [必选] jdbc数据库配置
+ datasource:
+ # mysql数据源
+ type: com.zaxxer.hikari.HikariDataSource
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ url: jdbc:mysql://${DATABASE_HOST:127.0.0.1}:${DATABASE_PORT:3306}/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true&allowPublicKeyRetrieval=true
+ username: ${DATABASE_USER:root}
+ password: ${DATABASE_PASSWORD:root}
#[可选] 监听的HTTP端口, 网页和接口调用都是这个端口
server:
- port: 18978
- ssl:
- # [可选] 是否开启HTTPS访问
- enabled: false
+ port: 18978
+ ssl:
+ # [可选] 是否开启HTTPS访问
+ # docker里运行,内部不需要HTTPS
+ enabled: false
# 作为28181服务器的配置
sip:
- # [必须修改] 本机的IP
- ip: ${SIP_HOST:127.0.0.1}
- # [可选]
- port: 8116
- # [可选]
- domain: 3402000000
- # [可选]
- id: 34020000002000000001
- password:
- alarm: true
+ # [必须修改] 本机的IP,对应你的网卡,监听什么ip就是使用什么网卡,
+ # 如果要监听多张网卡,可以使用逗号分隔多个IP, 例如: 192.168.1.4,10.0.0.4
+ # 如果不明白,就使用0.0.0.0,大部分情况都是可以的
+ # 请不要使用127.0.0.1,任何包括localhost在内的域名都是不可以的。
+ ip: 0.0.0.0
+ # [可选] 没有任何业务需求,仅仅是在前端展示的时候用
+ show-ip: ${SIP_ShowIP}
+ # [可选]
+ port: ${SIP_Port:8116}
+ # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007)
+ # 后两位为行业编码,定义参照附录D.3
+ # 3701020049标识山东济南历下区 信息行业接入
+ # [可选]
+ domain: ${SIP_Domain:3402000000}
+ # [可选]
+ id: ${SIP_Id:34020000002000000001}
+ # [可选] 默认设备认证密码,后续扩展使用设备单独密码, 移除密码将不进行校验
+ password: ${SIP_Password}
+ # [可选] 国标级联注册失败,再次发起注册的时间间隔。 默认60秒
+ register-time-interval: 60
+ # [可选] 云台控制速度
+ ptz-speed: 50
+ # TODO [可选] 收到心跳后自动上线, 重启服务后会将所有设备置为离线,默认false,等待注册后上线。设置为true则收到心跳设置为上线。
+ # keepalliveToOnline: false
+ # 是否存储alarm信息
+ alarm: true
+ # 命令发送等待回复的超时时间, 单位:毫秒
+ timeout: 1000
# 默认服务器配置
media:
id: polaris
# [必须修改] ZLM 内网IP与端口
ip: ${ZLM_HOST:127.0.0.1}
- http-port: ${ZLM_PORT:6080}
+ http-port: 80
# [可选] 返回流地址时的ip,置空使用 media.ip
- stream-ip: ${STREAM_HOST:127.0.0.1}
+ stream-ip: ${Stream_IP}
# [可选] wvp在国标信令中使用的ip,此ip为摄像机可以访问到的ip, 置空使用 media.ip
- sdp-ip: ${SIP_HOST:127.0.0.1}
- # [可选] Hook IP, 默认使用sip.ip
- hook-ip: ${SIP_HOST:127.0.0.1}
+ sdp-ip: ${SDP_IP}
+ # [可选] zlm服务器访问WVP所使用的IP, 默认使用127.0.0.1,zlm和wvp没有部署在同一台服务器时必须配置
+ hook-ip: ${ZLM_HOOK_HOST}
# [可选] sslport
- http-ssl-port: 4443
- rtp-proxy-port: 10000
- rtmp-port: 10935
- rtmp-ssl-port: 41935
- rtsp-port: 5540
- rtsp-ssl-port: 45540
+ http-ssl-port: 0
+ flv-port: ${MediaHttp:}
+ flv-ssl-port: ${MediaHttps:}
+ ws-flv-port: ${MediaHttp:}
+ ws-flv-ssl-port: ${MediaHttps:}
+ rtp-proxy-port: ${MediaRtp:}
+ rtmp-port: ${MediaRtmp:}
+ rtmp-ssl-port: 0
+ rtsp-port: ${MediaRtsp:}
+ rtsp-ssl-port: 0
+ # [可选] 是否自动配置ZLM, 如果希望手动配置ZLM, 可以设为false, 不建议新接触的用户修改
+ auto-config: true
# [可选]
secret: ${ZLM_SERCERT}
# 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试
@@ -79,28 +113,28 @@ media:
# [可选]
send-port-range: 50502,50506
- record-path: /opt/media/record
+ record-path: /opt/media/bin/www/record/
record-day: 7
record-assist-port: 0
user-settings:
auto-apply-play: true
play-timeout: 30000
wait-track: false
- record-push-live: false
- record-sip: false
+ record-push-live: ${RecordPushLive:false}
+ record-sip: ${RecordSip:false}
stream-on-demand: true
- interface-authentication: false
+ interface-authentication: true
broadcast-for-platform: TCP-PASSIVE
push-stream-after-ack: true
send-to-platforms-when-id-lost: true
interface-authentication-excludes:
- - /api/**
- push-authority: false
- allowed-origins:
- - http://localhost:8080
- - http://127.0.0.1:8080
- - http://0.0.0.0:8080
- - ${NGINX_HOST}
+ # - /api/**
+ push-authority: true
+ # allowed-origins:
+ # - http://localhost:8080
+ # - http://127.0.0.1:8080
+ # - http://0.0.0.0:8080
+ # - ${NGINX_HOST}
logging:
config: classpath:logback-spring.xml
diff --git a/pom.xml b/pom.xml
index 705f07b0f..b3eb307d6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -364,6 +364,27 @@
32.1.3-jre
+
+
+ org.apache.ftpserver
+ ftpserver-core
+ 1.2.0
+
+
+
+ org.apache.ftpserver
+ ftplet-api
+ 1.2.0
+
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+ provided
+
+
org.projectlombok
@@ -383,6 +404,7 @@
log-viewer-spring-boot
1.0.10
+
org.springframework.boot
spring-boot-starter-test
diff --git a/src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java b/src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java
index 28e7d6416..78c93e523 100644
--- a/src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java
+++ b/src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java
@@ -79,6 +79,8 @@ public class StreamInfo implements Serializable, Cloneable{
private String startTime;
@Schema(description = "结束时间")
private String endTime;
+ @Schema(description = "时长(回放时使用)")
+ private Double duration;
@Schema(description = "进度(录像下载使用)")
private double progress;
@Schema(description = "文件下载地址(录像下载使用)")
@@ -101,87 +103,112 @@ public class StreamInfo implements Serializable, Cloneable{
@Schema(description = "使用的WVP ID")
private String serverId;
- public void setRtmp(String host, int port, int sslPort, String app, String stream, String callIdParam) {
+ @Schema(description = "流绑定的流媒体操作key")
+ private String key;
+
+ public void setRtmp(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) {
String file = String.format("%s/%s%s", app, stream, callIdParam);
- if (port > 0) {
+ if (port != null && port > 0) {
this.rtmp = new StreamURL("rtmp", host, port, file);
}
- if (sslPort > 0) {
+ if (sslPort != null && sslPort > 0) {
this.rtmps = new StreamURL("rtmps", host, sslPort, file);
}
}
- public void setRtsp(String host, int port, int sslPort, String app, String stream, String callIdParam) {
+ public void setRtsp(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) {
String file = String.format("%s/%s%s", app, stream, callIdParam);
- if (port > 0) {
+ if (port != null && port > 0) {
this.rtsp = new StreamURL("rtsp", host, port, file);
}
- if (sslPort > 0) {
+ if (sslPort != null && sslPort > 0) {
this.rtsps = new StreamURL("rtsps", host, sslPort, file);
}
}
- public void setFlv(String host, int port, int sslPort, String file) {
- if (port > 0) {
+ public void setFlv(String host, Integer port, Integer sslPort, String file) {
+ if (port != null && port > 0) {
this.flv = new StreamURL("http", host, port, file);
}
- this.ws_flv = new StreamURL("ws", host, port, file);
- if (sslPort > 0) {
+ if (sslPort != null && sslPort > 0) {
this.https_flv = new StreamURL("https", host, sslPort, file);
- this.wss_flv = new StreamURL("wss", host, sslPort, file);
}
}
- public void setWsFlv(String host, int port, int sslPort, String file) {
- if (port > 0) {
+ public void setWsFlv(String host, Integer port, Integer sslPort, String file) {
+ if (port != null && port > 0) {
this.ws_flv = new StreamURL("ws", host, port, file);
}
- if (sslPort > 0) {
+ if (sslPort != null && sslPort > 0) {
this.wss_flv = new StreamURL("wss", host, sslPort, file);
}
}
- public void setFmp4(String host, int port, int sslPort, String app, String stream, String callIdParam) {
- String file = String.format("%s/%s.live.mp4%s", app, stream, callIdParam);
- if (port > 0) {
+ public void setFmp4(String host, Integer port, Integer sslPort, String file) {
+ if (port != null && port > 0) {
this.fmp4 = new StreamURL("http", host, port, file);
+ }
+ if (sslPort != null && sslPort > 0) {
+ this.https_fmp4 = new StreamURL("https", host, sslPort, file);
+ }
+ }
+
+ public void setWsMp4(String host, Integer port, Integer sslPort, String file) {
+ if (port != null && port > 0) {
this.ws_fmp4 = new StreamURL("ws", host, port, file);
}
- if (sslPort > 0) {
- this.https_fmp4 = new StreamURL("https", host, sslPort, file);
+ if (sslPort != null && sslPort > 0) {
this.wss_fmp4 = new StreamURL("wss", host, sslPort, file);
}
}
- public void setHls(String host, int port, int sslPort, String app, String stream, String callIdParam) {
+ public void setHls(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) {
String file = String.format("%s/%s/hls.m3u8%s", app, stream, callIdParam);
- if (port > 0) {
+ if (port != null && port > 0) {
this.hls = new StreamURL("http", host, port, file);
+ }
+ if (sslPort != null && sslPort > 0) {
+ this.https_hls = new StreamURL("https", host, sslPort, file);
+ }
+ }
+
+ public void setWsHls(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) {
+ String file = String.format("%s/%s/hls.m3u8%s", app, stream, callIdParam);
+ if (port != null && port > 0) {
this.ws_hls = new StreamURL("ws", host, port, file);
}
- if (sslPort > 0) {
- this.https_hls = new StreamURL("https", host, sslPort, file);
+ if (sslPort != null && sslPort > 0) {
this.wss_hls = new StreamURL("wss", host, sslPort, file);
}
}
- public void setTs(String host, int port, int sslPort, String app, String stream, String callIdParam) {
+ public void setTs(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) {
String file = String.format("%s/%s.live.ts%s", app, stream, callIdParam);
- if (port > 0) {
+ if (port != null && port > 0) {
this.ts = new StreamURL("http", host, port, file);
+ }
+ if (sslPort != null && sslPort > 0) {
+ this.https_ts = new StreamURL("https", host, sslPort, file);
+ }
+ }
+
+ public void setWsTs(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam) {
+ String file = String.format("%s/%s.live.ts%s", app, stream, callIdParam);
+
+ if (port != null && port > 0) {
this.ws_ts = new StreamURL("ws", host, port, file);
}
- if (sslPort > 0) {
- this.https_ts = new StreamURL("https", host, sslPort, file);
+ if (sslPort != null && sslPort > 0) {
this.wss_ts = new StreamURL("wss", host, sslPort, file);
}
}
- public void setRtc(String host, int port, int sslPort, String app, String stream, String callIdParam, boolean isPlay) {
+ public void setRtc(String host, Integer port, Integer sslPort, String app, String stream, String callIdParam, boolean isPlay) {
if (callIdParam != null) {
callIdParam = Objects.equals(callIdParam, "") ? callIdParam : callIdParam.replace("?", "&");
}
+// String file = String.format("%s/%s?type=%s%s", app, stream, isPlay?"play":"push", callIdParam);
String file = String.format("index/api/webrtc?app=%s&stream=%s&type=%s%s", app, stream, isPlay?"play":"push", callIdParam);
if (port > 0) {
this.rtc = new StreamURL("http", host, port, file);
diff --git a/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java b/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java
index 17bd502df..ca0be92e3 100644
--- a/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java
+++ b/src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java
@@ -153,4 +153,15 @@ public class VideoManagerConstants {
*/
public static final String REDIS_RECORD_INFO_RES_COUNT_PRE = "GB_RECORD_INFO_RES_COUNT:";
+ //************************** 1078 ****************************************
+
+
+ public static final String INVITE_INFO_1078_POSITION = "INVITE_INFO_1078_POSITION:";
+ public static final String INVITE_INFO_1078_PLAY = "INVITE_INFO_1078_PLAY:";
+ public static final String INVITE_INFO_1078_PLAYBACK = "INVITE_INFO_1078_PLAYBACK:";
+ public static final String INVITE_INFO_1078_TALK = "INVITE_INFO_1078_TALK:";
+
+
+ public static final String RECORD_LIST_1078 = "RECORD_LIST_1078:";
+
}
diff --git a/src/main/java/com/genersoft/iot/vmp/common/enums/ChannelDataType.java b/src/main/java/com/genersoft/iot/vmp/common/enums/ChannelDataType.java
index c2d2a11e0..27a09eafc 100644
--- a/src/main/java/com/genersoft/iot/vmp/common/enums/ChannelDataType.java
+++ b/src/main/java/com/genersoft/iot/vmp/common/enums/ChannelDataType.java
@@ -4,18 +4,18 @@ package com.genersoft.iot.vmp.common.enums;
* 支持的通道数据类型
*/
-public enum ChannelDataType {
+public class ChannelDataType {
- GB28181(1,"国标28181"),
- STREAM_PUSH(2,"推流设备"),
- STREAM_PROXY(3,"拉流代理");
+ public final static int GB28181 = 1;
+ public final static int STREAM_PUSH = 2;
+ public final static int STREAM_PROXY = 3;
+ public final static int JT_1078 = 200;
+
+ public final static String PLAY_SERVICE = "sourceChannelPlayService";
+ public final static String PLAYBACK_SERVICE = "sourceChannelPlaybackService";
+ public final static String DOWNLOAD_SERVICE = "sourceChannelDownloadService";
+ public final static String PTZ_SERVICE = "sourceChannelPTZService";
- public final int value;
- public final String desc;
- ChannelDataType(Integer value, String desc) {
- this.value = value;
- this.desc = desc;
- }
}
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java
index ef9669d5d..51a586c97 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java
@@ -2,6 +2,7 @@ package com.genersoft.iot.vmp.conf;
import com.genersoft.iot.vmp.media.bean.MediaServer;
import com.genersoft.iot.vmp.utils.DateUtil;
+import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@@ -15,6 +16,7 @@ import java.util.regex.Pattern;
@Slf4j
@Configuration("mediaConfig")
@Order(0)
+@Data
public class MediaConfig{
// 修改必须配置,不再支持自动获取
@@ -45,6 +47,9 @@ public class MediaConfig{
@Value("${media.flv-port:0}")
private Integer flvPort = 0;
+ @Value("${media.mp4-port:0}")
+ private Integer mp4Port = 0;
+
@Value("${media.ws-flv-port:0}")
private Integer wsFlvPort = 0;
@@ -66,6 +71,9 @@ public class MediaConfig{
@Value("${media.rtp-proxy-port:0}")
private Integer rtpProxyPort = 0;
+ @Value("${media.jtt-proxy-port:0}")
+ private Integer jttProxyPort = 0;
+
@Value("${media.rtsp-port:0}")
private Integer rtspPort = 0;
@@ -99,33 +107,7 @@ public class MediaConfig{
@Value("${media.type:zlm}")
private String type;
- public String getId() {
- return id;
- }
- public String getIp() {
- return ip;
- }
-
- public String getHookIp() {
- return hookIp;
- }
-
- public int getHttpPort() {
- return httpPort;
- }
-
- public int getHttpSSlPort() {
- return httpSSlPort;
- }
-
- public int getRtmpPort() {
- return rtmpPort;
- }
-
- public int getRtmpSSlPort() {
- return rtmpSSlPort;
- }
public int getRtpProxyPort() {
if (rtpProxyPort == null) {
@@ -136,32 +118,12 @@ public class MediaConfig{
}
- public int getRtspPort() {
- return rtspPort;
- }
-
- public int getRtspSSLPort() {
- return rtspSSLPort;
- }
-
- public boolean isAutoConfig() {
- return autoConfig;
- }
-
- public String getSecret() {
- return secret;
- }
-
- public boolean isRtpEnable() {
- return rtpEnable;
- }
-
- public String getRtpPortRange() {
- return rtpPortRange;
- }
-
- public int getRecordAssistPort() {
- return recordAssistPort;
+ public Integer getJttProxyPort() {
+ if (jttProxyPort == null) {
+ return 0;
+ }else {
+ return jttProxyPort;
+ }
}
public String getSdpIp() {
@@ -191,10 +153,6 @@ public class MediaConfig{
}
}
- public String getSipDomain() {
- return sipDomain;
- }
-
public MediaServer getMediaSerItem(){
MediaServer mediaServer = new MediaServer();
mediaServer.setId(id);
@@ -204,31 +162,17 @@ public class MediaConfig{
mediaServer.setSdpIp(getSdpIp());
mediaServer.setStreamIp(getStreamIp());
mediaServer.setHttpPort(httpPort);
- if (flvPort == 0) {
- mediaServer.setFlvPort(httpPort);
- }else {
- mediaServer.setFlvPort(flvPort);
- }
- if (wsFlvPort == 0) {
- mediaServer.setWsFlvPort(httpPort);
- }else {
- mediaServer.setWsFlvPort(wsFlvPort);
- }
- if (flvSSlPort == 0) {
- mediaServer.setFlvSSLPort(httpSSlPort);
- }else {
- mediaServer.setFlvSSLPort(flvSSlPort);
- }
- if (wsFlvSSlPort == 0) {
- mediaServer.setWsFlvSSLPort(httpSSlPort);
- }else {
- mediaServer.setWsFlvSSLPort(wsFlvSSlPort);
- }
+ mediaServer.setFlvPort(flvPort);
+ mediaServer.setMp4Port(mp4Port);
+ mediaServer.setWsFlvPort(wsFlvPort);
+ mediaServer.setFlvSSLPort(flvSSlPort);
+ mediaServer.setWsFlvSSLPort(wsFlvSSlPort);
mediaServer.setHttpSSlPort(httpSSlPort);
mediaServer.setRtmpPort(rtmpPort);
mediaServer.setRtmpSSlPort(rtmpSSlPort);
mediaServer.setRtpProxyPort(getRtpProxyPort());
+ mediaServer.setJttProxyPort(getJttProxyPort());
mediaServer.setRtspPort(rtspPort);
mediaServer.setRtspSSLPort(rtspSSLPort);
mediaServer.setAutoConfig(autoConfig);
@@ -250,42 +194,10 @@ public class MediaConfig{
return mediaServer;
}
- public Integer getRecordDay() {
- return recordDay;
- }
-
- public void setRecordDay(Integer recordDay) {
- this.recordDay = recordDay;
- }
-
- public String getRecordPath() {
- return recordPath;
- }
-
- public void setRecordPath(String recordPath) {
- this.recordPath = recordPath;
- }
-
- public String getRtpSendPortRange() {
- return rtpSendPortRange;
- }
-
- public void setRtpSendPortRange(String rtpSendPortRange) {
- this.rtpSendPortRange = rtpSendPortRange;
- }
-
private boolean isValidIPAddress(String ipAddress) {
if ((ipAddress != null) && (!ipAddress.isEmpty())) {
return Pattern.matches("^([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}$", ipAddress);
}
return false;
}
-
- public String getWanIp() {
- return wanIp;
- }
-
- public void setWanIp(String wanIp) {
- this.wanIp = wanIp;
- }
}
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ServiceInfo.java b/src/main/java/com/genersoft/iot/vmp/conf/ServiceInfo.java
index 3a503a896..67a0e4f7a 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/ServiceInfo.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/ServiceInfo.java
@@ -1,5 +1,6 @@
package com.genersoft.iot.vmp.conf;
+import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.context.ApplicationListener;
@@ -9,17 +10,14 @@ import org.springframework.stereotype.Component;
@Component
public class ServiceInfo implements ApplicationListener {
+ @Getter
private static int serverPort;
- public static int getServerPort() {
- return serverPort;
- }
-
@Override
public void onApplicationEvent(WebServerInitializedEvent event) {
// 项目启动获取启动的端口号
ServiceInfo.serverPort = event.getWebServer().getPort();
- log.info("项目启动获取启动的端口号: " + ServiceInfo.serverPort);
+ log.info("项目启动获取启动的端口号: {}", ServiceInfo.serverPort);
}
public void setServerPort(int serverPort) {
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/SpringDocConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/SpringDocConfig.java
index db5b6b4fc..245aba36f 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/SpringDocConfig.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/SpringDocConfig.java
@@ -98,4 +98,12 @@ public class SpringDocConfig {
.packagesToScan("com.genersoft.iot.vmp.user")
.build();
}
+
+ @Bean
+ public GroupedOpenApi publicApi7() {
+ return GroupedOpenApi.builder()
+ .group("6. 部标设备")
+ .packagesToScan("com.genersoft.iot.vmp.jt1078.controller")
+ .build();
+ }
}
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
index 26130a02b..26302e782 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
@@ -204,6 +204,9 @@ public class UserSetting {
*/
private boolean sipCacheServerConnections = true;
-
+ /**
+ * 禁用date头,变相禁用了校时
+ */
+ private boolean disableDateHeader = false;
}
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FileCallback.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FileCallback.java
new file mode 100644
index 000000000..aa564fcd7
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FileCallback.java
@@ -0,0 +1,8 @@
+package com.genersoft.iot.vmp.conf.ftpServer;
+
+import java.io.OutputStream;
+
+public interface FileCallback {
+
+ OutputStream run(String path);
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpAuthority.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpAuthority.java
new file mode 100644
index 000000000..0ccb14452
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpAuthority.java
@@ -0,0 +1,17 @@
+package com.genersoft.iot.vmp.conf.ftpServer;
+
+import org.apache.ftpserver.ftplet.Authority;
+import org.apache.ftpserver.ftplet.AuthorizationRequest;
+
+public class FtpAuthority implements Authority {
+
+ @Override
+ public boolean canAuthorize(AuthorizationRequest authorizationRequest) {
+ return true;
+ }
+
+ @Override
+ public AuthorizationRequest authorize(AuthorizationRequest authorizationRequest) {
+ return authorizationRequest;
+ }
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemFactory.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemFactory.java
new file mode 100644
index 000000000..5d771e9e0
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemFactory.java
@@ -0,0 +1,33 @@
+package com.genersoft.iot.vmp.conf.ftpServer;
+
+import org.apache.ftpserver.ftplet.FileSystemFactory;
+import org.apache.ftpserver.ftplet.FileSystemView;
+import org.apache.ftpserver.ftplet.FtpException;
+import org.apache.ftpserver.ftplet.User;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.OutputStream;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class FtpFileSystemFactory implements FileSystemFactory {
+
+ private final Map outputStreamMap = new ConcurrentHashMap<>();
+
+ @Override
+ public FileSystemView createFileSystemView(User user) throws FtpException {
+ return new FtpFileSystemView(user, path -> {
+ return outputStreamMap.get(path);
+ });
+ }
+
+ public void addOutputStream(String filePath, OutputStream outputStream) {
+ outputStreamMap.put(filePath, outputStream);
+ }
+
+ public void removeOutputStream(String filePath) {
+ outputStreamMap.remove(filePath);
+ }
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemView.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemView.java
new file mode 100644
index 000000000..e7e8d3d57
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpFileSystemView.java
@@ -0,0 +1,63 @@
+package com.genersoft.iot.vmp.conf.ftpServer;
+
+import org.apache.ftpserver.ftplet.FileSystemView;
+import org.apache.ftpserver.ftplet.FtpException;
+import org.apache.ftpserver.ftplet.FtpFile;
+import org.apache.ftpserver.ftplet.User;
+
+import java.io.OutputStream;
+
+public class FtpFileSystemView implements FileSystemView {
+
+ private User user;
+
+ private FileCallback fileCallback;
+
+ public FtpFileSystemView(User user, FileCallback fileCallback) {
+ this.user = user;
+ this.fileCallback = fileCallback;
+ }
+
+ public static String HOME_PATH = "root";
+
+ public FtpFile workDir = VirtualFtpFile.getDir(HOME_PATH);
+
+ @Override
+ public FtpFile getHomeDirectory() throws FtpException {
+ return VirtualFtpFile.getDir(HOME_PATH);
+ }
+
+ @Override
+ public FtpFile getWorkingDirectory() throws FtpException {
+ return workDir;
+ }
+
+ @Override
+ public boolean changeWorkingDirectory(String dir) throws FtpException {
+ workDir = VirtualFtpFile.getDir(dir);
+ return true;
+ }
+
+ @Override
+ public FtpFile getFile(String file) throws FtpException {
+ VirtualFtpFile ftpFile = VirtualFtpFile.getFile(file);
+ if (fileCallback != null) {
+ OutputStream outputStream = fileCallback.run(workDir.getName());
+ if (outputStream != null) {
+ ftpFile.setOutputStream(outputStream);
+ }
+ }
+ return ftpFile;
+ }
+
+ @Override
+ public boolean isRandomAccessible() throws FtpException {
+ return true;
+ }
+
+ @Override
+ public void dispose() {
+ }
+
+
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpServerConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpServerConfig.java
new file mode 100644
index 000000000..d04016305
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpServerConfig.java
@@ -0,0 +1,69 @@
+package com.genersoft.iot.vmp.conf.ftpServer;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ftpserver.*;
+import org.apache.ftpserver.ftplet.FtpException;
+import org.apache.ftpserver.listener.Listener;
+import org.apache.ftpserver.listener.ListenerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+@ConditionalOnProperty(value = "ftp.enable", havingValue = "true")
+@Slf4j
+public class FtpServerConfig {
+
+ @Autowired
+ private UserManager userManager;
+
+ @Autowired
+ private FtpFileSystemFactory fileSystemFactory;
+
+ @Autowired
+ private Ftplet ftplet;
+
+ @Autowired
+ private FtpSetting ftpSetting;
+
+ /**
+ * ftp server init
+ */
+ @Bean
+ public FtpServer ftpServer() {
+ FtpServerFactory serverFactory = new FtpServerFactory();
+ ListenerFactory listenerFactory = new ListenerFactory();
+ // 1、设置服务端口
+ listenerFactory.setPort(ftpSetting.getPort());
+ // 2、设置被动模式数据上传的接口范围,云服务器需要开放对应区间的端口给客户端
+ DataConnectionConfigurationFactory dataConnectionConfFactory = new DataConnectionConfigurationFactory();
+ dataConnectionConfFactory.setPassivePorts(ftpSetting.getPassivePorts());
+ listenerFactory.setDataConnectionConfiguration(dataConnectionConfFactory.createDataConnectionConfiguration());
+ // 4、替换默认的监听器
+ Listener listener = listenerFactory.createListener();
+ serverFactory.addListener("default", listener);
+ // 5、配置自定义用户事件
+ Map ftpLets = new HashMap<>();
+ ftpLets.put("ftpService", ftplet);
+ serverFactory.setFtplets(ftpLets);
+ // 6、读取用户的配置信息
+ // 6.2、设置用信息
+ serverFactory.setUserManager(userManager);
+ serverFactory.setFileSystem(fileSystemFactory);
+ // 7、实例化FTP Server
+ FtpServer server = serverFactory.createServer();
+ try {
+ server.start();
+ if (!server.isStopped()) {
+ log.info("[FTP服务] 已启动, 端口: {}", ftpSetting.getPort());
+ }
+ } catch (FtpException e) {
+ log.info("[FTP服务] 启动失败 ", e);
+ }
+ return server;
+ }
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpSetting.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpSetting.java
new file mode 100644
index 000000000..91acc8bb0
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/FtpSetting.java
@@ -0,0 +1,21 @@
+package com.genersoft.iot.vmp.conf.ftpServer;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+/**
+ * 配置文件 user-settings 映射的配置信息
+ */
+@Component
+@ConfigurationProperties(prefix = "ftp", ignoreInvalidFields = true)
+@Order(0)
+@Data
+public class FtpSetting {
+
+ private Boolean enable = Boolean.FALSE;
+
+ private int port = 21;
+ private String passivePorts = "10000-10500";
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/Ftplet.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/Ftplet.java
new file mode 100644
index 000000000..b413a427d
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/Ftplet.java
@@ -0,0 +1,59 @@
+package com.genersoft.iot.vmp.conf.ftpServer;
+
+import com.genersoft.iot.vmp.jt1078.event.FtpUploadEvent;
+import org.apache.ftpserver.ftplet.*;
+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.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+
+@Component
+public class Ftplet extends DefaultFtplet {
+
+ private final Logger logger = LoggerFactory.getLogger(Ftplet.class);
+
+ @Autowired
+ private ApplicationEventPublisher applicationEventPublisher;
+
+ @Override
+ public FtpletResult onUploadEnd(FtpSession session, FtpRequest request) throws FtpException, IOException {
+ FtpFile file = session.getFileSystemView().getFile(request.getArgument());
+ if (file == null) {
+ return super.onUploadEnd(session, request);
+ }
+ sendEvent(file.getAbsolutePath());
+ return super.onUploadUniqueEnd(session, request);
+ }
+
+ @Override
+ public FtpletResult onAppendEnd(FtpSession session, FtpRequest request) throws FtpException, IOException {
+ FtpFile file = session.getFileSystemView().getFile(request.getArgument());
+ if (file == null) {
+ return super.onUploadEnd(session, request);
+ }
+ sendEvent(file.getAbsolutePath());
+ return super.onUploadUniqueEnd(session, request);
+ }
+
+ @Override
+ public FtpletResult onUploadUniqueEnd(FtpSession session, FtpRequest request) throws FtpException, IOException {
+ FtpFile file = session.getFileSystemView().getFile(request.getArgument());
+ if (file == null) {
+ return super.onUploadEnd(session, request);
+ }
+ sendEvent(file.getAbsolutePath());
+ return super.onUploadUniqueEnd(session, request);
+ }
+
+ private void sendEvent(String filePath){
+ FtpUploadEvent event = new FtpUploadEvent(this);
+ logger.info("[文件已上传]: {}", filePath);
+ event.setFileName(filePath);
+ applicationEventPublisher.publishEvent(event);
+ }
+}
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/UserManager.java b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/UserManager.java
new file mode 100644
index 000000000..da5c5ed69
--- /dev/null
+++ b/src/main/java/com/genersoft/iot/vmp/conf/ftpServer/UserManager.java
@@ -0,0 +1,86 @@
+package com.genersoft.iot.vmp.conf.ftpServer;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.ftpserver.ftplet.*;
+import org.apache.ftpserver.usermanager.UsernamePasswordAuthentication;
+import org.apache.ftpserver.usermanager.impl.BaseUser;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+@Component
+public class UserManager implements org.apache.ftpserver.ftplet.UserManager {
+
+ private static final String PREFIX = "VMP_FTP_USER_";
+
+ @Autowired
+ private RedisTemplate