import threading import time import re import traceback import paho.mqtt.client as mqtt from flask import Blueprint, Response, session from app.glo import * # ==================== 全局配置 ==================== mqtt_client = None MQTT_SUB_TOPIC = "" latest_frame_cache = {} cache_lock = threading.Lock() mqtt_sub_lock = threading.Lock() wrobot_cam = Blueprint('wrobot_cam', __name__) # ==================== MQTT 核心逻辑 ==================== def on_mqtt_connect(client, userdata, flags, rc): """MQTT连接成功回调:此时暂不订阅(等第一次页面访问时再订阅)""" if rc == 0: print("MQTT连接成功(暂未订阅任何主题,等待页面访问)") else: print(f"MQTT连接失败 错误码: {rc}") def on_mqtt_message(client, userdata, message): """MQTT消息回调:仅处理已订阅的主题""" try: if not MQTT_SUB_TOPIC or message.topic != MQTT_SUB_TOPIC: return device_id = message.topic.split('/')[0] if len(message.payload) < 100: return with cache_lock: latest_frame_cache[device_id] = message.payload except Exception as e: print(f"MQTT消息处理异常: {e}") traceback.print_exc() def start_mqtt_background(): """启动MQTT客户端(仅连接,不订阅)""" global mqtt_client if mqtt_client is not None: return mqtt_client = mqtt.Client() mqtt_client.on_connect = on_mqtt_connect mqtt_client.on_message = on_mqtt_message try: mqtt_client.connect(MQTT_BROKER, MQTT_PORT, keepalive=60) mqtt_client.loop_start() print(f"MQTT客户端已启动 🚀(仅连接,未订阅主题)") except Exception as e: print(f"MQTT启动失败: {e}") traceback.print_exc() def subscribe_first_topic(new_topic): """第一次订阅主题(初始化订阅)""" global MQTT_SUB_TOPIC if not mqtt_client or not mqtt_client.is_connected(): raise RuntimeError("MQTT客户端未连接") with mqtt_sub_lock: MQTT_SUB_TOPIC = new_topic mqtt_client.subscribe(MQTT_SUB_TOPIC) print(f"MQTT首次订阅: {MQTT_SUB_TOPIC}") def update_mqtt_subscription(new_topic): """更新订阅主题(非首次)""" global MQTT_SUB_TOPIC if not mqtt_client or not mqtt_client.is_connected(): raise RuntimeError("MQTT客户端未连接") with mqtt_sub_lock: # 取消旧主题订阅 mqtt_client.unsubscribe(MQTT_SUB_TOPIC) print(f"MQTT取消订阅: {MQTT_SUB_TOPIC}") # 订阅新主题 MQTT_SUB_TOPIC = new_topic mqtt_client.subscribe(MQTT_SUB_TOPIC) print(f"MQTT重新订阅: {MQTT_SUB_TOPIC}") # ==================== Flask 路由核心逻辑 ==================== def generate_video_frame(device_id): """生成视频流""" blank_jpeg = b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xd9' while True: try: with cache_lock: frame_data = latest_frame_cache.get(device_id, blank_jpeg) yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame_data + b'\r\n') time.sleep(0.01) except GeneratorExit: break except Exception as e: print(f"视频流生成异常: {e}") yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + blank_jpeg + b'\r\n') time.sleep(0.1) @wrobot_cam.route('/wrobot_cam/') @wrobot_cam.route('/wrobot_cam') def main(device_id=None): """核心逻辑:第一次访问时初始化订阅,后续访问时更新订阅""" # 1. 校验设备ID(无参数时直接报错,因为启动时未知默认设备) if not device_id: return Response("必须指定设备ID(访问方式:/wrobot_cam/设备ID)", status=400, mimetype='text/plain') device_list = GetUserDeviceIdList(session['user']['user_id']) if device_id not in device_list: return Response(f"设备ID不合法", status=400, mimetype='text/plain') # 2. 构建真实主题 new_mqtt_topic = f"{device_id}/head_video" # 3. 区分「首次订阅」和「更新订阅」 try: if not MQTT_SUB_TOPIC: # 初始值为空 → 第一次访问,初始化订阅 threading.Thread(target=subscribe_first_topic, args=(new_mqtt_topic,), daemon=True).start() elif new_mqtt_topic != MQTT_SUB_TOPIC: # 非首次,更新订阅 threading.Thread(target=update_mqtt_subscription, args=(new_mqtt_topic,), daemon=True).start() except RuntimeError as e: return Response(f"MQTT客户端异常:{str(e)}", status=500, mimetype='text/plain') # 4. 返回视频流 return Response( generate_video_frame(device_id), mimetype='multipart/x-mixed-replace; boundary=frame' ) # ==================== 启动初始化 ==================== start_mqtt_background()