diff --git a/agv_app/app.py b/agv_app/app.py index f06983d..42a21da 100644 --- a/agv_app/app.py +++ b/agv_app/app.py @@ -1130,14 +1130,14 @@ def api_camera_capture(): @app.route("/api/camera/arm_refresh") def api_arm_camera_refresh(): - """从机械臂拉一张 JPEG(流式读第一个完整帧,超时则降级)""" + """从机械臂拉一张 JPEG(流式读第一个完整帧)""" import requests try: - r = requests.get(ARM_CAMERA_CONFIG["url"], stream=True, timeout=5) + r = requests.get(ARM_CAMERA_CONFIG["url"], stream=True, timeout=8) if r.status_code != 200: return "", 404 data = b"" - for chunk in r.iter_content(chunk_size=4096): + for chunk in r.iter_content(chunk_size=8192): data += chunk # 在累积数据中找 JPEG 完整帧 s = data.find(b"\xff\xd8") @@ -1145,8 +1145,8 @@ def api_arm_camera_refresh(): if s >= 0 and e > s: r.close() return Response(data[s:e+2], mimetype="image/jpeg") - # 数据太长还没找到 JPEG 也直接返回(可能是空流) - if len(data) > 1024 * 1024: + # 累积超过 5MB 还没找到完整帧,说明流异常,放弃 + if len(data) > 5 * 1024 * 1024: r.close() return "", 404 r.close() @@ -1460,6 +1460,16 @@ if __name__ == "__main__": gs.qr_scanner = QRScanner(CAMERA_CONFIG["device_index"]) gs.camera_opened = gs.qr_scanner.open() logger.info(f"AGV 摄像头初始化: {'成功' if gs.camera_opened else '失败'}") + # 启动时自动检测机械臂摄像头 + try: + import requests as _startup_req + r = _startup_req.get(ARM_CAMERA_CONFIG["url"], stream=True, timeout=5) + gs.arm_camera_opened = (r.status_code == 200) + r.close() + logger.info(f"机械臂摄像头检测: {'成功' if gs.arm_camera_opened else '失败'}") + except Exception as _e: + gs.arm_camera_opened = False + logger.warning(f"机械臂摄像头检测失败: {_e}") app.run( host=SERVER_CONFIG["host"], port=SERVER_CONFIG["port"], diff --git a/arm_server/arm_server.py b/arm_server/arm_server.py index 596c977..1723b46 100644 --- a/arm_server/arm_server.py +++ b/arm_server/arm_server.py @@ -35,39 +35,47 @@ ARM_CAMERA_INDEX = 0 # 机械臂端摄像头设备号 _ffmpeg_proc = None +def _ensure_ffmpeg(): + """确保 ffmpeg 进程在运行,自动重启崩溃的进程""" + global _ffmpeg_proc + if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None: + logger.info(f"启动 ffmpeg 视频流 (Video{ARM_CAMERA_INDEX})") + _ffmpeg_proc = subprocess.Popen( + [ + "ffmpeg", + "-f", "v4l2", + "-input_format", "mjpeg", + "-i", f"/dev/video{ARM_CAMERA_INDEX}", + "-vf", "rotate=PI", + "-q:v", "8", + "-f", "mjpeg", + "-" + ], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + ) + time.sleep(2.0) # 等待 ffmpeg 初始化完成(设备冷启动可能需要更久) + + @arm_video_app.route("/api/camera/preview") def arm_camera_preview(): """机械臂摄像头 MJPEG 流 (ffmpeg)""" - global _ffmpeg_proc + _ensure_ffmpeg() def generate(): global _ffmpeg_proc - # 启动 ffmpeg 进程(如果尚未运行) - if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None: - logger.info(f"启动 ffmpeg 视频流 (Video{ARM_CAMERA_INDEX})") - _ffmpeg_proc = subprocess.Popen( - [ - "ffmpeg", - "-f", "v4l2", - "-input_format", "mjpeg", - "-i", f"/dev/video{ARM_CAMERA_INDEX}", - "-vf", "rotate=PI", - "-q:v", "8", - "-f", "mjpeg", - "-" - ], - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL, - ) - time.sleep(0.5) # 等待 ffmpeg 初始化 - try: while True: if _ffmpeg_proc is None or _ffmpeg_proc.poll() is not None: - break + _ensure_ffmpeg() jpeg = _ffmpeg_proc.stdout.read(65536) if not jpeg: - break + # ffmpeg 无数据输出,重启它 + logger.warning("ffmpeg stdout 空,重启") + _ffmpeg_proc.terminate() + _ffmpeg_proc = None + _ensure_ffmpeg() + continue yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + jpeg + b"\r\n") except Exception as e: logger.error(f"视频流异常: {e}") @@ -92,6 +100,7 @@ def arm_camera_restart(): if _ffmpeg_proc: _ffmpeg_proc.terminate() _ffmpeg_proc = None + _ensure_ffmpeg() return jsonify({"ok": True})