145 lines
5.0 KiB
Python
145 lines
5.0 KiB
Python
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/<string:device_id>')
|
||
@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() |