上传警告信息改为传视频,已测试保存视频,未与接口联调

This commit is contained in:
zqc
2026-03-05 14:41:49 +08:00
parent 6a1013d138
commit 80bc288a88
4 changed files with 386 additions and 7 deletions

View File

@@ -4,6 +4,9 @@
import cv2
import base64
import json
import os
import subprocess
import time
import threading
import queue
@@ -13,6 +16,7 @@ from concurrent.futures import ThreadPoolExecutor
from common import constants
from utils.logger import get_logger
from utils.hls_utils import get_segments_before_current, parse_segment_info
logger = get_logger(__name__)
@@ -67,6 +71,12 @@ class BaseFrameProcessorWorker(threading.Thread):
max_workers=post_workers,
thread_name_prefix="alert_post"
)
# MP4缓存 {segment_path: mp4_path}
self._mp4_cache: Dict[str, str] = {}
# 启动视频文件清理线程
self._start_cleanup_thread()
def _encode_image_to_base64(self, img) -> str:
"""图像编码为 Base64"""
@@ -102,7 +112,7 @@ class BaseFrameProcessorWorker(threading.Thread):
return result
def _post_alert(self, msg: dict):
"""异步发送告警 POST 请求(在线程池中执行)"""
"""异步发送告警 POST 请求(在线程池中执行)- 旧接口,保留备用"""
try:
response = requests.post(constants.ALERT_PUSH_URL, json=msg, timeout=5.0)
if response.status_code == 200:
@@ -112,6 +122,218 @@ class BaseFrameProcessorWorker(threading.Thread):
except Exception as e:
print(f"[ERROR] POST alert request failed: {e}")
def _post_alert_with_video(self, msg: dict, video_path: str = None):
"""
异步发送告警 POST 请求带视频multipart/form-data
Args:
msg: 消息内容
video_path: 视频文件路径(可选)
"""
try:
if video_path and os.path.exists(video_path):
# 有视频,使用 multipart/form-data 上传
with open(video_path, 'rb') as f:
files = {
'video': f,
'metadata': (None, json.dumps(msg))
}
response = requests.post(constants.ALERT_PUSH_URL, files=files, timeout=10.0)
else:
# 无视频,也使用 multipart/form-data
files = {
'metadata': (None, json.dumps(msg))
}
response = requests.post(constants.ALERT_PUSH_URL, files=files, timeout=5.0)
if response.status_code == 200:
logger.info(f"[INFO] POST alert sent successfully for actions: {msg.get('result_type')}")
else:
logger.warning(f"[WARN] POST alert failed with status: {response.status_code}")
except Exception as e:
logger.error(f"[ERROR] POST alert request failed: {e}")
def _start_cleanup_thread(self):
"""启动视频文件清理线程"""
def cleanup_loop():
while not self.stop_event.is_set():
try:
self._cleanup_expired_files()
except Exception as e:
logger.error(f"[ERROR] Cleanup thread error: {e}")
# 每10分钟检查一次
self.stop_event.wait(600)
thread = threading.Thread(target=cleanup_loop, daemon=True, name="video_cleanup")
thread.start()
logger.info("[INFO] Video cleanup thread started")
def _cleanup_expired_files(self):
"""清理过期的视频文件"""
output_dir = constants.VIDEO_CLIP_OUTPUT_DIR
if not output_dir or not os.path.exists(output_dir):
return
retention_seconds = constants.VIDEO_CLIP_RETENTION_SECONDS
current_time = time.time()
try:
for filename in os.listdir(output_dir):
if not filename.endswith('.mp4'):
continue
filepath = os.path.join(output_dir, filename)
if os.path.isfile(filepath):
file_mtime = os.path.getmtime(filepath)
if current_time - file_mtime > retention_seconds:
try:
os.remove(filepath)
logger.info(f"[INFO] Cleaned up expired video: {filename}")
except Exception as e:
logger.error(f"[ERROR] Failed to delete {filename}: {e}")
except Exception as e:
logger.error(f"[ERROR] Cleanup expired files error: {e}")
def _create_or_get_video_clip(self, segment_path: str, segment_duration: float = None) -> str | None:
"""
创建或获取视频剪辑
Args:
segment_path: 当前TS分片路径
segment_duration: 当前分片时长(秒)
Returns:
MP4文件路径失败返回 None
"""
if not segment_path:
return None
# 检查缓存
if segment_path in self._mp4_cache:
cached_path = self._mp4_cache[segment_path]
if os.path.exists(cached_path):
return cached_path
else:
# 缓存失效,移除
del self._mp4_cache[segment_path]
# 解析分片信息构建MP4路径
camera_id, timestamp, seq = parse_segment_info(segment_path)
if not camera_id:
logger.warning(f"[WARN] Failed to parse segment info: {segment_path}")
return None
output_dir = constants.VIDEO_CLIP_OUTPUT_DIR
if not output_dir:
logger.warning("[WARN] VIDEO_CLIP_OUTPUT_DIR not configured")
return None
# 确保输出目录存在
os.makedirs(output_dir, exist_ok=True)
# MP4文件名
mp4_filename = f"{camera_id}_{timestamp}_{seq}.mp4"
mp4_path = os.path.join(output_dir, mp4_filename)
# 检查是否已存在
if os.path.exists(mp4_path):
self._mp4_cache[segment_path] = mp4_path
return mp4_path
# 计算需要回溯的分片数量
clip_duration = constants.VIDEO_CLIP_DURATION_SECONDS
default_segment_duration = constants.VIDEO_CLIP_DEFAULT_SEGMENT_DURATION
effective_duration = segment_duration if segment_duration else default_segment_duration
if effective_duration <= 0:
effective_duration = default_segment_duration
n_segments = int(clip_duration / effective_duration) + 1
# 获取需要合并的分片
ts_files = get_segments_before_current(segment_path, n_segments)
if not ts_files:
logger.warning(f"[WARN] No segments found for clip: {segment_path}")
return None
# 合并TS为MP4
if self._merge_ts_to_mp4(ts_files, mp4_path):
self._mp4_cache[segment_path] = mp4_path
return mp4_path
return None
def _merge_ts_to_mp4(self, ts_files: list, output_path: str) -> bool:
"""
使用 ffmpeg 合并 TS 分片为 MP4
Args:
ts_files: TS文件路径列表按时间顺序
output_path: 输出MP4路径
Returns:
是否成功
"""
if not ts_files:
return False
try:
# 构建 concat 字符串
concat_str = "|".join(ts_files)
# ffmpeg 命令
cmd = [
'ffmpeg',
'-i', f'concat:{concat_str}',
'-c', 'copy',
'-y', # 覆盖输出文件
output_path
]
# 执行命令
result = subprocess.run(
cmd,
capture_output=True,
timeout=60 # 60秒超时
)
if result.returncode == 0:
logger.info(f"[INFO] Created video clip: {output_path}")
return True
else:
logger.error(f"[ERROR] ffmpeg failed: {result.stderr.decode()}")
return False
except subprocess.TimeoutExpired:
logger.error(f"[ERROR] ffmpeg timeout for {output_path}")
return False
except FileNotFoundError:
logger.error("[ERROR] ffmpeg not found, please install ffmpeg")
return False
except Exception as e:
logger.error(f"[ERROR] Failed to merge TS files: {e}")
return False
def _process_alert_with_video(self, msg: dict, segment_path: str, segment_duration: float):
"""
处理告警(含视频剪辑)- 在线程池中执行
Args:
msg: 基础消息
segment_path: 当前TS分片路径
segment_duration: 当前分片时长
"""
# 尝试创建/获取视频剪辑
mp4_path = None
if segment_path:
mp4_path = self._create_or_get_video_clip(segment_path, segment_duration)
# 展开 result_type
expanded_msgs = self._expand_msg_by_result_type(msg)
# 发送每个展开后的消息
for expanded_msg in expanded_msgs:
self._post_alert_with_video(expanded_msg, mp4_path)
def _create_detector(self, params):
"""创建检测器实例"""
# 使用 type(self) 访问类属性,避免 lambda 被绑定 self 参数
@@ -203,13 +425,24 @@ class BaseFrameProcessorWorker(threading.Thread):
try:
self.ws_queue.put(msg, timeout=1.0)
if push_actions and len(push_actions) > 0:
# 异步发送 POST 请求(提交到线程池)
# 构建消息
post_msg = msg.copy()
post_msg['type'] = self.POST_TYPE
# 展开 result_type 为多个独立的 msg
expanded_msgs = self._expand_msg_by_result_type(post_msg)
for expanded_msg in expanded_msgs:
self.post_executor.submit(self._post_alert, expanded_msg)
#备用backup
#self.post_executor.submit(self._post_alert, post_msg)
# 获取视频相关信息仅HLS模式有
segment_path = item.get("segment_path")
segment_duration = item.get("segment_duration")
# 提交到线程池执行包含视频剪辑和POST
self.post_executor.submit(
self._process_alert_with_video,
post_msg,
segment_path,
segment_duration
)
except queue.Full:
logger.warning("[WARN] ws_send_queue full, drop frame message")