从 0 到 1 构建企业级视频智能分析平台:完整工程实践

一份可直接落地的全栈技术方案 覆盖相机接入、流媒体编排、存储治理、智能分析全链路 包含架构设计、核心代码、数据模型、运维体系与性能基准


目录

第一部分:平台基础与相机接入

  1. 问题定义与架构设计
  2. 技术选型与核心组件
  3. 数据模型设计
  4. ONVIF 协议深度解析与实现
  5. MediaMTX 流媒体网关集成
  6. FFmpeg 编解码治理
  7. 相机接入完整流程
  8. 设备状态巡检与事件闭环

第二部分:录像存储系统

  1. 录像系统架构设计
  2. 存储卷治理与容量管理
  3. 录像策略编排
  4. 文件索引与回放检索

第三部分:智能分析平台

  1. 算法任务模型设计
  2. 算法节点管理与调度
  3. WebSocket 协议规范
  4. 结果处理与告警推送
  5. 预览链路实现

第四部分:运维与演进

  1. 配置管理
  2. 可观测性体系
  3. 典型问题与解决方案
  4. FAQ 与最佳实践
  5. 性能基准与压测
  6. 架构演进路线

第四部分:运维与演进

18. 配置管理

18.1 应用配置模板

application.yml
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/video_analysis?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: ${DB_PASSWORD}
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 30000
# 相机配置
camera:
onboarding:
async-enabled: true
sync-timeout-ms: 15000
duplicate-check:
by-ip-port: true
onvif:
default-port: 80
connect-timeout-ms: 3000
read-timeout-ms: 5000
heartbeat-timeout-ms: 3000
soap:
strict-fault-check: true
allow-capability-fallback: true
stream:
naming: "cam_{ip}_ch{channel}_{subtype}"
subtype-rule:
main-min-width: 1280
retry:
register-max-attempts: 3
register-backoff-ms: 1000
health:
check-interval-ms: 30000
initial-delay-ms: 10000
offline-alarm-threshold-sec: 180
# 编解码配置
codec:
probe:
ffprobe-bin: ffprobe
timeout-ms: 3000
max-concurrency: 8
transcode:
enabled: true
assume-h265-when-unknown: true
ffmpeg-bin: ffmpeg
rtsp-transport: tcp
stimeout-us: 5000000
probesize: 5000000
analyzeduration: 5000000
x264:
preset: veryfast
crf: 23
gop: 50
enable-audio: false
# MediaMTX 配置
media:
mtx:
api-base-url: http://127.0.0.1:9997
rtsp-port: 8554
webrtc-port: 8889
api:
path-add: /v3/config/paths/add/
path-get: /v3/config/paths/get/
path-list: /v3/config/paths/list
path-delete: /v3/config/paths/delete/
# 录像配置
recording:
default:
retention-days: 7
segment-duration: 10m
record-format: fmp4
index:
fixed-delay-ms: 30000
scan-depth: 4
lookback-hours: 24
storage:
refresh-interval-ms: 15000
auto-switch-enabled: true
migrate-batch-size: 20
# 算法任务配置
ai:
task:
keepalive-timeout-seconds: 30
scene-keepalive-timeout-seconds: 30
dedup-running-seconds: 10
result:
queue:
size: 20000
worker:
min: 4
max: 16
sample-interval-ms: 1000
batch:
enabled: false
size: 200
flush-ms: 500
preview:
auto-start: true
default-fps: 15
default-width: 1280
default-height: 720
default-bitrate: 3000k
default-encoder: h264_nvenc
push:
queue-size: 20000
flush-ms: 1000
routes-file: push_routes.json
level-policy-file: level_policy.json
# WebSocket 配置
ws:
ai-engine:
endpoint: /ws/ai-engine
web-notify:
endpoint: /ws/web-notify

18.2 MediaMTX 配置模板

mediamtx.yml
###############################################
# Global settings
# API
api: yes
apiAddress: :9997
# Metrics
metrics: yes
metricsAddress: :9998
###############################################
# RTSP server
rtsp: yes
rtspAddress: :8554
protocols: [tcp]
encryption: "no"
rtspAddress: :8554
###############################################
# WebRTC server
webrtc: yes
webrtcAddress: :8889
webrtcICEServers:
- urls: [stun:stun.l.google.com:19302]
###############################################
# HLS server
hls: yes
hlsAddress: :8888
hlsAlwaysRemux: no
hlsVariant: lowLatency
hlsSegmentCount: 7
hlsSegmentDuration: 1s
hlsPartDuration: 200ms
###############################################
# Recording
record: no
recordPath: /recordings/%path/%Y-%m-%d/%H-%M-%S
recordFormat: fmp4
recordPartDuration: 1s
recordSegmentDuration: 10m
recordDeleteAfter: 7d
###############################################
# Path defaults
paths:
all:
sourceOnDemand: yes
sourceOnDemandStartTimeout: 10s
sourceOnDemandCloseAfter: 10s
runOnDemandStartTimeout: 10s
runOnDemandCloseAfter: 10s

18.3 Docker Compose 部署模板

version: "3.8"
services:
mysql:
image: mysql:8.0
container_name: video-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: video_analysis
TZ: Asia/Shanghai
ports:
- "3306:3306"
volumes:
- ./data/mysql:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
command: --default-authentication-plugin=mysql_native_password
mediamtx:
image: bluenviron/mediamtx:latest
container_name: video-mediamtx
restart: always
ports:
- "8554:8554" # RTSP
- "8888:8888" # HLS
- "8889:8889" # WebRTC
- "9997:9997" # API
- "9998:9998" # Metrics
volumes:
- ./mediamtx.yml:/mediamtx.yml
- ./data/recordings:/recordings
environment:
TZ: Asia/Shanghai
app:
image: video-analysis-platform:latest
container_name: video-app
restart: always
depends_on:
- mysql
- mediamtx
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/video_analysis?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MEDIA_MTX_API_BASE_URL: http://mediamtx:9997
TZ: Asia/Shanghai
volumes:
- ./data/snapshots:/app/snapshots
- ./logs:/app/logs
nginx:
image: nginx:alpine
container_name: video-nginx
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./data/recordings:/usr/share/nginx/html/recordings
- ./data/snapshots:/usr/share/nginx/html/snapshots
depends_on:
- app

19. 可观测性体系

19.1 核心指标定义

相机接入层:

camera_onboarding_total # 接入总数
camera_onboarding_success_total # 接入成功数
camera_onboarding_duration_seconds # 接入耗时
camera_online_count # 在线设备数
camera_offline_count # 离线设备数
camera_offline_event_duration_seconds # 离线时长

流媒体层:

stream_path_total # 路径总数
stream_path_register_success_rate # 注册成功率
stream_transcode_count # 转码路数
media_gateway_request_duration_seconds # API 调用耗时

录像层:

recording_plan_active_count # 活跃计划数
recording_file_index_lag_seconds # 索引延迟
storage_volume_usage_percent # 存储使用率
storage_volume_full_count # 满盘卷数

算法层:

ai_task_running_count # 运行中任务数
ai_task_error_count # 异常任务数
ai_engine_online_nodes # 在线节点数
ai_result_queue_depth # 结果队列深度
ai_result_drop_total # 丢弃结果数
ai_alarm_push_success_rate # 推送成功率

19.2 Prometheus 指标暴露

@Component
public class MetricsCollector {
private final Counter cameraOnboardingTotal;
private final Counter cameraOnboardingSuccess;
private final Histogram cameraOnboardingDuration;
private final Gauge cameraOnlineCount;
private final Gauge cameraOfflineCount;
public MetricsCollector(MeterRegistry registry) {
this.cameraOnboardingTotal = Counter.builder("camera_onboarding_total")
.description("Total camera onboarding attempts")
.register(registry);
this.cameraOnboardingSuccess = Counter.builder("camera_onboarding_success_total")
.description("Successful camera onboardings")
.register(registry);
this.cameraOnboardingDuration = Histogram.builder("camera_onboarding_duration_seconds")
.description("Camera onboarding duration")
.register(registry);
this.cameraOnlineCount = Gauge.builder("camera_online_count", this::getOnlineCount)
.description("Online camera count")
.register(registry);
this.cameraOfflineCount = Gauge.builder("camera_offline_count", this::getOfflineCount)
.description("Offline camera count")
.register(registry);
}
public void recordOnboardingAttempt() {
cameraOnboardingTotal.increment();
}
public void recordOnboardingSuccess(long durationMs) {
cameraOnboardingSuccess.increment();
cameraOnboardingDuration.record(durationMs / 1000.0);
}
private double getOnlineCount() {
return cameraRepository.countByStatus(1);
}
private double getOfflineCount() {
return cameraRepository.countByStatus(0);
}
}

19.3 告警规则模板

prometheus-alerts.yml
groups:
- name: camera_alerts
interval: 30s
rules:
- alert: CameraOnboardingFailureRateHigh
expr: |
(
rate(camera_onboarding_total[5m]) -
rate(camera_onboarding_success_total[5m])
) / rate(camera_onboarding_total[5m]) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "相机接入失败率过高"
description: "过去 5 分钟接入失败率超过 5%"
- alert: CameraOfflineCountHigh
expr: camera_offline_count > 10
for: 3m
labels:
severity: critical
annotations:
summary: "离线设备数量过多"
description: "当前离线设备数: {{ $value }}"
- name: storage_alerts
interval: 30s
rules:
- alert: StorageVolumeAlmostFull
expr: storage_volume_usage_percent > 90
for: 3m
labels:
severity: warning
annotations:
summary: "存储卷即将满盘"
description: "卷 {{ $labels.volume_id }} 使用率: {{ $value }}%"
- alert: RecordingIndexLagHigh
expr: recording_file_index_lag_seconds > 120
for: 5m
labels:
severity: warning
annotations:
summary: "录像索引延迟过高"
description: "索引延迟: {{ $value }} 秒"
- name: ai_alerts
interval: 30s
rules:
- alert: AiResultQueueFull
expr: ai_result_queue_depth > 18000
for: 2m
labels:
severity: critical
annotations:
summary: "结果队列即将满"
description: "队列深度: {{ $value }}"
- alert: AlarmPushFailureRateHigh
expr: ai_alarm_push_success_rate < 0.95
for: 5m
labels:
severity: warning
annotations:
summary: "告警推送成功率过低"
description: "成功率: {{ $value }}"

19.4 日志规范

日志级别使用:

  • ERROR: 系统异常,需要人工介入
  • WARN: 降级或重试成功的场景
  • INFO: 关键业务事件 (接入成功、任务启动等)
  • DEBUG: 详细调试信息

日志格式:

[时间] [级别] [线程] [类名] - [业务标识] 消息内容

示例:

2026-02-13 14:30:00.123 INFO [main] c.e.CameraOnboardingService - [camera:1001] Device onboarded successfully: ip=10.0.1.20, manufacturer=Hikvision
2026-02-13 14:30:05.456 WARN [task-1] c.e.MediaPathService - [path:cam_10_0_1_20_ch1_main] MediaMTX registration failed, retrying (1/3)
2026-02-13 14:30:10.789 ERROR [task-2] c.e.OnvifManager - [camera:1002] ONVIF connection timeout: ip=10.0.1.21, timeout=3000ms

20. 典型问题与解决方案

20.1 设备接入层

问题 1: ONVIF 连接超时

现象:

OnvifException: Connection timeout after 3000ms

排查步骤:

  1. ping 设备 IP 确认网络连通性
  2. telnet ip 80 确认端口开放
  3. 浏览器访问 http://ip/onvif/device_service 验证服务
  4. 检查防火墙规则

解决方案:

  • 增加超时时间至 5-10 秒
  • 配置网络路由
  • 使用设备 Web 界面开启 ONVIF 服务

问题 2: WS-Security 认证失败

现象:

SOAP Fault: The security token could not be authenticated

原因: 服务器与设备时间误差 > 5 秒

解决方案:

Terminal window
# 服务器端同步 NTP
ntpdate ntp.aliyun.com
# 设备端配置 NTP
设备 Web 界面 -> 系统配置 -> 时间设置 -> NTP 服务器: ntp.aliyun.com

问题 3: 在线但无画面

现象: camera_status=1, 但 RTSP 拉流失败

原因: 控制面可达但媒体面不可达

解决方案:

// 增加媒体可用性检查
public boolean checkMediaAvailable(String rtspUrl) {
try {
ProcessBuilder pb = new ProcessBuilder(
"ffmpeg", "-i", rtspUrl,
"-t", "1", "-f", "null", "-"
);
Process p = pb.start();
return p.waitFor(5, TimeUnit.SECONDS) && p.exitValue() == 0;
} catch (Exception e) {
return false;
}
}

20.2 录像层

问题 4: 计划显示已开启但无文件

排查步骤:

  1. 检查 recording_plan.status 是否为 1
  2. 检查 MediaMTX 路径配置: GET /v3/config/paths/get/{pathName}
  3. 检查存储卷状态: storage_volume.statusis_full
  4. 检查源 RTSP 可拉流性

解决方案:

// 增加健康检查
@Scheduled(fixedDelay = 60000)
public void checkRecordingHealth() {
List<RecordingPlan> plans = planRepository.findByStatus(1);
for (RecordingPlan plan : plans) {
// 检查最近 5 分钟是否有新文件
long recentFileCount = fileRepository.countByStreamNameAndTimeSince(
plan.getStreamName(),
LocalDateTime.now().minusMinutes(5)
);
if (recentFileCount == 0) {
log.warn("No recent files for plan {}, reapplying config", plan.getId());
planService.applyPlan(plan.toRequest());
}
}
}

问题 5: 回放查询有空洞

现象: 物理文件存在,数据库查不到

原因: 索引任务只扫描最近窗口,漏扫历史补写文件

解决方案:

// 增加全量补扫任务
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点
public void fullScan() {
List<StorageVolume> volumes = volumeRepository.findByStatus(1);
for (StorageVolume volume : volumes) {
scanVolumeFullRange(volume);
}
}

20.3 算法层

问题 6: 任务显示运行中,但算法端无负载

排查步骤:

  1. 检查 ai_task_runtime.state 是否真的为 RUNNING
  2. 检查节点 WebSocket 会话是否在线
  3. 检查算法端日志是否收到 START_TASK 命令
  4. 检查 TASK_KEEPALIVE 心跳上报

解决方案:

// 增加守护任务
@Scheduled(fixedDelay = 10000)
public void watchdogTask() {
List<TaskRuntime> runtimes = runtimeRepository.findByState("RUNNING");
for (TaskRuntime runtime : runtimes) {
long elapsed = Duration.between(
runtime.getLastKeepalive(),
LocalDateTime.now()
).getSeconds();
if (elapsed > 30) {
log.warn("Task {} keepalive timeout, marking ERROR", runtime.getTaskId());
runtime.setState("ERROR");
runtimeRepository.updateById(runtime);
// 通知节点断连处理
onNodeDisconnected(runtime.getNodeId());
}
}
}

问题 7: 结果队列堆积

现象: ai_result_queue_depth 持续增长,内存告警

原因: 处理速度跟不上生产速度

解决方案:

// 1. 启用非告警抽样
@Value("${ai.result.sample-interval-ms:1000}")
private long sampleIntervalMs;
// 2. 启用批量入库
@Value("${ai.result.batch.enabled:false}")
private boolean batchEnabled;
@Value("${ai.result.batch.size:200}")
private int batchSize;
// 3. 增加处理线程
int coreThreads = Runtime.getRuntime().availableProcessors();
this.processExecutor = new ThreadPoolExecutor(
coreThreads * 2, // 增加并发
coreThreads * 4,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)
);

21. FAQ 与最佳实践

21.1 相机接入

Q: 是否必须支持 ONVIF?

A: 不是必须,但强烈建议。ONVIF 提供标准化的设备管理和流获取方式。不支持 ONVIF 的设备可以通过厂商兜底模式接入,但需要手动维护 RTSP 模板。

Q: 如何处理厂商差异?

A: 分层处理:

  1. 协议层做最大兼容 (Fault 解析、能力降级)
  2. 业务层提供厂商兜底开关
  3. 配置层维护厂商 RTSP 模板库

Q: 主码流和子码流如何选择?

A: 建议策略:

  • 录像: 主码流
  • 实时预览: 子码流
  • 算法分析: 根据精度要求选择

21.2 流媒体

Q: 为什么推荐 MediaMTX 而不是 ZLMediaKit?

A: 两者都是优秀的开源方案,选择 MediaMTX 的原因:

  1. 单一二进制,部署简单
  2. RESTful API 设计现代化
  3. WebRTC 支持原生
  4. 社区活跃,文档完善

Q: H.265 一定要转码吗?

A: 不是。建议策略:

  • 浏览器实时预览: 转 H.264
  • 录像存储: 保留 H.265 (节省空间)
  • 算法分析: 视算法输入要求而定

Q: 转码会增加多少延迟?

A: 软件转码: 1-3 秒 硬件转码 (NVENC): 500ms-1 秒

21.3 录像

Q: 切片时长怎么选?

A: 综合考虑:

  • 1-2 分钟: 文件数量大,IO 压力高
  • 5-10 分钟: 推荐,综合最优
  • 30-60 分钟: 文件少,但回放拖拽粗糙

Q: FMP4 vs TS 格式?

A: FMP4 优势:

  • 现代浏览器原生支持
  • 元数据结构清晰
  • 后期处理友好

TS 优势:

  • 兼容性更广 (老设备/老浏览器)
  • 容错性强 (部分损坏不影响播放)

Q: 如何避免磁盘满导致录像中断?

A: 多层防护:

  1. 水位线机制自动切卷
  2. 定时清理过期文件
  3. 磁盘使用率告警
  4. 降级策略 (关闭非关键路径录像)

21.4 算法

Q: 一个任务可以配置多少个场景?

A: 理论无上限,实际建议:

  • 单节点模式: 3-5 个
  • 拆分模式: 5-10 个

过多场景会导致:

  • 推理开销增加
  • 结果处理复杂
  • 告警频率过高

Q: 任务失败如何重试?

A: 分级重试:

  1. 瞬时失败 (网络抖动): 自动重启,计数器 +1
  2. 持续失败 (设备离线): 标记 ERROR,等待设备恢复
  3. 配置错误: 不重试,需要人工修复

Q: 如何控制告警推送频率?

A: 使用推送模式:

  • 高优先级: IMMEDIATE
  • 中优先级: COUNT=5, WINDOW=600s
  • 低优先级: WINDOW=3600s

并配合冷却期避免风暴。


22. 性能基准与压测

22.1 接入性能基准

测试环境:

  • CPU: 8C16G
  • 网络: 千兆局域网
  • 设备: 海康威视 DS-2CD2142FWD-I

测试结果:

并发数接入成功率P50 耗时P95 耗时P99 耗时
10100%850ms1200ms1500ms
5099.2%1100ms1800ms2400ms
10097.5%1450ms2500ms3200ms
20094.8%2100ms3800ms4800ms

瓶颈分析:

  • 网络 I/O 是主要瓶颈
  • ONVIF SOAP 解析占用 15-20% CPU
  • FFprobe 探测占用 30-40% CPU

优化建议:

  • 接入任务异步化
  • FFprobe 并发限制 (信号量)
  • 设备信息缓存

22.2 录像性能基准

测试环境:

  • CPU: 16C32G
  • 存储: NVMe SSD
  • 路数: 500 路并发录像

测试结果:

码率切片时长磁盘写入CPU 使用内存使用
2Mbps10m125MB/s18%4.2GB
4Mbps10m250MB/s22%5.8GB
6Mbps10m375MB/s28%7.5GB

索引性能:

  • 扫描速度: 10000 文件/秒
  • 索引延迟: P95 < 30 秒

22.3 算法性能基准

测试环境:

  • GPU: NVIDIA RTX 3080
  • 算法: YOLOv8m
  • 输入: 1280x720 @ 15fps

单节点容量:

场景数/任务并发任务数GPU 使用推理延迟
15068%45ms
23572%52ms
32578%61ms

结果处理:

  • 队列吞吐: 20000 条/秒
  • 入库延迟: P95 < 200ms
  • 告警推送: P95 < 500ms

23. 架构演进路线

23.1 当前架构 (v1.0)

特点:

  • 单体应用
  • MySQL 单实例
  • 本地文件存储
  • 单节点 MediaMTX

适用场景:

  • 设备数 < 1000
  • 并发流 < 200
  • 算法任务 < 100

23.2 中期演进 (v2.0)

目标: 支撑 5000 设备、1000 并发流

改进点:

  1. 服务拆分
单体应用 → 接入服务 + 录像服务 + 算法服务
  1. 存储优化
本地存储 → 对象存储 (MinIO/OSS)
分层存储: 热数据 SSD + 冷数据 HDD
  1. MediaMTX 集群
单节点 → 多节点 + LVS 负载均衡
路径哈希分片
  1. 数据库读写分离
主库: 写入
从库: 查询 + 统计

23.3 长期演进 (v3.0)

目标: 支撑 10万+ 设备、万级并发流

改进点:

  1. 微服务化
按业务域拆分:
- 设备管理服务
- 流媒体服务
- 录像服务
- 算法服务
- 告警服务
服务间通信: gRPC + MQ
  1. 分布式存储
录像: Ceph 集群
元数据: TiDB
缓存: Redis 集群
  1. 边缘计算
中心节点 + 边缘节点
边缘: 实时分析 + 本地录像
中心: 长期存储 + 全局调度
  1. 云原生改造
容器化: Kubernetes
服务网格: Istio
可观测: Prometheus + Grafana + Jaeger

总结

本文从零开始,构建了一个完整的企业级视频智能分析平台,覆盖:

  1. 相机接入: ONVIF 协议深度实践、流路径治理、编码决策
  2. 录像存储: 存储卷管理、文件索引、回放检索
  3. 智能分析: 多场景任务模型、节点调度、结果处理、告警推送
  4. 运维体系: 配置管理、可观测性、问题排查、性能优化

核心设计原则:

  • 分层解耦: 控制面/媒体面、策略/执行、实时/批量
  • 统一抽象: 路径模型、场景模型、状态模型
  • 容错优先: 异步化、重试、降级、回滚
  • 可观测: 日志、指标、链路、告警

技术栈总结:

层次技术选型
协议ONVIF (SOAP/WS-Security)
流媒体MediaMTX
编解码FFmpeg/FFprobe
存储MySQL + 文件系统
通信WebSocket + RESTful
监控Prometheus + Grafana