parent
63663ede43
commit
2722248c88
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.11 (yolo8)" />
|
||||
</component>
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (yolo8)" project-jdk-type="Python SDK" />
|
||||
</project>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/pythonProject2.iml" filepath="$PROJECT_DIR$/.idea/pythonProject2.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@
|
||||
|
@ -0,0 +1,261 @@
|
||||
import cv2
|
||||
import time
|
||||
import numpy as np
|
||||
from src import PersonDetector, DistanceCalculator, MapManager, config
|
||||
|
||||
class RealTimePersonDistanceDetector:
|
||||
def __init__(self):
|
||||
self.detector = PersonDetector()
|
||||
self.distance_calculator = DistanceCalculator()
|
||||
self.cap = None
|
||||
self.fps_counter = 0
|
||||
self.fps_time = time.time()
|
||||
self.current_fps = 0
|
||||
|
||||
# 初始化地图管理器
|
||||
if config.ENABLE_MAP_DISPLAY:
|
||||
self.map_manager = MapManager(
|
||||
api_key=config.GAODE_API_KEY,
|
||||
camera_lat=config.CAMERA_LATITUDE,
|
||||
camera_lng=config.CAMERA_LONGITUDE
|
||||
)
|
||||
self.map_manager.set_camera_position(
|
||||
config.CAMERA_LATITUDE,
|
||||
config.CAMERA_LONGITUDE,
|
||||
config.CAMERA_HEADING
|
||||
)
|
||||
print("🗺️ 地图管理器已初始化")
|
||||
else:
|
||||
self.map_manager = None
|
||||
|
||||
def initialize_camera(self):
|
||||
"""初始化摄像头"""
|
||||
self.cap = cv2.VideoCapture(config.CAMERA_INDEX)
|
||||
if not self.cap.isOpened():
|
||||
raise Exception(f"无法开启摄像头 {config.CAMERA_INDEX}")
|
||||
|
||||
# 设置摄像头参数
|
||||
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, config.FRAME_WIDTH)
|
||||
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, config.FRAME_HEIGHT)
|
||||
self.cap.set(cv2.CAP_PROP_FPS, config.FPS)
|
||||
|
||||
# 获取实际设置的参数
|
||||
actual_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
actual_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
actual_fps = int(self.cap.get(cv2.CAP_PROP_FPS))
|
||||
|
||||
print(f"摄像头初始化成功:")
|
||||
print(f" 分辨率: {actual_width}x{actual_height}")
|
||||
print(f" 帧率: {actual_fps} FPS")
|
||||
|
||||
def calculate_fps(self):
|
||||
"""计算实际帧率"""
|
||||
self.fps_counter += 1
|
||||
current_time = time.time()
|
||||
if current_time - self.fps_time >= 1.0:
|
||||
self.current_fps = self.fps_counter
|
||||
self.fps_counter = 0
|
||||
self.fps_time = current_time
|
||||
|
||||
def draw_info_panel(self, frame, person_count=0):
|
||||
"""绘制信息面板"""
|
||||
height, width = frame.shape[:2]
|
||||
|
||||
# 绘制顶部信息栏
|
||||
info_height = 60
|
||||
cv2.rectangle(frame, (0, 0), (width, info_height), (0, 0, 0), -1)
|
||||
|
||||
# 显示FPS
|
||||
fps_text = f"FPS: {self.current_fps}"
|
||||
cv2.putText(frame, fps_text, (10, 25), config.FONT, 0.6, (0, 255, 0), 2)
|
||||
|
||||
# 显示人员计数
|
||||
person_text = f"Persons: {person_count}"
|
||||
cv2.putText(frame, person_text, (150, 25), config.FONT, 0.6, (0, 255, 255), 2)
|
||||
|
||||
# 显示模型信息
|
||||
model_text = self.detector.get_model_info()
|
||||
cv2.putText(frame, model_text, (10, 45), config.FONT, 0.5, (255, 255, 255), 1)
|
||||
|
||||
# 显示操作提示
|
||||
help_text = "Press 'q' to quit | 'c' to calibrate | 'r' to reset | 'm' to open map"
|
||||
text_size = cv2.getTextSize(help_text, config.FONT, 0.5, 1)[0]
|
||||
cv2.putText(frame, help_text, (width - text_size[0] - 10, 25),
|
||||
config.FONT, 0.5, (255, 255, 0), 1)
|
||||
|
||||
# 显示地图状态
|
||||
if self.map_manager:
|
||||
map_status = "Map: ON"
|
||||
cv2.putText(frame, map_status, (10, height - 10),
|
||||
config.FONT, 0.5, (0, 255, 255), 1)
|
||||
|
||||
return frame
|
||||
|
||||
def calibrate_distance(self, detections):
|
||||
"""距离校准模式"""
|
||||
if len(detections) == 0:
|
||||
print("未检测到人体,无法校准")
|
||||
return
|
||||
|
||||
print("\n=== 距离校准模式 ===")
|
||||
print("请确保画面中有一个人,并输入该人距离摄像头的真实距离")
|
||||
|
||||
try:
|
||||
real_distance = float(input("请输入真实距离(厘米): "))
|
||||
|
||||
# 使用第一个检测到的人进行校准
|
||||
detection = detections[0]
|
||||
x1, y1, x2, y2, conf = detection
|
||||
bbox_height = y2 - y1
|
||||
|
||||
# 更新参考参数
|
||||
config.REFERENCE_DISTANCE = real_distance
|
||||
config.REFERENCE_HEIGHT_PIXELS = bbox_height
|
||||
|
||||
# 重新初始化距离计算器
|
||||
self.distance_calculator = DistanceCalculator()
|
||||
|
||||
print(f"校准完成!")
|
||||
print(f"参考距离: {real_distance}cm")
|
||||
print(f"参考像素高度: {bbox_height}px")
|
||||
|
||||
except ValueError:
|
||||
print("输入无效,校准取消")
|
||||
except Exception as e:
|
||||
print(f"校准失败: {e}")
|
||||
|
||||
def process_frame(self, frame):
|
||||
"""处理单帧图像"""
|
||||
# 检测人体
|
||||
detections = self.detector.detect_persons(frame)
|
||||
|
||||
# 计算距离并更新地图位置
|
||||
distances = []
|
||||
if self.map_manager:
|
||||
self.map_manager.clear_persons()
|
||||
|
||||
for i, detection in enumerate(detections):
|
||||
bbox = detection[:4] # [x1, y1, x2, y2]
|
||||
x1, y1, x2, y2 = bbox
|
||||
distance = self.distance_calculator.get_distance(bbox)
|
||||
distance_str = self.distance_calculator.format_distance(distance)
|
||||
distances.append(distance_str)
|
||||
|
||||
# 更新地图上的人员位置
|
||||
if self.map_manager:
|
||||
# 计算人体中心点
|
||||
center_x = (x1 + x2) / 2
|
||||
center_y = (y1 + y2) / 2
|
||||
|
||||
# 将距离从厘米转换为米
|
||||
distance_meters = distance / 100.0
|
||||
|
||||
# 添加到地图
|
||||
self.map_manager.add_person_position(
|
||||
center_x, center_y, distance_meters,
|
||||
frame.shape[1], frame.shape[0], # width, height
|
||||
f"P{i+1}"
|
||||
)
|
||||
|
||||
# 绘制检测结果
|
||||
frame = self.detector.draw_detections(frame, detections, distances)
|
||||
|
||||
# 绘制信息面板
|
||||
frame = self.draw_info_panel(frame, len(detections))
|
||||
|
||||
# 计算FPS
|
||||
self.calculate_fps()
|
||||
|
||||
return frame, detections
|
||||
|
||||
def run(self):
|
||||
"""运行主程序"""
|
||||
try:
|
||||
print("正在初始化...")
|
||||
self.initialize_camera()
|
||||
|
||||
print("系统启动成功!")
|
||||
print("操作说明:")
|
||||
print(" - 按 'q' 键退出程序")
|
||||
print(" - 按 'c' 键进入距离校准模式")
|
||||
print(" - 按 'r' 键重置为默认参数")
|
||||
print(" - 按 's' 键保存当前帧")
|
||||
if self.map_manager:
|
||||
print(" - 按 'm' 键打开地图显示")
|
||||
print(" - 按 'h' 键设置摄像头朝向")
|
||||
print("\n开始实时检测...")
|
||||
|
||||
frame_count = 0
|
||||
|
||||
while True:
|
||||
ret, frame = self.cap.read()
|
||||
if not ret:
|
||||
print("无法读取摄像头画面")
|
||||
break
|
||||
|
||||
# 处理帧
|
||||
processed_frame, detections = self.process_frame(frame)
|
||||
|
||||
# 显示结果
|
||||
cv2.imshow('Real-time Person Distance Detection', processed_frame)
|
||||
|
||||
# 处理按键
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
|
||||
if key == ord('q'):
|
||||
print("用户退出程序")
|
||||
break
|
||||
elif key == ord('c'):
|
||||
# 校准模式
|
||||
self.calibrate_distance(detections)
|
||||
elif key == ord('r'):
|
||||
# 重置参数
|
||||
print("重置为默认参数")
|
||||
self.distance_calculator = DistanceCalculator()
|
||||
elif key == ord('s'):
|
||||
# 保存当前帧
|
||||
filename = f"capture_{int(time.time())}.jpg"
|
||||
cv2.imwrite(filename, processed_frame)
|
||||
print(f"已保存截图: {filename}")
|
||||
elif key == ord('m') and self.map_manager:
|
||||
# 打开地图显示
|
||||
print("正在打开地图...")
|
||||
self.map_manager.open_map()
|
||||
elif key == ord('h') and self.map_manager:
|
||||
# 设置摄像头朝向
|
||||
try:
|
||||
heading = float(input("请输入摄像头朝向角度 (0-360°, 0为正北): "))
|
||||
if 0 <= heading <= 360:
|
||||
self.map_manager.update_camera_heading(heading)
|
||||
else:
|
||||
print("角度必须在0-360度之间")
|
||||
except ValueError:
|
||||
print("输入无效")
|
||||
|
||||
frame_count += 1
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n程序被用户中断")
|
||||
except Exception as e:
|
||||
print(f"程序运行出错: {e}")
|
||||
finally:
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
"""清理资源"""
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
print("资源已清理,程序结束")
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
print("=" * 50)
|
||||
print("实时人体距离检测系统")
|
||||
print("=" * 50)
|
||||
|
||||
detector = RealTimePersonDistanceDetector()
|
||||
detector.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,97 @@
|
||||
1#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
无人机战场态势感知系统 - 启动脚本
|
||||
让用户选择运行模式
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
def show_menu():
|
||||
"""显示菜单"""
|
||||
print("=" * 60)
|
||||
print("🚁 无人机战场态势感知系统")
|
||||
print("=" * 60)
|
||||
print()
|
||||
print("请选择运行模式:")
|
||||
print()
|
||||
print("1. 🌐 Web模式 (推荐)")
|
||||
print(" • 地图作为主界面")
|
||||
print(" • 通过浏览器操作")
|
||||
print(" • 可视化程度更高")
|
||||
print(" • 支持远程访问")
|
||||
print()
|
||||
print("2. 🖥️ 传统模式")
|
||||
print(" • 直接显示摄像头画面")
|
||||
print(" • 键盘快捷键操作")
|
||||
print(" • 性能更好")
|
||||
print(" • 适合本地使用")
|
||||
print()
|
||||
print("3. ⚙️ 配置摄像头位置")
|
||||
print(" • 设置GPS坐标")
|
||||
print(" • 配置朝向角度")
|
||||
print(" • 设置API Key")
|
||||
print()
|
||||
print("4. 🧪 运行系统测试")
|
||||
print(" • 检查各模块状态")
|
||||
print(" • 验证系统功能")
|
||||
print()
|
||||
print("0. ❌ 退出")
|
||||
print()
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
while True:
|
||||
show_menu()
|
||||
try:
|
||||
choice = input("请输入选择 (0-4): ").strip()
|
||||
|
||||
if choice == "1":
|
||||
print("\n🌐 启动Web模式...")
|
||||
import main_web
|
||||
main_web.main()
|
||||
break
|
||||
|
||||
elif choice == "2":
|
||||
print("\n🖥️ 启动传统模式...")
|
||||
import main
|
||||
main.main()
|
||||
break
|
||||
|
||||
elif choice == "3":
|
||||
print("\n⚙️ 配置摄像头位置...")
|
||||
import sys
|
||||
sys.path.append('tools')
|
||||
import setup_camera_location
|
||||
setup_camera_location.main()
|
||||
print("\n配置完成,请重新选择运行模式")
|
||||
input("按回车键继续...")
|
||||
|
||||
elif choice == "4":
|
||||
print("\n🧪 运行系统测试...")
|
||||
import sys
|
||||
sys.path.append('tests')
|
||||
import test_system
|
||||
test_system.main()
|
||||
print("\n测试完成")
|
||||
input("按回车键继续...")
|
||||
|
||||
elif choice == "0":
|
||||
print("\n👋 再见!")
|
||||
sys.exit(0)
|
||||
|
||||
else:
|
||||
print("\n❌ 无效选择,请重新输入")
|
||||
input("按回车键继续...")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n👋 再见!")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"\n❌ 运行出错: {e}")
|
||||
input("按回车键继续...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
实时人体距离检测系统 - 核心模块包
|
||||
|
||||
包含以下模块:
|
||||
- config: 配置文件
|
||||
- person_detector: 人体检测模块
|
||||
- distance_calculator: 距离计算模块
|
||||
"""
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__author__ = "Distance Detection System"
|
||||
|
||||
# 导入核心模块
|
||||
from .config import *
|
||||
from .person_detector import PersonDetector
|
||||
from .distance_calculator import DistanceCalculator
|
||||
from .map_manager import MapManager
|
||||
from .web_server import WebServer
|
||||
from .mobile_connector import MobileConnector, MobileDevice
|
||||
from .orientation_detector import OrientationDetector
|
||||
from .web_orientation_detector import WebOrientationDetector
|
||||
|
||||
__all__ = [
|
||||
'PersonDetector',
|
||||
'DistanceCalculator',
|
||||
'MapManager',
|
||||
'WebServer',
|
||||
'MobileConnector',
|
||||
'MobileDevice',
|
||||
'CAMERA_INDEX',
|
||||
'FRAME_WIDTH',
|
||||
'FRAME_HEIGHT',
|
||||
'FPS',
|
||||
'MODEL_PATH',
|
||||
'CONFIDENCE_THRESHOLD',
|
||||
'IOU_THRESHOLD',
|
||||
'KNOWN_PERSON_HEIGHT',
|
||||
'FOCAL_LENGTH',
|
||||
'REFERENCE_DISTANCE',
|
||||
'REFERENCE_HEIGHT_PIXELS',
|
||||
'FONT',
|
||||
'FONT_SCALE',
|
||||
'FONT_THICKNESS',
|
||||
'BOX_COLOR',
|
||||
'TEXT_COLOR',
|
||||
'TEXT_BG_COLOR',
|
||||
'PERSON_CLASS_ID'
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
手机连接器模块
|
||||
用于接收手机传送的摄像头图像、GPS位置和设备信息
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
from datetime import datetime
|
||||
import base64
|
||||
import socket
|
||||
import struct
|
||||
from typing import Dict, List, Optional, Tuple, Callable
|
||||
from . import config
|
||||
|
||||
class MobileDevice:
|
||||
"""移动设备信息类"""
|
||||
def __init__(self, device_id: str, device_name: str):
|
||||
self.device_id = device_id
|
||||
self.device_name = device_name
|
||||
self.last_seen = time.time()
|
||||
self.is_online = True
|
||||
self.current_location = None # (lat, lng, accuracy)
|
||||
self.battery_level = 100
|
||||
self.signal_strength = 100
|
||||
self.camera_info = {}
|
||||
self.connection_info = {}
|
||||
|
||||
def update_status(self, data: dict):
|
||||
"""更新设备状态"""
|
||||
self.last_seen = time.time()
|
||||
self.is_online = True
|
||||
|
||||
if 'gps' in data:
|
||||
self.current_location = (
|
||||
data['gps'].get('latitude'),
|
||||
data['gps'].get('longitude'),
|
||||
data['gps'].get('accuracy', 0)
|
||||
)
|
||||
|
||||
if 'battery' in data:
|
||||
self.battery_level = data['battery']
|
||||
|
||||
if 'signal' in data:
|
||||
self.signal_strength = data['signal']
|
||||
|
||||
if 'camera_info' in data:
|
||||
self.camera_info = data['camera_info']
|
||||
|
||||
def is_location_valid(self) -> bool:
|
||||
"""检查GPS位置是否有效"""
|
||||
if not self.current_location:
|
||||
return False
|
||||
lat, lng, _ = self.current_location
|
||||
return lat is not None and lng is not None and -90 <= lat <= 90 and -180 <= lng <= 180
|
||||
|
||||
class MobileConnector:
|
||||
"""手机连接器主类"""
|
||||
|
||||
def __init__(self, port: int = 8080):
|
||||
self.port = port
|
||||
self.server_socket = None
|
||||
self.is_running = False
|
||||
self.devices = {} # device_id -> MobileDevice
|
||||
self.frame_callbacks = [] # 帧数据回调函数列表
|
||||
self.location_callbacks = [] # 位置数据回调函数列表
|
||||
self.device_callbacks = [] # 设备状态回调函数列表
|
||||
self.client_threads = []
|
||||
|
||||
# 统计信息
|
||||
self.total_frames_received = 0
|
||||
self.total_data_received = 0
|
||||
self.start_time = time.time()
|
||||
|
||||
def add_frame_callback(self, callback: Callable):
|
||||
"""添加帧数据回调函数"""
|
||||
self.frame_callbacks.append(callback)
|
||||
|
||||
def add_location_callback(self, callback: Callable):
|
||||
"""添加位置数据回调函数"""
|
||||
self.location_callbacks.append(callback)
|
||||
|
||||
def add_device_callback(self, callback: Callable):
|
||||
"""添加设备状态回调函数"""
|
||||
self.device_callbacks.append(callback)
|
||||
|
||||
def start_server(self):
|
||||
"""启动服务器"""
|
||||
try:
|
||||
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.server_socket.bind(('0.0.0.0', self.port))
|
||||
self.server_socket.listen(5)
|
||||
self.is_running = True
|
||||
|
||||
print(f"📱 手机连接服务器启动成功,端口: {self.port}")
|
||||
print(f"🌐 等待手机客户端连接...")
|
||||
|
||||
# 启动服务器监听线程
|
||||
server_thread = threading.Thread(target=self._server_loop, daemon=True)
|
||||
server_thread.start()
|
||||
|
||||
# 启动设备状态监控线程
|
||||
monitor_thread = threading.Thread(target=self._device_monitor, daemon=True)
|
||||
monitor_thread.start()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 启动服务器失败: {e}")
|
||||
return False
|
||||
|
||||
def stop_server(self):
|
||||
"""停止服务器"""
|
||||
self.is_running = False
|
||||
if self.server_socket:
|
||||
self.server_socket.close()
|
||||
|
||||
# 清理客户端连接
|
||||
for thread in self.client_threads:
|
||||
if thread.is_alive():
|
||||
thread.join(timeout=1.0)
|
||||
|
||||
print("📱 手机连接服务器已停止")
|
||||
|
||||
def _server_loop(self):
|
||||
"""服务器主循环"""
|
||||
while self.is_running:
|
||||
try:
|
||||
client_socket, address = self.server_socket.accept()
|
||||
print(f"📱 新的手机客户端连接: {address}")
|
||||
|
||||
# 为每个客户端创建处理线程
|
||||
client_thread = threading.Thread(
|
||||
target=self._handle_client,
|
||||
args=(client_socket, address),
|
||||
daemon=True
|
||||
)
|
||||
client_thread.start()
|
||||
self.client_threads.append(client_thread)
|
||||
|
||||
except Exception as e:
|
||||
if self.is_running:
|
||||
print(f"⚠️ 服务器接受连接时出错: {e}")
|
||||
break
|
||||
|
||||
def _handle_client(self, client_socket, address):
|
||||
"""处理客户端连接"""
|
||||
device_id = None
|
||||
try:
|
||||
while self.is_running:
|
||||
# 接收数据长度
|
||||
length_data = self._recv_all(client_socket, 4)
|
||||
if not length_data:
|
||||
break
|
||||
|
||||
data_length = struct.unpack('!I', length_data)[0]
|
||||
|
||||
# 接收JSON数据
|
||||
json_data = self._recv_all(client_socket, data_length)
|
||||
if not json_data:
|
||||
break
|
||||
|
||||
try:
|
||||
data = json.loads(json_data.decode('utf-8'))
|
||||
device_id = data.get('device_id')
|
||||
|
||||
if device_id:
|
||||
self._process_mobile_data(device_id, data, address)
|
||||
self.total_data_received += len(json_data)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"⚠️ JSON解析错误: {e}")
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 处理客户端 {address} 时出错: {e}")
|
||||
finally:
|
||||
client_socket.close()
|
||||
if device_id and device_id in self.devices:
|
||||
self.devices[device_id].is_online = False
|
||||
print(f"📱 设备 {device_id} 已断开连接")
|
||||
|
||||
def _recv_all(self, socket, length):
|
||||
"""接收指定长度的数据"""
|
||||
data = b''
|
||||
while len(data) < length:
|
||||
packet = socket.recv(length - len(data))
|
||||
if not packet:
|
||||
return None
|
||||
data += packet
|
||||
return data
|
||||
|
||||
def _process_mobile_data(self, device_id: str, data: dict, address):
|
||||
"""处理手机发送的数据"""
|
||||
# 更新或创建设备信息
|
||||
if device_id not in self.devices:
|
||||
device_name = data.get('device_name', f'Mobile-{device_id[:8]}')
|
||||
self.devices[device_id] = MobileDevice(device_id, device_name)
|
||||
print(f"📱 新设备注册: {device_name} ({device_id[:8]})")
|
||||
|
||||
# 触发设备状态回调
|
||||
for callback in self.device_callbacks:
|
||||
try:
|
||||
callback('device_connected', self.devices[device_id])
|
||||
except Exception as e:
|
||||
print(f"⚠️ 设备回调错误: {e}")
|
||||
|
||||
device = self.devices[device_id]
|
||||
device.update_status(data)
|
||||
device.connection_info = {'address': address}
|
||||
|
||||
# 处理图像数据
|
||||
if 'frame' in data:
|
||||
try:
|
||||
frame_data = base64.b64decode(data['frame'])
|
||||
frame = cv2.imdecode(
|
||||
np.frombuffer(frame_data, np.uint8),
|
||||
cv2.IMREAD_COLOR
|
||||
)
|
||||
|
||||
if frame is not None:
|
||||
self.total_frames_received += 1
|
||||
|
||||
# 触发帧数据回调
|
||||
for callback in self.frame_callbacks:
|
||||
try:
|
||||
callback(device_id, frame, device)
|
||||
except Exception as e:
|
||||
print(f"⚠️ 帧回调错误: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 图像数据处理错误: {e}")
|
||||
|
||||
# 处理GPS位置数据
|
||||
if 'gps' in data and device.is_location_valid():
|
||||
for callback in self.location_callbacks:
|
||||
try:
|
||||
callback(device_id, device.current_location, device)
|
||||
except Exception as e:
|
||||
print(f"⚠️ 位置回调错误: {e}")
|
||||
|
||||
def _device_monitor(self):
|
||||
"""设备状态监控"""
|
||||
while self.is_running:
|
||||
try:
|
||||
current_time = time.time()
|
||||
offline_devices = []
|
||||
|
||||
for device_id, device in self.devices.items():
|
||||
# 超过30秒没有数据认为离线
|
||||
if current_time - device.last_seen > 30:
|
||||
if device.is_online:
|
||||
device.is_online = False
|
||||
offline_devices.append(device_id)
|
||||
|
||||
# 通知离线设备
|
||||
for device_id in offline_devices:
|
||||
print(f"📱 设备 {device_id[:8]} 已离线")
|
||||
for callback in self.device_callbacks:
|
||||
try:
|
||||
callback('device_disconnected', self.devices[device_id])
|
||||
except Exception as e:
|
||||
print(f"⚠️ 设备回调错误: {e}")
|
||||
|
||||
time.sleep(5) # 每5秒检查一次
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ 设备监控错误: {e}")
|
||||
time.sleep(5)
|
||||
|
||||
def get_online_devices(self) -> List[MobileDevice]:
|
||||
"""获取在线设备列表"""
|
||||
return [device for device in self.devices.values() if device.is_online]
|
||||
|
||||
def get_device_by_id(self, device_id: str) -> Optional[MobileDevice]:
|
||||
"""根据ID获取设备"""
|
||||
return self.devices.get(device_id)
|
||||
|
||||
def get_statistics(self) -> dict:
|
||||
"""获取连接统计信息"""
|
||||
online_count = len(self.get_online_devices())
|
||||
total_count = len(self.devices)
|
||||
uptime = time.time() - self.start_time
|
||||
|
||||
return {
|
||||
'online_devices': online_count,
|
||||
'total_devices': total_count,
|
||||
'frames_received': self.total_frames_received,
|
||||
'data_received_mb': self.total_data_received / (1024 * 1024),
|
||||
'uptime_seconds': uptime,
|
||||
'avg_frames_per_second': self.total_frames_received / uptime if uptime > 0 else 0
|
||||
}
|
||||
|
||||
def send_command_to_device(self, device_id: str, command: dict):
|
||||
"""向指定设备发送命令(预留接口)"""
|
||||
# TODO: 实现向手机发送控制命令的功能
|
||||
pass
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDiTCCAnGgAwIBAgIUD45qB5JkkfGfRqN8cZTJ1Q2TE14wDQYJKoZIhvcNAQEL
|
||||
BQAwaTELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0Jl
|
||||
aWppbmcxIjAgBgNVBAoMGURpc3RhbmNlIEp1ZGdlbWVudCBTeXN0ZW0xEjAQBgNV
|
||||
BAMMCWxvY2FsaG9zdDAeFw0yNTA2MjkwODQ2MTRaFw0yNjA2MjkwODQ2MTRaMGkx
|
||||
CzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdCZWlqaW5n
|
||||
MSIwIAYDVQQKDBlEaXN0YW5jZSBKdWRnZW1lbnQgU3lzdGVtMRIwEAYDVQQDDAls
|
||||
b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3u/JfTd1P
|
||||
/62wGwE0vAEOOPh0Zxn+lCssp0K9axWTfrvp0oWErcyGCVp+E+QjFOPyf0ocw7BX
|
||||
31O5UoJtOCYHACutXvp+Vd2YFxptXYU+CN/qj4MF+n28U7AwUiWPqSOy9/IMcdOl
|
||||
IfDKkSHCLWmUtNC8ot5eG/mYxqDVLZfI3Carclw/hwIYBa18YnaYG0xYM+G13Xpp
|
||||
yP5itRXLGS8I4GpTCoYFlPq0n+rW81sWNQjw3RmK4t1dF2AWhuDc5nYvRZdf4Qhk
|
||||
ovwW9n48fRaTfsUDylTVZ9RgmSo3KRWmw8DDCo4rlTtOS4x7fd1l6m1JPgPWg9bX
|
||||
9Qbz17wGGoUdAgMBAAGjKTAnMCUGA1UdEQQeMByCCWxvY2FsaG9zdIIJMTI3LjAu
|
||||
MC4xhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQBEneYvDdzdvv65rHUA9UKJzBGs
|
||||
4+j5ZYhCTl0E1HCVxWVHtheUmpUUTlXd0q40NayD0fqt+Cak+0gxKoh8vj1jceKU
|
||||
EO2OSMx7GIEETF1DU2mvaEHvlgLC5YC72DzirGrM+e4VXIIf7suvmcvAw42IGMtw
|
||||
xzEZANYeVY87LYVtJQ0Uw11j2C3dKdQJpEFhldWYwlaLYU6jhtkkiybAa7ZAI1AQ
|
||||
mL+02Y+IQ2sNOuVL7ltqoo0b5BmD4MXjn0wjcy/ARNlq7LxQcvm9UKQCFWtgPGNh
|
||||
qP8BBUq2pbJJFoxgjQYqAAL7tbdimWElBXwiOEESAjjIC8l/YG4s8QKWhGcq
|
||||
-----END CERTIFICATE-----
|
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC3u/JfTd1P/62w
|
||||
GwE0vAEOOPh0Zxn+lCssp0K9axWTfrvp0oWErcyGCVp+E+QjFOPyf0ocw7BX31O5
|
||||
UoJtOCYHACutXvp+Vd2YFxptXYU+CN/qj4MF+n28U7AwUiWPqSOy9/IMcdOlIfDK
|
||||
kSHCLWmUtNC8ot5eG/mYxqDVLZfI3Carclw/hwIYBa18YnaYG0xYM+G13XppyP5i
|
||||
tRXLGS8I4GpTCoYFlPq0n+rW81sWNQjw3RmK4t1dF2AWhuDc5nYvRZdf4QhkovwW
|
||||
9n48fRaTfsUDylTVZ9RgmSo3KRWmw8DDCo4rlTtOS4x7fd1l6m1JPgPWg9bX9Qbz
|
||||
17wGGoUdAgMBAAECggEAAJVp+AexNkHRez5xCFrg2XQp+yW7ifWRiM4RbN0xPs0Y
|
||||
ZJ1BgcwnOTIX7+Q5LdrS2CBitB7zixzCG1qgj2K7nhYg0MJo+pynepOmvNBAyrUa
|
||||
dP1fCF0eXevqc37zGM5w+lpg6aTxw5ByOJtaNOqfikN4QLNBU6GSwA/Hkm8NP56J
|
||||
ZtVBfGE/inq4pyoFxLBwfGgYn9sRoo4AgPaUYiCFL7s4CXpkrFAg86sxkt0ak6pa
|
||||
9Hj9nVIcYdhNlEfvO53pnmU3KeXEGUVaE5CtxATEuYfTqNfb2+CBAUAkd1JTzC6P
|
||||
YLZC1WnrajC9LbblDgWvKQ2ItuNxPcCQOEgQl0IVRwKBgQDf74VeEaCAzQwY48q8
|
||||
/RiuJfCc/C7zAHNk4LuYalWSRFaMfciJSfWHNi2UhTuTYiYdg7rSfdrwLOJg/gz0
|
||||
c/H9k5SPwObFP0iXSY7FRsfviA5BJIe5xHyMNs0upiO9bmPA0X948esk4dCaUwWz
|
||||
TleMHlFSf7gk5sOsL7utYPqF0wKBgQDSCtHnXEaVCzoSrpuw9sEZnNIAqqfPOmfg
|
||||
OYwjz2yp89X4i/N1Lp15oe2vyfGNF4TzRl5kcGwv534om3PjMF9j4ANgy7BCdAx2
|
||||
5YXtoCull8lFd5ansBKM6BYtN/YWABTywxkFxMrR+f7gg7L8ywopGomyyyGc/hX6
|
||||
4UWaRQdDTwKBgAzt/31W9zV4oWIuhN40nuAvQJ1P0kYlmIQSlcJPIXG4kGa8PH/w
|
||||
zURpVGhm6PGxkRHTMU5GBgYoEUoYYRccOrSxeLp0IN7ysHZLwPqTA6hI6snIGi4X
|
||||
sjlGUMKIxTeC0C+p6PpKvZD7mNfQQ1v/Af8NIRTqWu+Gg3XFq8hu+QgRAoGBAMYh
|
||||
+MFTFS2xKnXHCgyTp7G+cYa5dJSRlr0368838lwbLGNJuT133IqJSkpBp78dSYem
|
||||
gJIkTpmduC8b/OR5k/IFtYoQelMlX0Ck4II4ThPlq7IAzjeeatFKeOjs2hEEwL4D
|
||||
dc4wRdZvCZPGCAhYi1wcsXncDfgm4psG934/0UsXAoGAf1mWndfCOtj3/JqjcAKz
|
||||
cCpfdwgFnTt0U3SNZ5FMXZ4oCRXcDiKN7VMJg6ZtxCxLgAXN92eF/GdMotIFd0ou
|
||||
6xXLJzIp0XPc1uh5+VPOEjpqtl/ByURge0sshzce53mrhx6ixgAb2qWBJH/cNmIK
|
||||
VKGQWzXu+zbojPTSWzJltA0=
|
||||
-----END PRIVATE KEY-----
|
@ -0,0 +1,392 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>设备选择器测试</title>
|
||||
<style>
|
||||
body {
|
||||
background: linear-gradient(135deg, #1e3c72, #2a5298);
|
||||
color: white;
|
||||
font-family: 'Microsoft YaHei', sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 15px;
|
||||
margin: 20px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.device-select-btn {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.device-selector {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.device-selector-content {
|
||||
background: rgba(0, 20, 40, 0.95);
|
||||
border: 2px solid #00aaff;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
max-width: 90%;
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.device-selector h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #00aaff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.device-list {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.device-item {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.device-item:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-color: #00aaff;
|
||||
}
|
||||
|
||||
.device-item.selected {
|
||||
background: rgba(0, 170, 255, 0.3);
|
||||
border-color: #00aaff;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
font-weight: bold;
|
||||
color: #00aaff;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.device-id {
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.device-kind {
|
||||
display: inline-block;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.device-selector-buttons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.log {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin: 20px 0;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🧪 设备选择器测试</h1>
|
||||
|
||||
<div class="video-container">
|
||||
<div class="video-header">
|
||||
<span>📹 视频设备</span>
|
||||
<button class="device-select-btn" onclick="showDeviceSelector()">📷 选择设备</button>
|
||||
</div>
|
||||
<div id="videoPlaceholder" style="text-align: center; padding: 40px; color: #ccc;">
|
||||
点击"选择设备"开始使用摄像头
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备选择弹窗 -->
|
||||
<div class="device-selector" id="deviceSelector" style="display: none;">
|
||||
<div class="device-selector-content">
|
||||
<h3>📷 选择视频设备</h3>
|
||||
|
||||
<!-- 本地设备列表 -->
|
||||
<div>
|
||||
<h4 style="color: #4CAF50; margin: 15px 0 10px 0;">📱 本地设备</h4>
|
||||
<div class="device-list" id="localDeviceList">
|
||||
<div class="loading">正在扫描本地设备...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="device-selector-buttons">
|
||||
<button class="btn btn-secondary" onclick="hideDeviceSelector()">❌ 取消</button>
|
||||
<button class="btn btn-primary" onclick="refreshDevices()">🔄 刷新设备</button>
|
||||
<button class="btn" onclick="useSelectedDevice()" id="useDeviceBtn" disabled>✅ 使用选择的设备</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="log" id="logPanel">
|
||||
<div>系统初始化中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let availableDevices = [];
|
||||
let selectedDeviceId = null;
|
||||
let selectedDeviceInfo = null;
|
||||
|
||||
// 日志函数
|
||||
function log(message, type = 'info') {
|
||||
const logPanel = document.getElementById('logPanel');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const entry = document.createElement('div');
|
||||
entry.style.color = type === 'error' ? '#ff6b6b' : type === 'success' ? '#51cf66' : '#74c0fc';
|
||||
entry.textContent = `${timestamp} - ${message}`;
|
||||
logPanel.appendChild(entry);
|
||||
logPanel.scrollTop = logPanel.scrollHeight;
|
||||
}
|
||||
|
||||
// 扫描设备
|
||||
async function scanDevices() {
|
||||
log('正在扫描可用视频设备...', 'info');
|
||||
try {
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
|
||||
throw new Error('浏览器不支持设备枚举功能');
|
||||
}
|
||||
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
availableDevices = devices.filter(device => device.kind === 'videoinput');
|
||||
|
||||
log(`发现 ${availableDevices.length} 个视频设备`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
log(`设备扫描失败: ${error.message}`, 'error');
|
||||
availableDevices = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 显示设备选择器
|
||||
async function showDeviceSelector() {
|
||||
log('打开设备选择器', 'info');
|
||||
const selector = document.getElementById('deviceSelector');
|
||||
selector.style.display = 'flex';
|
||||
|
||||
await scanDevices();
|
||||
updateDeviceList();
|
||||
}
|
||||
|
||||
// 隐藏设备选择器
|
||||
function hideDeviceSelector() {
|
||||
document.getElementById('deviceSelector').style.display = 'none';
|
||||
clearDeviceSelection();
|
||||
}
|
||||
|
||||
// 刷新设备
|
||||
async function refreshDevices() {
|
||||
document.getElementById('localDeviceList').innerHTML = '<div class="loading">正在扫描设备...</div>';
|
||||
|
||||
await scanDevices();
|
||||
updateDeviceList();
|
||||
}
|
||||
|
||||
// 更新设备列表
|
||||
function updateDeviceList() {
|
||||
const localList = document.getElementById('localDeviceList');
|
||||
|
||||
if (availableDevices.length === 0) {
|
||||
localList.innerHTML = '<div style="color: #ff6b6b; text-align: center; padding: 20px;">未发现本地摄像头设备<br><small>请确保已连接摄像头并允许浏览器访问</small></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
localList.innerHTML = '';
|
||||
availableDevices.forEach((device, index) => {
|
||||
const deviceItem = document.createElement('div');
|
||||
deviceItem.className = 'device-item';
|
||||
deviceItem.onclick = () => selectDevice(device.deviceId, {
|
||||
label: device.label || `摄像头 ${index + 1}`,
|
||||
kind: device.kind,
|
||||
isRemote: false
|
||||
});
|
||||
|
||||
const deviceName = device.label || `摄像头 ${index + 1}`;
|
||||
const isFrontCamera = deviceName.toLowerCase().includes('front') || deviceName.toLowerCase().includes('前');
|
||||
const isBackCamera = deviceName.toLowerCase().includes('back') || deviceName.toLowerCase().includes('后');
|
||||
|
||||
let cameraIcon = '📷';
|
||||
if (isFrontCamera) cameraIcon = '🤳';
|
||||
else if (isBackCamera) cameraIcon = '📹';
|
||||
|
||||
deviceItem.innerHTML = `
|
||||
<div class="device-name">${cameraIcon} ${deviceName}</div>
|
||||
<div class="device-id">${device.deviceId}</div>
|
||||
<div class="device-kind">本地设备</div>
|
||||
`;
|
||||
localList.appendChild(deviceItem);
|
||||
});
|
||||
}
|
||||
|
||||
// 选择设备
|
||||
function selectDevice(deviceId, deviceInfo) {
|
||||
// 清除之前的选择
|
||||
document.querySelectorAll('.device-item').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
|
||||
// 选择当前设备
|
||||
event.currentTarget.classList.add('selected');
|
||||
selectedDeviceId = deviceId;
|
||||
selectedDeviceInfo = deviceInfo;
|
||||
|
||||
// 启用使用按钮
|
||||
document.getElementById('useDeviceBtn').disabled = false;
|
||||
|
||||
log(`已选择设备: ${deviceInfo.label}`, 'info');
|
||||
}
|
||||
|
||||
// 清除设备选择
|
||||
function clearDeviceSelection() {
|
||||
selectedDeviceId = null;
|
||||
selectedDeviceInfo = null;
|
||||
document.getElementById('useDeviceBtn').disabled = true;
|
||||
document.querySelectorAll('.device-item').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
}
|
||||
|
||||
// 使用选择的设备
|
||||
async function useSelectedDevice() {
|
||||
if (!selectedDeviceId || !selectedDeviceInfo) {
|
||||
log('请先选择一个设备', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log(`正在启动设备: ${selectedDeviceInfo.label}`, 'info');
|
||||
|
||||
const constraints = {
|
||||
video: {
|
||||
deviceId: { exact: selectedDeviceId },
|
||||
width: { ideal: 640 },
|
||||
height: { ideal: 480 }
|
||||
},
|
||||
audio: false
|
||||
};
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
|
||||
// 创建视频元素显示
|
||||
const placeholder = document.getElementById('videoPlaceholder');
|
||||
placeholder.innerHTML = `
|
||||
<video autoplay muted playsinline style="width: 100%; height: auto;"></video>
|
||||
<div style="font-size: 12px; color: #ccc; margin-top: 10px;">
|
||||
正在使用: ${selectedDeviceInfo.label}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const videoElement = placeholder.querySelector('video');
|
||||
videoElement.srcObject = stream;
|
||||
|
||||
hideDeviceSelector();
|
||||
log(`设备启动成功: ${selectedDeviceInfo.label}`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
let errorMsg = error.message;
|
||||
if (error.name === 'NotAllowedError') {
|
||||
errorMsg = '设备权限被拒绝,请允许访问摄像头';
|
||||
} else if (error.name === 'NotFoundError') {
|
||||
errorMsg = '设备未找到或已被占用';
|
||||
}
|
||||
|
||||
log(`设备启动失败: ${errorMsg}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
window.addEventListener('load', () => {
|
||||
log('设备选择器测试页面已加载', 'success');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
Binary file not shown.
@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDjzCCAnegAwIBAgIUJK9wxusX1FTV1FbBSRVlZUwUdmcwDQYJKoZIhvcNAQEL
|
||||
BQAwaTELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0Jl
|
||||
aWppbmcxIjAgBgNVBAoMGURpc3RhbmNlIEp1ZGdlbWVudCBTeXN0ZW0xEjAQBgNV
|
||||
BAMMCWxvY2FsaG9zdDAeFw0yNTA2MjkwODQ2MDNaFw0yNjA2MjkwODQ2MDNaMGkx
|
||||
CzAJBgNVBAYTAkNOMRAwDgYDVQQIDAdCZWlqaW5nMRAwDgYDVQQHDAdCZWlqaW5n
|
||||
MSIwIAYDVQQKDBlEaXN0YW5jZSBKdWRnZW1lbnQgU3lzdGVtMRIwEAYDVQQDDAls
|
||||
b2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCEb/6AFLiJ
|
||||
18UpEH5DKJdiXBAzc/c9ECzU/4k+ZJpp9Hs2SfhZhRvDpTw90dZReQfh2squyLcy
|
||||
DhyWTF+7nedSX4AaU1zQSwdQR/J+0T4fCH23gwxcSWRXA2yUrIgjgCtfc3wSzU+X
|
||||
eeA9ntjBAv+Axf9PPwMiqtxmWjNCc8YmDr24L0oBWwBSX0lsIB6cmTBOhwkAI5J5
|
||||
DWcHaIi/jPjojwJu6z8mGIPmaOBqf12ZJfMB1u3YUHysZYidVuA3TaR/Dx07tZJk
|
||||
yd/RT8sJYJ4VdiA7ZxOynrKh/z84fqpKl2xzuCHGIMJFsyTyKmsYNux0h0lCKGNL
|
||||
dLotTjYC8ravAgMBAAGjLzAtMCsGA1UdEQQkMCKCCWxvY2FsaG9zdIIJMTI3LjAu
|
||||
MC4xhwR/AAABhwQAAAAAMA0GCSqGSIb3DQEBCwUAA4IBAQAaU/fpR8g5mUmFjdco
|
||||
/0vkcoUxH4mO3w8cbVcRO513KKmV3mLQim+sNkL+mdEXSsHDdoiz/rjhTD6i9LW4
|
||||
qrQCIvYPJHtFr2SEdOJWLHsPMaOv86DnF0ufEIB22SmnFAFa75PN35p08JZoWiUk
|
||||
19RmC5gXn2G32eGRfwir9a+sB9lS4Q0MfmSdK8myb32JmuXkFWJgB5jtzEsVDX3q
|
||||
RpLVBlM7CIisX9+EfrjJVeaj5EnlLeFayHEnyuRBFy2k4mqdhdMOFxdmaqmTtmS+
|
||||
TFrmCiGGKU74HLmGr4m10ZBkL5hhw/7XtGqTDMzKLmPXf62j1HoJhhdzVH2QbbRy
|
||||
QnR2
|
||||
-----END CERTIFICATE-----
|
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCEb/6AFLiJ18Up
|
||||
EH5DKJdiXBAzc/c9ECzU/4k+ZJpp9Hs2SfhZhRvDpTw90dZReQfh2squyLcyDhyW
|
||||
TF+7nedSX4AaU1zQSwdQR/J+0T4fCH23gwxcSWRXA2yUrIgjgCtfc3wSzU+XeeA9
|
||||
ntjBAv+Axf9PPwMiqtxmWjNCc8YmDr24L0oBWwBSX0lsIB6cmTBOhwkAI5J5DWcH
|
||||
aIi/jPjojwJu6z8mGIPmaOBqf12ZJfMB1u3YUHysZYidVuA3TaR/Dx07tZJkyd/R
|
||||
T8sJYJ4VdiA7ZxOynrKh/z84fqpKl2xzuCHGIMJFsyTyKmsYNux0h0lCKGNLdLot
|
||||
TjYC8ravAgMBAAECggEAC9+MhggViUoeY3GWmEfF1qwhSbOeWUufcVMdh0n2rAQe
|
||||
nb3g9Ymg9RfVwEcVO0WqBr4aSLQ29FZeirz7IjNkXzavoeySWBw54iEpJOR2eMrG
|
||||
lpK5o3Zy9/gXHncfV2twuAR+/aKJfa+QAoZAsYEmzfEyU/T2v39o9gYlLVJ608OC
|
||||
iyb3xRsiidnonRR7pCIX2ghI/GJcKFZmYbc2g4hehBz6zBN7Xu7t26sUrdJd9tT2
|
||||
wWIEz0pH2Iutwiy4mlfkqJ+dezSZPCRXxLHbq2RRKn/17YNiCvTjBpsX83FxcwKR
|
||||
6XlIabWMNJ6EOvNGtwufXAUwrieHq6uPFx5mKHfIoQKBgQC6WWk0IyZOFyuqTNBq
|
||||
2axuXkxmU+xjfbu4NyFXxoNmyiR6VkK4tpcj7nV2ZN6gdA8pCOjZiQM9NA3GYPPv
|
||||
eOcTvgIL16cE4EdrkE+Jv+UF7SAhToPbrnBF9y2GN5FBk9L2tvDDeF0qcXzyIleK
|
||||
9dJYqoAxssCUIhASb5AsCoo6oQKBgQC18BqB1Ucp9tz7juRP+rT81zESJCEGpfu1
|
||||
TPOiZgjkr6jR5xsvFHOBtbnXkCi7hir1Wo9kRTujsL58Pu+vA7M6ZWjpGIBtdfmw
|
||||
fSUZmt+hW+V6O1K8WQRFQgErM3PJNBN6l/mLh9Lj39tyeFrrA1WBhtx4mVot4DTC
|
||||
ds9CVb0/TwKBgCXWX8kpVe7HP6N9o1f+yMdEOGkSo030Srh14TxMX4PwiYWZnESb
|
||||
NociNRGMG7QivK1NVNJOwqybtCxSpVU7jFfy3cF/0TbpPzc0/yFuKFeStVJt+dIS
|
||||
UlOyg7jb8Y+KL2zO6oYWG3yxvHgBxxq9HS/Jtuvgar/pRrAnnPOEVFrhAoGAHVwx
|
||||
6uHQKiV8Y9wbXAzJSEQx1wudiMUgaZGRf5OXu8/dHoJ9EIvsV/JLm03YROrR4+ZJ
|
||||
XZUOmsva8ZH2e/fM5I+Y7oTVtNRlBuYrJoansBJ0ZdVM9LgoyERui9oxxTZyLkZ4
|
||||
LtwsXDmz4DUr9uEC23Q3//4/X0ffO8KQj9PmRmECgYBR3YU15qledPzD2n5CtqKD
|
||||
EiVkB1TRZPq46bJFTZ/WhwvIOOrSDb4u7aYP4DzW7kzZt+uRiAHNfrY5AE29681c
|
||||
llt1kr+MrAbX0CdqYUWJoT0Z8Svuw083m9O0EPAZMiYT73izgcvlvvG5MT9uHkQB
|
||||
q6LmyYRBH1NLxYz0aFvY+w==
|
||||
-----END PRIVATE KEY-----
|
Binary file not shown.
Loading…
Reference in new issue