上传警告信息改为传视频,已测试保存视频,未与接口联调
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user