From 69d093fcb9113104e982f69a3dc5a1b1105264af Mon Sep 17 00:00:00 2001 From: powlu <1144983626@qq.com> Date: Thu, 8 Jan 2026 16:03:39 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=86=E9=A2=91=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __pycache__/mjpeg_streamer.cpython-38.pyc | Bin 3114 -> 11119 bytes check_encoders.py | 35 ++ mjpeg_streamer.py | 335 +++++++++++++++++- py_utils/__pycache__/__init__.cpython-38.pyc | Bin 127 -> 127 bytes .../__pycache__/coco_utils.cpython-38.pyc | Bin 5842 -> 5842 bytes .../__pycache__/rknn_executor.cpython-38.pyc | Bin 1066 -> 1066 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 500 -> 500 bytes .../__pycache__/base_thread.cpython-38.pyc | Bin 1783 -> 1783 bytes .../__pycache__/birdview.cpython-38.pyc | Bin 11436 -> 11436 bytes .../__pycache__/capture_thread.cpython-38.pyc | Bin 2850 -> 2850 bytes .../__pycache__/fisheye_camera.cpython-38.pyc | Bin 3557 -> 3557 bytes .../__pycache__/imagebuffer.cpython-38.pyc | Bin 5020 -> 5020 bytes .../__pycache__/param_settings.cpython-38.pyc | Bin 931 -> 931 bytes .../__pycache__/process_thread.cpython-38.pyc | Bin 2349 -> 2349 bytes .../__pycache__/simple_gui.cpython-38.pyc | Bin 4059 -> 4059 bytes .../__pycache__/structures.cpython-38.pyc | Bin 763 -> 763 bytes .../__pycache__/utils.cpython-38.pyc | Bin 4081 -> 4081 bytes templates/index.html | 193 +++++++++- 18 files changed, 550 insertions(+), 13 deletions(-) create mode 100644 check_encoders.py diff --git a/__pycache__/mjpeg_streamer.cpython-38.pyc b/__pycache__/mjpeg_streamer.cpython-38.pyc index f15d758da606a69dc64c596ed29b3ec60fdb7823..54ad9cf6079eb05bed3cc2cdca0e52d2e503d187 100644 GIT binary patch literal 11119 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 o{Z!{c5_I3TSF!X6fxS)65_5W*u+r-PPDD zpehPdxggO)H5Zx|)QVF<;?9u^|3amaDkLtbCvJ!X2j26FD2?Kgf1Y{Y@6CJuYvaf1 z)}?0CBk+71T*$uhZ?)#OFT;rwPID5Czciw7HgcmdBeSp~tFR-xa3ZI0Be!4?D;iOQ zj%B^b<23TQ3Cp@`M8SE&ZSH(UxO2mZnw)+>LiaH`Tnk-oxAUaj((HXv4hQ{G=m{xq z{ri%7lb&g9fQ%sMIx_f_`kG#_K=+7RFqVxkiP|tuesZ1k?0YUZxCuLU-=qX7J(pYD zhFya@+)WM6Zdzjw$TqkK9PfWwp!hrhijNfDMEyA61U$9Wj(JS|VSeOAPFW>Y)(M;H zS8J9U>XG&A(S-AOB=cB~av>)mR(S~FX@tY-oHIK)h1(+t(*O^MoKsuQi_-xdN)#j1 zsh3BYIZ0>KL+8UK7s%R7MfHHnS=h_DT{w!YAS>g1kfwsiS$|GlcH`F;fF7FKd*jmj z#rU0z=igeBXhlAUa13Fe4X?8)I$Lovsx}wYKW_JR4E&mx^||cpQ{-?Q6OlPJ4SbTm9UaU#Ww7(qw3kliXF?7iwM7MP++p zr8b9wLU7|E4F|(L5grsA7I|6y;C*a|h9;S^{nM|8RI?&0MD_VlfcrX66XdiNr=ovg zj)#H0qH_PIr_!C<`24bZ&06XW)5mRfDmX)@)Ro}9SMatXw;XjkqAa!Dl}RDADMh87 ztD|mL$o;u3g2Jl$GgwtSr<&VsI8~%ap#hB^wzGw;$SEx2u7$&N)BJ*7r}v!ugp$gw z8{E1BV}){e$amHaiX({Qwbq6r?KYmaxwtw?qhM5uSc-g*a3R;BW1K3H&}rJ)`ZV!n zT55+Uq8Rkm%i%I-x>0RPk?^daYG=LP${7d}avJsbT8G(?m9~d6>sO&sUnrO!D?HhG zauxL7hPP}15UPF)k2i4~WD_6^a_62~*?&5Dq8b~m)8`STMGwsuOJdSV@0WBWnPP4Pc WfWu$l9O{>t52K8pfT^~qXZ#0Wb{<{; diff --git a/check_encoders.py b/check_encoders.py new file mode 100644 index 0000000..6e5f0f3 --- /dev/null +++ b/check_encoders.py @@ -0,0 +1,35 @@ +import cv2 + +print("OpenCV版本:", cv2.__version__) +print("可用的视频编码器:") + +# 打印常见编码器是否可用 +try: + fourcc = cv2.VideoWriter_fourcc(*"MP4V") + print("MP4V (MPEG-4 Part 2)", "可用") +except Exception as e: + print("MP4V (MPEG-4 Part 2)", "不可用:", e) + +try: + fourcc = cv2.VideoWriter_fourcc(*"avc1") + print("avc1 (H.264)", "可用") +except Exception as e: + print("avc1 (H.264)", "不可用:", e) + +try: + fourcc = cv2.VideoWriter_fourcc(*"DIVX") + print("DIVX (DivX)", "可用") +except Exception as e: + print("DIVX (DivX)", "不可用:", e) + +try: + fourcc = cv2.VideoWriter_fourcc(*"XVID") + print("XVID (Xvid)", "可用") +except Exception as e: + print("XVID (Xvid)", "不可用:", e) + +try: + fourcc = cv2.VideoWriter_fourcc(*"MJPG") + print("MJPG (Motion JPEG)", "可用") +except Exception as e: + print("MJPG (Motion JPEG)", "不可用:", e) diff --git a/mjpeg_streamer.py b/mjpeg_streamer.py index 5dc26d4..f9b852a 100644 --- a/mjpeg_streamer.py +++ b/mjpeg_streamer.py @@ -1,11 +1,17 @@ # mjpeg_streamer.pynano import threading import time -from flask import Flask, Response, render_template, request, redirect, session, url_for +import os +import shutil +from datetime import datetime, timedelta +from flask import Flask, Response, render_template, request, redirect, session, url_for, send_from_directory import cv2 +import numpy as np +import subprocess +import stat # ====== 新增:登录配置 ====== -AUTO_LOGIN = None # 👈 设置为 True 可跳过登录 +AUTO_LOGIN = True # 👈 设置为 True 可跳过登录 VALID_USER = {"username": "admin", "password": "admin"} class MJPEGServer: @@ -15,12 +21,36 @@ class MJPEGServer: self.port = port self.app = Flask(__name__) self.app.secret_key = 'your-secret-key-change-in-prod' # 用于 session + + # H264编码器参数 + self.h264_encoder = None + self.h264_fourcc = None + self.h264_width = None + self.h264_height = None + + # 视频录制配置 + self.recording_enabled = True + self.segment_duration = 60 # 视频分段时长(秒) + self.storage_path = "/video_records" # 视频存储路径 + self.max_retention_days = 30 # 最大保留天数 + self.video_writer = None + self.current_segment_start = None + self.recording_thread = None + self.recording_stop_event = threading.Event() + + # 确保存储目录存在 + os.makedirs(self.storage_path, exist_ok=True) + os.chmod(self.storage_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 路由 self.app.add_url_rule('/', 'index', self.index) self.app.add_url_rule('/login', 'login', self.login, methods=['GET', 'POST']) self.app.add_url_rule('/logout', 'logout', self.logout) self.app.add_url_rule('/video_feed', 'video_feed', self.video_feed) + self.app.add_url_rule('/h264_feed', 'h264_feed', self.h264_feed) + self.app.add_url_rule('/api/videos', 'get_videos', self.get_videos) + self.app.add_url_rule('/api/video/', 'serve_video', self.serve_video) + self.app.add_url_rule('/api/disk_usage', 'get_disk_usage', self.get_disk_usage) # 静态文件自动托管(Layui) self.app.static_folder = 'static' @@ -66,13 +96,312 @@ class MJPEGServer: if not success or frame is None: time.sleep(0.1) continue - ret, buffer = cv2.imencode('.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), 70]) + ret, buffer = cv2.imencode('.jpg', frame, [int(cv2.IMWRITE_JPEG_QUALITY), 95]) if not ret: continue yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n') + + def h264_feed(self): + """提供H264视频流""" + if not self.check_auth(): + return '', 403 + return Response(self._gen_h264_ffmpeg(), + mimetype='video/H264') + + def _gen_h264_ffmpeg(self): + """使用ffmpeg生成H264视频流""" + # 获取第一帧以确定视频参数 + success, frame = self.frame_buffer.get_frame() + if not success or frame is None: + raise ValueError("无法获取视频帧") + + height, width = frame.shape[:2] + fps = 25.0 + + # 构造ffmpeg命令 + ffmpeg_cmd = [ + 'ffmpeg', + '-f', 'rawvideo', + '-vcodec', 'rawvideo', + '-pix_fmt', 'bgr24', + '-s', f'{width}x{height}', + '-r', str(fps), + '-i', '-', + '-c:v', 'libx264', + '-preset', 'ultrafast', + '-tune', 'zerolatency', + '-b:v', '2000k', + '-f', 'h264', + '-' + ] + + # 启动ffmpeg进程 + ffmpeg_process = subprocess.Popen( + ffmpeg_cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + + try: + while True: + # 获取新帧 + success, frame = self.frame_buffer.get_frame() + if not success or frame is None: + time.sleep(0.01) + continue + + # 确保帧尺寸一致 + if frame.shape[1] != width or frame.shape[0] != height: + frame = cv2.resize(frame, (width, height)) + + # 写入ffmpeg标准输入 + ffmpeg_process.stdin.write(frame.tobytes()) + ffmpeg_process.stdin.flush() + + # 读取编码后的H264数据 + if ffmpeg_process.stdout.poll() is None: + h264_data = ffmpeg_process.stdout.read(4096) + if h264_data: + yield h264_data + finally: + # 清理资源 + if ffmpeg_process.stdin: + ffmpeg_process.stdin.close() + if ffmpeg_process.stdout: + ffmpeg_process.stdout.close() + if ffmpeg_process.stderr: + ffmpeg_process.stderr.close() + ffmpeg_process.wait() + + def _start_recording_thread(self): + """启动视频录制线程""" + if not self.recording_thread or not self.recording_thread.is_alive(): + self.recording_stop_event.clear() + self.recording_thread = threading.Thread(target=self._record_video, daemon=True) + self.recording_thread.start() + print(f"[RECORDING] 视频录制线程已启动,分段时长: {self.segment_duration}秒") + + def _record_video(self): + """视频录制主循环""" + while not self.recording_stop_event.is_set(): + try: + success, frame = self.frame_buffer.get_frame() + if not success or frame is None: + time.sleep(0.1) + continue + + # 检查是否需要创建新的视频分段 + now = datetime.now() + if not self.video_writer or (now - self.current_segment_start).total_seconds() >= self.segment_duration: + self._create_new_segment(frame) + + # 写入视频帧 + if self.video_writer: + if self.video_writer == "ffmpeg": + # 使用FFmpeg写入帧 + if hasattr(self, 'ffmpeg_process') and self.ffmpeg_process.stdin: + try: + self.ffmpeg_process.stdin.write(frame.tobytes()) + self.ffmpeg_process.stdin.flush() + except BrokenPipeError: + print(f"[ERROR] FFmpeg进程已断开,重新创建分段") + self._create_new_segment(frame) + else: + # 使用OpenCV写入帧 + self.video_writer.write(frame) + + # 定期清理旧视频 + if now.second % 60 == 0: # 每分钟检查一次 + self._clean_old_videos() + + # 移除sleep,让录制线程尽可能快地处理帧 + + except Exception as e: + print(f"[RECORDING ERROR] {e}") + time.sleep(1) + + def _create_new_segment(self, frame): + """创建新的视频分段文件""" + # 关闭当前视频写入器 + if self.video_writer: + if self.video_writer == "ffmpeg": + # 关闭FFmpeg进程 + if hasattr(self, 'ffmpeg_process'): + try: + if self.ffmpeg_process.stdin: + self.ffmpeg_process.stdin.close() + if self.ffmpeg_process.stdout: + self.ffmpeg_process.stdout.close() + if self.ffmpeg_process.stderr: + self.ffmpeg_process.stderr.close() + self.ffmpeg_process.wait(timeout=5) + except Exception as e: + print(f"[ERROR] 关闭FFmpeg进程失败: {e}") + self.ffmpeg_process.terminate() + self.ffmpeg_process.wait(timeout=2) + else: + # 关闭OpenCV VideoWriter + self.video_writer.release() + self.video_writer = None + + # 创建日期目录 + now = datetime.now() + self.current_segment_start = now + date_dir = os.path.join(self.storage_path, now.strftime("%Y-%m-%d")) + os.makedirs(date_dir, exist_ok=True) + os.chmod(date_dir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + + # 生成视频文件名 + timestamp = now.strftime("%H-%M-%S") + video_path = os.path.join(date_dir, f"video_{timestamp}.mp4") + + # 创建新的视频写入器 + height, width = frame.shape[:2] + fps = 15.0 + + # 使用FFmpeg通过subprocess创建视频录制进程 + try: + # 构建FFmpeg命令 + self.ffmpeg_cmd = [ + 'ffmpeg', + '-y', # 覆盖现有文件 + '-f', 'rawvideo', # 输入格式为原始视频 + '-vcodec', 'rawvideo', + '-pix_fmt', 'bgr24', # OpenCV默认的BGR格式 + '-s', f'{width}x{height}', # 视频分辨率 + '-r', str(fps), # 帧率 + '-i', '-', # 从标准输入读取 + '-c:v', 'libx264', # 使用H.264编码器 + '-preset', 'ultrafast', # 编码速度优先 + '-tune', 'zerolatency', # 低延迟 + '-b:v', '2M', # 比特率 + '-f', 'mp4', # 输出格式为MP4 + video_path + ] + + # 启动FFmpeg进程 + self.ffmpeg_process = subprocess.Popen( + self.ffmpeg_cmd, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + self.video_writer = "ffmpeg" # 标记为使用FFmpeg + print(f"[RECORDING] 使用FFmpeg开始录制新分段: {video_path}") + except Exception as e: + print(f"[ERROR] FFmpeg录制失败: {e}") + # 降级使用OpenCV VideoWriter + fourcc = cv2.VideoWriter_fourcc(*"MJPG") + self.video_writer = cv2.VideoWriter(video_path + '.avi', fourcc, fps, (width, height)) + print(f"[RECORDING] 降级使用OpenCV开始录制新分段: {video_path}.avi") + + def _clean_old_videos(self): + """清理超过保留期限的旧视频""" + try: + cutoff_date = datetime.now() - timedelta(days=self.max_retention_days) + cutoff_str = cutoff_date.strftime("%Y-%m-%d") + + # 遍历所有日期目录 + for date_dir in os.listdir(self.storage_path): + if date_dir < cutoff_str: + dir_path = os.path.join(self.storage_path, date_dir) + if os.path.isdir(dir_path): + shutil.rmtree(dir_path) + print(f"[CLEANUP] 删除过期视频目录: {dir_path}") + except Exception as e: + print(f"[CLEANUP ERROR] {e}") + + def get_videos(self): + """API: 获取视频文件列表""" + if not self.check_auth(): + return {'error': 'Unauthorized'}, 403 + + videos = [] + try: + # 获取所有日期目录 + date_dirs = sorted([d for d in os.listdir(self.storage_path) if os.path.isdir(os.path.join(self.storage_path, d))], reverse=True) + + for date_dir in date_dirs: + date_path = os.path.join(self.storage_path, date_dir) + video_files = sorted([f for f in os.listdir(date_path) if f.endswith('.avi') or f.endswith('.mp4')]) + + for video_file in video_files: + video_path = os.path.join(date_dir, video_file) + full_path = os.path.join(self.storage_path, video_path) + size = os.path.getsize(full_path) / (1024 * 1024) # MB + mtime = os.path.getmtime(full_path) + # 根据文件扩展名获取时间 + if video_file.endswith('.avi'): + time_str = video_file.split('_')[1].replace('.avi', '') + else: + time_str = video_file.split('_')[1].replace('.mp4', '') + videos.append({ + 'path': video_path, + 'date': date_dir, + 'time': time_str, + 'size': round(size, 2), + 'mtime': mtime + }) + except Exception as e: + print(f"[API ERROR] 获取视频列表失败: {e}") + return {'error': 'Failed to get videos'}, 500 + + return {'videos': videos} + + def serve_video(self, video_path): + """API: 提供视频文件下载/播放""" + if not self.check_auth(): + return {'error': 'Unauthorized'}, 403 + + try: + # 确保路径安全,防止目录遍历 + full_path = os.path.abspath(os.path.join(self.storage_path, video_path)) + if not full_path.startswith(os.path.abspath(self.storage_path)): + return {'error': 'Invalid path'}, 400 + + directory = os.path.dirname(full_path) + filename = os.path.basename(full_path) + # 根据文件扩展名设置正确的MIME类型 + if filename.endswith('.avi'): + mimetype = 'video/x-msvideo' + elif filename.endswith('.mp4'): + mimetype = 'video/mp4' + else: + mimetype = 'video/mp4' # 默认使用mp4的MIME类型 + return send_from_directory(directory, filename, mimetype=mimetype) + except Exception as e: + print(f"[API ERROR] 提供视频文件失败: {e}") + return {'error': 'Failed to serve video'}, 500 + + def get_disk_usage(self): + """API: 获取磁盘使用情况""" + if not self.check_auth(): + return {'error': 'Unauthorized'}, 403 + + try: + statvfs = os.statvfs(self.storage_path) + total = statvfs.f_frsize * statvfs.f_blocks / (1024 * 1024 * 1024) # GB + used = statvfs.f_frsize * (statvfs.f_blocks - statvfs.f_bfree) / (1024 * 1024 * 1024) # GB + free = statvfs.f_frsize * statvfs.f_bfree / (1024 * 1024 * 1024) # GB + usage = (used / total) * 100 if total > 0 else 0 + + return { + 'total': round(total, 2), + 'used': round(used, 2), + 'free': round(free, 2), + 'usage_percent': round(usage, 1) + } + except Exception as e: + print(f"[API ERROR] 获取磁盘使用情况失败: {e}") + return {'error': 'Failed to get disk usage'}, 500 def start(self): + # 启动视频录制 + self._start_recording_thread() + + # 启动Web服务器 thread = threading.Thread( target=self.app.run, kwargs={'host': self.host, 'port': self.port, 'debug': False, 'use_reloader': False}, diff --git a/py_utils/__pycache__/__init__.cpython-38.pyc b/py_utils/__pycache__/__init__.cpython-38.pyc index 4f0b24d00e63d5580247b2dc5c17c28f2ea7ac34..d908e28e2d9b033465d32b3f7ad24b36000cd58f 100644 GIT binary patch delta 17 Wcmb=g=MLrNX{NGBMgt zj^z<&DzckwBzT6=ZSsFXb2e|F&LaECHbPR2-jl4GSxTTFTeMM0B4 zatcpY6JGBW0#Y0bBEmofKb#N%u_A!P3Lq|K0TOHsObnW=MNuG$Xb>R|B4Q?si$tiQ z3xZ^vfJ78mK~ZW-d~RZKc5zYg=BXmdj2yN=aZRQo+eu>5Qq~}aHXy@ zV|m1xifktv37%neo%~BR|Rtb delta 20 acmeyu{Dql2l$V!_0SE+2tT%GMWCQ>;K?K+U diff --git a/surround_view/__pycache__/base_thread.cpython-38.pyc b/surround_view/__pycache__/base_thread.cpython-38.pyc index 2bfad32f1feb58b9416c2d9757ce07b71816f2d2..68677b344c1be178e2d06b33b58ccf32af9529f0 100644 GIT binary patch delta 38 rcmey)`<<6Nl$V!_0SHzE$8Y40U}cP&oWi;kNOG~MF{({AWIF}`!Gj6c delta 38 rcmey)`<<6Nl$V!_0SE+2tT%E;url77oWi;kNOG~MF{)2CWIF}`z>Eq# diff --git a/surround_view/__pycache__/birdview.cpython-38.pyc b/surround_view/__pycache__/birdview.cpython-38.pyc index 8fd841b53811e7968505355856eea7654713b855..e1fda188c5167a3411846cc0b43d4974ca30e40c 100644 GIT binary patch delta 69 zcmZ1zxh9f3l$V!_0SHzE$8Y48RA#(2Syefl@z&;Abr=LP^RUj!Tg diff --git a/surround_view/__pycache__/fisheye_camera.cpython-38.pyc b/surround_view/__pycache__/fisheye_camera.cpython-38.pyc index 0a3359f90cab66bfdb69bf6414d1ae8a3dba834b..b0614de5df6888f6974e919c59221933cc2e115e 100644 GIT binary patch delta 20 acmaDV{ZyJel$V!_0SHzE$8Y4m#R~vBYXyP; delta 20 acmaDV{ZyJel$V!_0SH)_JvVaS;spRRp#*pU diff --git a/surround_view/__pycache__/imagebuffer.cpython-38.pyc b/surround_view/__pycache__/imagebuffer.cpython-38.pyc index dcc9287e1bffc5f5c6f0289116e8ac60a4407f04..dd3ce67d929ca93595a0230c43a2cd26653b319f 100644 GIT binary patch delta 20 acmbQEK1ZE9l$V!_0SHzE$8Y4GC=38Fxdg=k delta 20 acmbQEK1ZE9l$V!_0SE+2tT%E`6b1k+qXa4d diff --git a/surround_view/__pycache__/param_settings.cpython-38.pyc b/surround_view/__pycache__/param_settings.cpython-38.pyc index cf139b791e9168cef6bb0163e4063439858c649e..83029f6e37b34d1e9c4819d8e1cd832248c9e31c 100644 GIT binary patch delta 20 acmZ3?zL=dml$V!_0SHzE$8Y4G!3+Q`K?I5b delta 20 acmZ3?zL=dml$V!_0SI=__SwiigBbuX7X;D( diff --git a/surround_view/__pycache__/process_thread.cpython-38.pyc b/surround_view/__pycache__/process_thread.cpython-38.pyc index 3d088af02bb1b36f92a66358294534559ddb724e..575cf33bfe190e2bcc5047ba996ccfadd47e5ad6 100644 GIT binary patch delta 20 acmZ20v{r~al$V!_0SHzE$8Y3T;sgLMV+2G1 delta 20 acmZ20v{r~al$V!_0SI({`E2A?;sgLMiUeE$ diff --git a/surround_view/__pycache__/simple_gui.cpython-38.pyc b/surround_view/__pycache__/simple_gui.cpython-38.pyc index df8b600290e523ccf2cba4c04b1a54ccd1bef186..35348bab51a676a76f60ad2dd72ae956fadb3aef 100644 GIT binary patch delta 89 zcmcaDe_NhAl$V!_0SHzE$8Y4G$iaAP^IVP(jErWJ`?%s6%_qO(3S=_|N*9@IcIE!T h#ArX+j_-r07f3`LMCgJDa}Z%O*_l5SC_j_m2>?$m7U2K@ delta 89 zcmcaDe_NhAl$V!_0SE+2tT%E` 环视系统 + @@ -24,26 +31,192 @@
+ + +
+
+
硬盘使用率
+
+
0%
+
+
+ +
+
当前模式:实时监控
+
+
+
+ + + +
+
+ + +
+
+