You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Software_Architecture/distance-judgement/mobile/mobile_client.html

2874 lines
124 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🚁 移动侦察终端</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
min-height: 100vh;
padding: 10px;
}
.container {
max-width: 100%;
margin: 0 auto;
}
.header {
text-align: center;
padding: 20px 0;
border-bottom: 2px solid rgba(255, 255, 255, 0.2);
margin-bottom: 20px;
}
.header h1 {
font-size: 24px;
margin-bottom: 5px;
}
.status-panel {
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
padding: 15px;
margin-bottom: 20px;
}
.status-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
font-size: 14px;
}
.status-value {
font-weight: bold;
color: #4CAF50;
}
.video-container {
position: relative;
background: #000;
border-radius: 10px;
overflow: hidden;
margin-bottom: 20px;
}
#videoElement {
width: 100%;
height: auto;
display: block;
}
.controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-bottom: 20px;
}
.btn {
padding: 15px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-primary {
background: #4CAF50;
color: white;
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-secondary {
background: #2196F3;
color: white;
}
.btn-success {
background: #4CAF50;
color: white;
}
.btn:active {
transform: scale(0.95);
}
.btn:disabled {
background: #666;
cursor: not-allowed;
}
.settings {
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
padding: 15px;
margin-bottom: 20px;
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.setting-row:last-child {
margin-bottom: 0;
}
input[type="text"],
input[type="number"],
select {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 5px;
padding: 8px;
color: white;
width: 120px;
}
input[type="text"]::placeholder {
color: rgba(255, 255, 255, 0.6);
}
.log-panel {
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
padding: 15px;
height: 200px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
line-height: 1.4;
}
.log-entry {
margin-bottom: 5px;
opacity: 0.8;
}
.log-error {
color: #ff6b6b;
}
.log-success {
color: #51cf66;
}
.log-info {
color: #74c0fc;
}
.connection-indicator {
position: fixed;
top: 10px;
right: 10px;
width: 12px;
height: 12px;
border-radius: 50%;
background: #f44336;
animation: pulse 2s infinite;
}
.connection-indicator.connected {
background: #4CAF50;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
font-size: 12px;
}
.stat-item {
text-align: center;
padding: 10px;
background: rgba(255, 255, 255, 0.1);
border-radius: 5px;
}
/* 视频头部样式 */
.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;
}
</style>
</head>
<body>
<div class="connection-indicator" id="connectionIndicator"></div>
<div class="container">
<div class="header">
<h1>🚁 移动侦察终端</h1>
<div id="deviceInfo">正在初始化...</div>
<div id="compatibilityNotice"
style="display: none; margin-top: 10px; padding: 8px; background: rgba(255, 152, 0, 0.2); border-radius: 5px; font-size: 12px; color: #ffa726;">
<strong>兼容模式</strong>:已为您的浏览器启用兼容支持
</div>
</div>
<div class="status-panel">
<div class="status-row">
<span>📍 GPS坐标</span>
<span class="status-value" id="gpsStatus">获取中...</span>
</div>
<div class="status-row">
<span>🧭 设备朝向</span>
<span class="status-value" id="orientationStatus">获取中...</span>
</div>
<div class="status-row">
<span>🔋 电池电量</span>
<span class="status-value" id="batteryStatus">--</span>
</div>
<div class="status-row">
<span>📶 信号强度</span>
<span class="status-value" id="signalStatus">--</span>
</div>
<div class="status-row">
<span>🌐 连接状态</span>
<span class="status-value" id="connectionStatus">离线</span>
</div>
</div>
<div class="video-container">
<div class="video-header">
<span>📹 实时视频监控</span>
<span id="videoStatus" style="font-size: 12px; color: #ccc;">准备就绪</span>
</div>
<video id="videoElement" autoplay muted playsinline
style="width: 100%; height: auto; display: none;"></video>
<div id="videoPlaceholder" style="text-align: center; padding: 40px; color: #ccc;">
点击"开始传输"启动视频监控
</div>
</div>
<div class="controls">
<button class="btn btn-secondary" id="settingsBtn">⚙️ 设置</button>
<button class="btn btn-primary" id="startBtn">📹 开始传输</button>
<button class="btn btn-danger" id="stopBtn" disabled>⏹️ 停止传输</button>
<button class="btn btn-secondary" id="reconnectBtn">🔄 重连</button>
<button class="btn btn-secondary" id="diagnosisBtn" style="background: #ff9800; color: white;">🔧
诊断</button>
<button class="btn btn-secondary" onclick="window.open('baidu_browser_test.html', '_blank')"
style="background: #9C27B0;">🔧 百度浏览器测试</button>
<button class="btn btn-secondary" onclick="mobileClient.tryFileCapture()" style="background: #FF9800;">📷
文件捕获</button>
<button class="btn btn-secondary" onclick="mobileClient.tryRealTimeCapture()"
style="background: #4CAF50;">📹 实时视频流</button>
<button id="stopStreamBtn" class="btn btn-secondary" onclick="mobileClient.stopVideoStream()"
style="background: #f44336; display: none;">⏹️ 停止视频流</button>
<button id="cameraInfoBtn" class="btn btn-secondary" onclick="mobileClient.getCameraCapabilities()"
style="background: #9C27B0; display: none;">📋 摄像头信息</button>
<button id="qualityBtn" class="btn btn-secondary" onclick="mobileClient.adjustVideoQuality(1280, 720, 60)"
style="background: #FF5722; display: none;">🎬 高质量模式</button>
</div>
<div class="settings" id="settingsPanel" style="display: none;">
<h3 style="margin-bottom: 15px;">⚙️ 连接设置</h3>
<div class="setting-row">
<label>服务器地址</label>
<input type="text" id="serverHost" value="" placeholder="自动检测">
</div>
<div class="setting-row">
<label>端口</label>
<input type="number" id="serverPort" value="5000" min="1" max="65535">
</div>
<div class="setting-row">
<label>帧率</label>
<select id="frameRate">
<option value="1">1 FPS</option>
<option value="2" selected>2 FPS</option>
<option value="5">5 FPS</option>
<option value="10">10 FPS</option>
</select>
</div>
<div class="setting-row">
<label>图像质量</label>
<select id="imageQuality">
<option value="0.3">低 (30%)</option>
<option value="0.5">中 (50%)</option>
<option value="0.7" selected>高 (70%)</option>
<option value="0.9">极高 (90%)</option>
</select>
</div>
</div>
<div class="stats">
<div class="stat-item">
<div>📊 已发送帧数</div>
<div id="frameCount">0</div>
</div>
<div class="stat-item">
<div>📈 数据量</div>
<div id="dataAmount">0 KB</div>
</div>
</div>
<!-- 🚀 性能优化建议面板 -->
<div class="performance-tips"
style="background: rgba(33, 150, 243, 0.1); border: 1px solid #2196F3; border-radius: 8px; padding: 15px; margin: 20px 0;">
<h4 style="margin: 0 0 10px 0; color: #2196F3;">🚀 性能优化建议</h4>
<div style="font-size: 13px; color: #666;">
<div>📶 <strong>网络良好</strong>: 可选择 5-10 FPS + 高质量(70-90%)</div>
<div>📱 <strong>网络一般</strong>: 建议 2-5 FPS + 中质量(50-70%)</div>
<div>🐌 <strong>网络较慢</strong>: 选择 1-2 FPS + 低质量(30-50%)</div>
<div style="margin-top: 8px; color: #2196F3;">💡 系统会自动监控网络状况并给出调整建议</div>
</div>
</div>
<div class="log-panel" id="logPanel">
<div class="log-entry log-info">系统初始化中...</div>
</div>
<div
style="background: rgba(255, 193, 7, 0.2); border: 1px solid #ffc107; border-radius: 8px; padding: 15px; margin: 20px 0; color: #856404;">
<h4 style="margin: 0 0 10px 0; color: #ffc107;">📍 GPS权限说明</h4>
<p style="margin: 5px 0; font-size: 14px;">如果GPS获取失败请确保</p>
<ul style="margin: 5px 0; padding-left: 20px; font-size: 13px;">
<li>在浏览器弹出权限请求时点击"允许"</li>
<li>在浏览器地址栏点击🔒图标,设置位置权限为"允许"</li>
<li>确保设备GPS已开启</li>
<li>在室外或窗边以获得更好的GPS信号</li>
</ul>
</div>
</div>
<script>
class MobileClient {
constructor() {
this.socket = null;
this.isStreaming = false;
this.videoElement = document.getElementById('videoElement');
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
this.deviceId = this.generateDeviceId();
this.frameCount = 0;
this.dataAmount = 0;
this.currentPosition = null;
this.currentOrientation = null; // 🌟 当前设备朝向
this.lastDataSendTime = 0; // 数据发送节流
// 🚀 性能监控
this.lastSendTime = 0;
this.averageUploadTime = 0;
this.performanceStats = [];
// 视频流管理
this.currentStream = null;
// 自动检测服务器地址和协议
this.serverHost = window.location.hostname;
this.serverPort = window.location.port || 5000;
this.serverProtocol = window.location.protocol; // 'http:' 或 'https:'
this.baseURL = `${this.serverProtocol}//${this.serverHost}:${this.serverPort}`;
this.init();
}
generateDeviceId() {
return 'mobile_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
async init() {
this.log('正在初始化移动终端...', 'info');
// 首先进行浏览器兼容性检查
this.checkBrowserCompatibility();
this.updateDeviceInfo();
this.initializeServerSettings();
this.startLocationTracking();
this.startOrientationTracking(); // 🌟 启动朝向追踪
this.updateBatteryStatus();
this.bindEvents();
this.log('移动终端初始化完成', 'success');
}
initializeServerSettings() {
// 设置服务器地址到输入框
document.getElementById('serverHost').value = this.serverHost;
document.getElementById('serverPort').value = this.serverPort;
this.log(`服务器地址: ${this.baseURL}`, 'info');
this.log(`协议: ${this.serverProtocol.replace(':', '')}, 主机: ${this.serverHost}, 端口: ${this.serverPort}`, 'info');
}
updateDeviceInfo() {
const userAgent = navigator.userAgent;
let deviceName = 'Unknown Device';
if (/iPhone/i.test(userAgent)) deviceName = 'iPhone';
else if (/iPad/i.test(userAgent)) deviceName = 'iPad';
else if (/Android/i.test(userAgent)) deviceName = 'Android';
document.getElementById('deviceInfo').textContent = `${deviceName} (${this.deviceId.substr(0, 8)})`;
}
checkBrowserCompatibility() {
const compatibility = {
mediaDevices: !!navigator.mediaDevices,
getUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
enumerateDevices: !!(navigator.mediaDevices && navigator.mediaDevices.enumerateDevices),
permissions: !!navigator.permissions,
isSecure: location.protocol === 'https:' || location.hostname === 'localhost',
userAgent: navigator.userAgent
};
this.log('浏览器兼容性检查:', 'info');
this.log(`MediaDevices API: ${compatibility.mediaDevices ? '✅' : '❌'}`, compatibility.mediaDevices ? 'success' : 'error');
this.log(`getUserMedia: ${compatibility.getUserMedia ? '✅' : '❌'}`, compatibility.getUserMedia ? 'success' : 'error');
this.log(`enumerateDevices: ${compatibility.enumerateDevices ? '✅' : '❌'}`, compatibility.enumerateDevices ? 'success' : 'warning');
this.log(`Permissions API: ${compatibility.permissions ? '✅' : '❌'}`, compatibility.permissions ? 'success' : 'warning');
this.log(`安全环境: ${compatibility.isSecure ? '✅' : '❌'}`, compatibility.isSecure ? 'success' : 'error');
if (!compatibility.getUserMedia) {
this.log('⚠️ 检测到旧版浏览器,正在尝试启用兼容模式...', 'warning');
this.enableLegacySupport();
// 显示兼容性提示
document.getElementById('compatibilityNotice').style.display = 'block';
}
return compatibility;
}
enableLegacySupport() {
this.log('🔧 正在启用多浏览器兼容模式...', 'info');
// 检测浏览器类型
const userAgent = navigator.userAgent.toLowerCase();
const isBaidu = userAgent.includes('baidubrowser') || userAgent.includes('baidu');
const isUC = userAgent.includes('ucbrowser') || userAgent.includes('ucweb');
const isQQ = userAgent.includes('qqbrowser') || userAgent.includes('mqqbrowser');
const is360 = userAgent.includes('360') || userAgent.includes('qihoo');
const isSogou = userAgent.includes('sogou') || userAgent.includes('metasr');
const isWeChat = userAgent.includes('micromessenger');
this.log(`检测到浏览器类型: ${isBaidu ? '百度' : isUC ? 'UC' : isQQ ? 'QQ' : is360 ? '360' : isSogou ? '搜狗' : isWeChat ? '微信' : '其他'}浏览器`, 'info');
// 为navigator.mediaDevices创建兼容层
if (!navigator.mediaDevices) {
navigator.mediaDevices = {};
this.log('✅ 已创建mediaDevices兼容层', 'success');
}
// 多重getUserMedia兼容性实现
if (!navigator.mediaDevices.getUserMedia) {
navigator.mediaDevices.getUserMedia = (constraints) => {
return new Promise((resolve, reject) => {
this.log('🎯 尝试多种getUserMedia API...', 'info');
// 方法1: 标准webkit前缀 (Chrome内核)
if (navigator.webkitGetUserMedia) {
this.log('使用webkit前缀API', 'info');
navigator.webkitGetUserMedia(constraints, resolve, reject);
return;
}
// 方法2: Mozilla前缀 (Firefox内核)
if (navigator.mozGetUserMedia) {
this.log('使用moz前缀API', 'info');
navigator.mozGetUserMedia(constraints, resolve, reject);
return;
}
// 方法3: 标准getUserMedia (老版本)
if (navigator.getUserMedia) {
this.log('使用标准getUserMedia API', 'info');
navigator.getUserMedia(constraints, resolve, reject);
return;
}
// 方法4: 百度浏览器特殊处理
if (isBaidu) {
this.log('检测到百度浏览器尝试专用API...', 'info');
// 尝试多种百度浏览器可能的API
if (window.external) {
this.log('发现window.external尝试百度专用方法', 'info');
const baiduMethods = ['GetUserMedia', 'getUserMedia', 'requestUserMedia'];
for (let method of baiduMethods) {
if (window.external[method]) {
this.log(`尝试百度方法: external.${method}`, 'info');
try {
if (method === 'GetUserMedia') {
window.external.GetUserMedia(JSON.stringify(constraints), resolve, reject);
} else {
window.external[method](constraints, resolve, reject);
}
return;
} catch (e) {
this.log(`百度${method}调用失败: ${e.message}`, 'warning');
}
}
}
}
if (window.BaiduBrowser && window.BaiduBrowser.getUserMedia) {
this.log('使用window.BaiduBrowser.getUserMedia', 'info');
try {
window.BaiduBrowser.getUserMedia(constraints, resolve, reject);
return;
} catch (e) {
this.log('BaiduBrowser.getUserMedia失败尝试其他方法', 'warning');
}
}
}
// 方法5: UC浏览器特殊处理
if (isUC && window.ucweb && window.ucweb.getUserMedia) {
this.log('使用UC浏览器专用API', 'info');
try {
window.ucweb.getUserMedia(constraints, resolve, reject);
return;
} catch (e) {
this.log('UC浏览器API调用失败尝试其他方法', 'warning');
}
}
// 方法6: 通过iframe尝试
if (isBaidu || isUC || isQQ) {
this.log('尝试iframe兼容方案', 'info');
this.tryIFrameGetUserMedia(constraints, resolve, reject);
return;
}
// 方法7: 强制尝试webkitGetUserMedia (某些国产浏览器隐藏了标准检测)
this.log('尝试强制调用webkitGetUserMedia', 'info');
try {
if (navigator['webkitGetUserMedia']) {
navigator['webkitGetUserMedia'](constraints, resolve, reject);
return;
}
} catch (e) {
this.log('强制调用失败: ' + e.message, 'warning');
}
// 方法8: 最后尝试 - 检查是否有隐藏的方法
this.log('检查隐藏的getUserMedia方法...', 'info');
const possibleNames = [
'getUserMedia',
'webkitGetUserMedia',
'mozGetUserMedia',
'msGetUserMedia',
'oGetUserMedia',
'baiduGetUserMedia',
'ucGetUserMedia',
'qqGetUserMedia'
];
for (let name of possibleNames) {
if (navigator[name] && typeof navigator[name] === 'function') {
this.log(`发现隐藏方法: ${name}`, 'success');
try {
navigator[name](constraints, resolve, reject);
return;
} catch (e) {
this.log(`${name}调用失败: ${e.message}`, 'warning');
}
}
}
// 如果所有方法都失败
reject(new Error('此浏览器不支持摄像头访问功能建议换用Chrome、Firefox或Safari浏览器'));
});
};
this.log('✅ 已启用多重getUserMedia兼容模式', 'success');
}
// 检查并提示安全环境问题
if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
this.log('⚠️ 检测到非安全环境,摄像头功能可能受限', 'warning');
if (isBaidu) {
this.log('💡 百度浏览器提示:在地址栏输入"baidu://settings/privacy"调整权限设置', 'info');
}
}
// 特殊浏览器额外设置
if (isBaidu) {
this.setupBaiduBrowserSupport();
} else if (isUC) {
this.setupUCBrowserSupport();
} else if (isQQ) {
this.setupQQBrowserSupport();
}
}
// 百度浏览器特殊支持
setupBaiduBrowserSupport() {
this.log('🎯 配置百度浏览器特殊支持...', 'info');
// 尝试注入百度浏览器特殊方法
if (!window.BaiduBrowser) {
window.BaiduBrowser = {};
}
// 检查百度浏览器的特殊对象
if (window.external && window.external.GetUserMedia) {
this.log('发现百度浏览器external.GetUserMedia', 'success');
window.BaiduBrowser.getUserMedia = function (constraints, success, error) {
try {
window.external.GetUserMedia(JSON.stringify(constraints), success, error);
} catch (e) {
error(e);
}
};
}
this.log('✅ 百度浏览器支持配置完成', 'success');
}
// UC浏览器特殊支持
setupUCBrowserSupport() {
this.log('🎯 配置UC浏览器特殊支持...', 'info');
if (!window.ucweb) {
window.ucweb = {};
}
// UC浏览器可能使用不同的API路径
if (window.ucapi && window.ucapi.getUserMedia) {
window.ucweb.getUserMedia = window.ucapi.getUserMedia;
this.log('发现UC浏览器ucapi.getUserMedia', 'success');
}
this.log('✅ UC浏览器支持配置完成', 'success');
}
// QQ浏览器特殊支持
setupQQBrowserSupport() {
this.log('🎯 配置QQ浏览器特殊支持...', 'info');
// QQ浏览器通常基于Chromium但可能有特殊的API
if (window.qqapi && window.qqapi.getUserMedia) {
this.log('发现QQ浏览器专用API', 'success');
}
this.log('✅ QQ浏览器支持配置完成', 'success');
}
// iframe兼容方案用于某些受限环境
tryIFrameGetUserMedia(constraints, resolve, reject) {
this.log('尝试iframe兼容方案...', 'info');
try {
// 创建一个隐藏的iframe尝试获取权限
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = 'about:blank';
document.body.appendChild(iframe);
const iframeWindow = iframe.contentWindow;
if (iframeWindow && iframeWindow.navigator && iframeWindow.navigator.webkitGetUserMedia) {
this.log('通过iframe获取摄像头权限', 'info');
iframeWindow.navigator.webkitGetUserMedia(constraints,
(stream) => {
document.body.removeChild(iframe);
resolve(stream);
},
(error) => {
document.body.removeChild(iframe);
reject(error);
}
);
return;
}
document.body.removeChild(iframe);
} catch (e) {
this.log('iframe方案失败: ' + e.message, 'warning');
}
reject(new Error('iframe兼容方案也无法获取摄像头权限'));
}
async checkCameraPermission() {
try {
if (!navigator.permissions) {
this.log('浏览器不支持权限查询API', 'info');
return 'unknown';
}
const result = await navigator.permissions.query({ name: 'camera' });
this.log(`摄像头权限状态: ${result.state}`, 'info');
// 监听权限状态变化
result.addEventListener('change', () => {
this.log(`摄像头权限状态已更改为: ${result.state}`, 'info');
if (result.state === 'denied') {
this.log('摄像头权限被拒绝,请在浏览器设置中重新允许', 'error');
}
});
return result.state; // 'granted', 'denied', 'prompt'
} catch (error) {
this.log(`权限检查失败: ${error.message}`, 'error');
return 'unknown';
}
}
async initCamera() {
try {
// 检查浏览器是否支持摄像头访问
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error('浏览器不支持摄像头访问请使用现代浏览器Chrome/Firefox/Safari');
}
// 先检查权限状态
const permissionState = await this.checkCameraPermission();
if (permissionState === 'denied') {
throw new Error('摄像头权限已被拒绝,请在浏览器设置中重新允许后刷新页面');
}
const constraints = {
video: {
facingMode: 'environment',
width: { ideal: 640 },
height: { ideal: 480 },
// 添加帧率限制以提高性能
frameRate: { ideal: 30, max: 30 }
},
audio: false
};
this.log('正在请求摄像头权限...', 'info');
const stream = await navigator.mediaDevices.getUserMedia(constraints);
this.videoElement.srcObject = stream;
// 等待视频元素准备就绪
await new Promise((resolve) => {
this.videoElement.onloadedmetadata = resolve;
});
this.log('摄像头初始化成功', 'success');
} catch (error) {
let errorMsg = error.message;
// 根据错误类型提供更友好的提示
if (error.name === 'NotAllowedError') {
errorMsg = '摄像头权限被拒绝,请允许访问摄像头后刷新页面';
} else if (error.name === 'NotFoundError') {
errorMsg = '未找到可用的摄像头设备,请检查设备连接';
} else if (error.name === 'NotSupportedError') {
errorMsg = '浏览器不支持摄像头功能,请更新浏览器版本';
} else if (error.name === 'NotReadableError') {
errorMsg = '摄像头被其他应用占用,请关闭其他使用摄像头的应用';
} else if (error.name === 'OverconstrainedError') {
errorMsg = '摄像头不支持请求的配置,尝试使用默认设置';
} else if (error.name === 'SecurityError') {
errorMsg = '安全限制请确保在HTTPS环境下访问或使用localhost';
}
this.log(`摄像头初始化失败: ${errorMsg}`, 'error');
// 如果是权限问题,提供解决建议
if (error.name === 'NotAllowedError') {
this.log('💡 解决方案:', 'info');
this.log('1. 点击浏览器地址栏的摄像头图标或锁图标', 'info');
this.log('2. 选择"允许"摄像头权限', 'info');
this.log('3. 刷新页面重试', 'info');
}
}
}
startLocationTracking() {
console.log("=== GPS调试开始 ===");
console.log("🔍 浏览器信息:", navigator.userAgent);
console.log("🔍 当前URL:", window.location.href);
console.log("🔍 协议:", window.location.protocol);
console.log("🔍 是否为安全环境:", location.protocol === 'https:' || location.hostname === 'localhost');
this.log('🛰️ 开始GPS位置跟踪...', 'info');
// 检查基础支持
console.log("🔍 navigator对象:", navigator);
console.log("🔍 'geolocation' in navigator:", ('geolocation' in navigator));
console.log("🔍 navigator.geolocation:", navigator.geolocation);
if (!('geolocation' in navigator)) {
this.log('❌ 设备不支持GPS定位', 'error');
console.log("❌ GPS调试: 设备不支持geolocation");
document.getElementById('gpsStatus').textContent = '不支持';
return;
}
// 🚀 增强的GPS配置
const options = {
enableHighAccuracy: true, // 启用高精度
timeout: 30000, // 延长超时到30秒
maximumAge: 5000 // 缓存5秒
};
// 先检查权限状态
console.log("🔍 'permissions' in navigator:", ('permissions' in navigator));
if ('permissions' in navigator) {
console.log("🔍 开始权限状态查询...");
navigator.permissions.query({ name: 'geolocation' }).then((result) => {
console.log("🔍 权限查询结果:", result);
console.log("🔍 权限状态:", result.state);
this.log(`📍 GPS权限状态: ${result.state}`, 'info');
if (result.state === 'denied') {
console.log("❌ GPS权限被拒绝");
this.showGPSHelp();
} else if (result.state === 'granted') {
console.log("✅ GPS权限已授予");
} else {
console.log("⚠️ GPS权限状态为prompt需要用户授权");
}
}).catch((error) => {
console.log("❌ 权限查询失败:", error);
this.log('权限检查失败,继续尝试获取位置', 'warning');
});
} else {
console.log("⚠️ 浏览器不支持permissions API");
}
this.log('📱 正在请求GPS位置权限...', 'info');
this.log('💡 请在浏览器弹窗中点击"允许"', 'info');
console.log("🔍 GPS选项配置:", options);
console.log("🔍 开始getCurrentPosition调用...");
// 🚀 使用getCurrentPosition先获取一次位置再启动watchPosition
try {
navigator.geolocation.getCurrentPosition(
(position) => {
console.log("✅ getCurrentPosition成功回调");
console.log("🔍 位置数据:", position);
this.handleGPSSuccess(position);
// 成功后启动持续监听
this.startGPSWatching(options);
},
(error) => {
console.log("❌ getCurrentPosition错误回调");
console.log("🔍 错误详情:", error);
console.log("🔍 错误代码:", error.code);
console.log("🔍 错误信息:", error.message);
this.handleGPSError(error);
// 即使首次失败,也尝试启动监听
this.startGPSWatching(options);
},
options
);
console.log("🔍 getCurrentPosition调用已发出等待回调...");
} catch (e) {
console.log("❌ getCurrentPosition调用异常:", e);
this.log(`❌ GPS调用异常: ${e.message}`, 'error');
}
}
startGPSWatching(options) {
console.log("🔍 开始GPS监听 (watchPosition)...");
try {
this.gpsWatchId = navigator.geolocation.watchPosition(
(position) => {
console.log("✅ watchPosition成功回调");
console.log("🔍 监听位置数据:", position);
this.handleGPSSuccess(position);
},
(error) => {
console.log("❌ watchPosition错误回调");
console.log("🔍 监听错误详情:", error);
this.handleGPSError(error);
},
options
);
console.log("🔍 watchPosition ID:", this.gpsWatchId);
this.log(`🔍 GPS监听已启动ID: ${this.gpsWatchId}`, 'info');
} catch (e) {
console.log("❌ watchPosition调用异常:", e);
this.log(`❌ GPS监听异常: ${e.message}`, 'error');
}
}
handleGPSSuccess(position) {
console.log("=== GPS成功处理开始 ===");
console.log("🔍 完整position对象:", position);
console.log("🔍 coords对象:", position.coords);
console.log("🔍 纬度:", position.coords.latitude);
console.log("🔍 经度:", position.coords.longitude);
console.log("🔍 精度:", position.coords.accuracy);
console.log("🔍 时间戳:", position.timestamp);
this.currentPosition = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
timestamp: Date.now()
};
console.log("🔍 设置的currentPosition:", this.currentPosition);
const gpsText = `${position.coords.latitude.toFixed(6)}, ${position.coords.longitude.toFixed(6)}`;
const accuracyText = `(精度: ±${position.coords.accuracy.toFixed(0)}m)`;
const gpsStatusElement = document.getElementById('gpsStatus');
console.log("🔍 gpsStatus元素:", gpsStatusElement);
if (gpsStatusElement) {
gpsStatusElement.textContent = gpsText;
console.log("✅ GPS状态已更新到UI");
} else {
console.log("❌ 找不到gpsStatus元素");
}
this.log(`🛰️ GPS获取成功: ${gpsText} ${accuracyText}`, 'success');
console.log("=== GPS成功处理结束 ===");
// 🌟 实时传输GPS数据到服务器
this.sendLocationToServer(this.currentPosition);
// 如果精度较差,提供改善建议
if (position.coords.accuracy > 100) {
this.log(`⚠️ GPS精度较差 (±${position.coords.accuracy.toFixed(0)}m),建议移至室外`, 'warning');
} else if (position.coords.accuracy > 50) {
this.log(`📍 GPS精度一般 (±${position.coords.accuracy.toFixed(0)}m)`, 'warning');
} else {
this.log(`🎯 GPS精度良好 (±${position.coords.accuracy.toFixed(0)}m)`, 'success');
}
}
handleGPSError(error) {
console.log("=== GPS错误处理开始 ===");
console.log("🔍 完整error对象:", error);
console.log("🔍 错误代码:", error.code);
console.log("🔍 错误信息:", error.message);
console.log("🔍 错误常量对比:");
console.log(" - PERMISSION_DENIED:", error.PERMISSION_DENIED);
console.log(" - POSITION_UNAVAILABLE:", error.POSITION_UNAVAILABLE);
console.log(" - TIMEOUT:", error.TIMEOUT);
let errorMsg = '';
let solution = '';
switch (error.code) {
case error.PERMISSION_DENIED:
errorMsg = '用户拒绝了位置访问请求';
solution = '请在浏览器设置中允许位置权限';
console.log("🔍 错误类型: 权限被拒绝");
break;
case error.POSITION_UNAVAILABLE:
errorMsg = '位置信息不可用';
solution = '请确保设备GPS已开启并移至窗边或室外';
console.log("🔍 错误类型: 位置不可用");
break;
case error.TIMEOUT:
errorMsg = '位置获取超时';
solution = '请移至信号良好的地方重试';
console.log("🔍 错误类型: 超时");
break;
default:
errorMsg = '未知位置错误';
solution = '请检查设备设置或重启应用';
console.log("🔍 错误类型: 未知");
break;
}
console.log("🔍 错误描述:", errorMsg);
console.log("🔍 建议解决方案:", solution);
this.log(`❌ GPS获取失败: ${errorMsg}`, 'error');
this.log(`💡 解决方案: ${solution}`, 'info');
const gpsStatusElement = document.getElementById('gpsStatus');
if (gpsStatusElement) {
gpsStatusElement.textContent = '获取失败';
console.log("✅ GPS错误状态已更新到UI");
} else {
console.log("❌ 找不到gpsStatus元素");
}
if (error.code === error.PERMISSION_DENIED) {
console.log("🔍 开始显示GPS权限帮助");
this.showGPSHelp();
}
console.log("=== GPS错误处理结束 ===");
}
// 🌟 启动设备朝向追踪
startOrientationTracking() {
this.log('🧭 开始设备朝向跟踪...', 'info');
// 检查设备朝向支持
if (typeof DeviceOrientationEvent === 'undefined') {
this.log('❌ 设备不支持朝向检测', 'warning');
return;
}
// 检查是否需要权限请求
if (typeof DeviceOrientationEvent.requestPermission === 'function') {
// iOS 13+ 需要权限
DeviceOrientationEvent.requestPermission()
.then(response => {
if (response === 'granted') {
this.log('✅ 朝向权限已授予', 'success');
this.setupOrientationListener();
} else {
this.log('❌ 朝向权限被拒绝', 'error');
}
})
.catch(error => {
this.log('❌ 朝向权限请求失败: ' + error.message, 'error');
});
} else {
// 其他平台直接监听
this.setupOrientationListener();
}
}
// 设置朝向监听器
setupOrientationListener() {
window.addEventListener('deviceorientation', (event) => {
this.handleOrientationChange(event);
}, true);
this.log('🧭 朝向监听器已启动', 'success');
}
// 处理朝向变化
handleOrientationChange(event) {
// 获取设备朝向数据
const alpha = event.alpha; // 围绕Z轴旋转0-360度
const beta = event.beta; // 围绕X轴旋转-180到180度
const gamma = event.gamma; // 围绕Y轴旋转-90到90度
if (alpha !== null && alpha !== undefined) {
this.currentOrientation = {
heading: alpha,
tilt: beta,
roll: gamma,
timestamp: Date.now()
};
// 更新UI显示
const orientationElement = document.getElementById('orientationStatus');
if (orientationElement) {
orientationElement.textContent = `${alpha.toFixed(1)}°`;
}
// 🌟 实时传输朝向数据到服务器
this.sendOrientationToServer(this.currentOrientation);
}
}
// 🌟 发送GPS位置数据到服务器
async sendLocationToServer(position) {
try {
// 节流限制发送频率每2秒最多一次
const now = Date.now();
if (now - this.lastDataSendTime < 2000) {
return;
}
this.lastDataSendTime = now;
const locationData = {
device_id: this.deviceId,
type: 'location',
data: {
latitude: position.latitude,
longitude: position.longitude,
accuracy: position.accuracy,
timestamp: position.timestamp
}
};
const response = await fetch('/api/mobile/realtime_data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(locationData)
});
if (response.ok) {
console.log('📍 GPS数据已发送到服务器');
console.log('📍 发送的GPS数据详情:', {
device_id: this.deviceId.substr(0, 8),
latitude: position.latitude,
longitude: position.longitude,
accuracy: position.accuracy
});
} else {
console.warn('⚠️ GPS数据发送失败:', response.status);
const errorText = await response.text();
console.warn('⚠️ 错误详情:', errorText);
}
} catch (error) {
console.error('❌ GPS数据发送错误:', error);
}
}
// 🌟 发送设备朝向数据到服务器
async sendOrientationToServer(orientation) {
try {
// 节流限制发送频率每1秒最多一次
const now = Date.now();
if (now - this.lastDataSendTime < 1000) {
return;
}
const orientationData = {
device_id: this.deviceId,
type: 'orientation',
data: {
heading: orientation.heading,
tilt: orientation.tilt,
roll: orientation.roll,
timestamp: orientation.timestamp
}
};
const response = await fetch('/api/mobile/realtime_data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(orientationData)
});
if (response.ok) {
console.log('🧭 朝向数据已发送到服务器');
console.log('🧭 发送的朝向数据详情:', {
device_id: this.deviceId.substr(0, 8),
heading: orientation.heading.toFixed(1),
tilt: orientation.tilt ? orientation.tilt.toFixed(1) : 'null',
roll: orientation.roll ? orientation.roll.toFixed(1) : 'null'
});
} else {
console.warn('⚠️ 朝向数据发送失败:', response.status);
const errorText = await response.text();
console.warn('⚠️ 错误详情:', errorText);
}
} catch (error) {
console.error('❌ 朝向数据发送错误:', error);
}
}
showGPSHelp() {
this.log('🔧 GPS权限设置指南:', 'info');
this.log('1. 点击浏览器地址栏的🔒图标', 'info');
this.log('2. 找到"位置"或"Location"选项', 'info');
this.log('3. 选择"允许"或"Allow"', 'info');
this.log('4. 刷新页面重试', 'info');
this.log('5. 确保设备GPS已开启', 'info');
// 🚀 添加手动位置输入选项
setTimeout(() => {
this.log('📍 或者点击下方按钮手动输入位置', 'info');
this.showManualLocationInput();
}, 2000);
}
showManualLocationInput() {
if (document.getElementById('manualLocationBtn')) {
return; // 按钮已存在
}
const settingsPanel = document.querySelector('.settings');
const manualLocationDiv = document.createElement('div');
manualLocationDiv.style.marginTop = '15px';
manualLocationDiv.style.padding = '10px';
manualLocationDiv.style.background = 'rgba(255, 193, 7, 0.1)';
manualLocationDiv.style.border = '1px solid #ffc107';
manualLocationDiv.style.borderRadius = '8px';
manualLocationDiv.innerHTML = `
<h4 style="margin: 0 0 10px 0; color: #ffc107;">📍 手动位置输入</h4>
<div style="margin-bottom: 10px;">
<input type="number" id="manualLat" placeholder="纬度 (如: 39.9042)" step="any"
style="width: 100%; padding: 8px; margin-bottom: 5px; border: 1px solid #ccc; border-radius: 4px;">
<input type="number" id="manualLng" placeholder="经度 (如: 116.4074)" step="any"
style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;">
</div>
<button id="manualLocationBtn" class="btn" style="width: 100%; background: #ffc107; color: #000; border: none; padding: 10px; border-radius: 4px;">
📍 使用此位置
</button>
<div style="font-size: 12px; color: #666; margin-top: 8px;">
💡 可通过地图应用获取当前位置的经纬度
</div>
`;
settingsPanel.appendChild(manualLocationDiv);
// 绑定按钮事件
document.getElementById('manualLocationBtn').onclick = () => {
this.setManualLocation();
};
}
setManualLocation() {
const lat = parseFloat(document.getElementById('manualLat').value);
const lng = parseFloat(document.getElementById('manualLng').value);
if (isNaN(lat) || isNaN(lng)) {
this.log('❌ 请输入有效的经纬度数值', 'error');
return;
}
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
this.log('❌ 经纬度范围不正确 (纬度: -90~90, 经度: -180~180)', 'error');
return;
}
this.currentPosition = {
latitude: lat,
longitude: lng,
accuracy: 1000, // 手动输入精度设为1000米
timestamp: Date.now(),
manual: true
};
const gpsText = `${lat.toFixed(6)}, ${lng.toFixed(6)}`;
document.getElementById('gpsStatus').textContent = gpsText;
this.log(`📍 手动位置设置成功: ${gpsText} (手动输入)`, 'success');
// 隐藏手动输入面板
const manualDiv = document.getElementById('manualLocationBtn').parentElement;
if (manualDiv) {
manualDiv.style.display = 'none';
}
}
async updateBatteryStatus() {
try {
if ('getBattery' in navigator) {
const battery = await navigator.getBattery();
const level = Math.round(battery.level * 100);
document.getElementById('batteryStatus').textContent = `${level}%`;
battery.addEventListener('levelchange', () => {
const level = Math.round(battery.level * 100);
document.getElementById('batteryStatus').textContent = `${level}%`;
});
} else {
document.getElementById('batteryStatus').textContent = '不支持';
}
} catch (error) {
document.getElementById('batteryStatus').textContent = '获取失败';
}
}
bindEvents() {
// 设备选择按钮已经在HTML中直接绑定了onclick事件
document.getElementById('startBtn').addEventListener('click', () => this.startStreaming());
document.getElementById('stopBtn').addEventListener('click', () => this.stopStreaming());
document.getElementById('settingsBtn').addEventListener('click', () => {
this.toggleSettings();
});
document.getElementById('reconnectBtn').addEventListener('click', () => {
this.reconnect();
});
document.getElementById('diagnosisBtn').addEventListener('click', () => {
this.systemDiagnosis();
});
}
toggleSettings() {
const panel = document.getElementById('settingsPanel');
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
}
async testConnection() {
try {
const response = await fetch(`${this.baseURL}/mobile/ping`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device_id: this.deviceId })
});
if (response.ok) {
this.log(`服务器连接正常 ${this.baseURL}`, 'success');
this.updateConnectionStatus(true);
return true;
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
this.log(`连接失败: ${error.message}`, 'error');
this.updateConnectionStatus(false);
return false;
}
}
async startStreaming() {
try {
// 如果摄像头尚未初始化,先初始化摄像头
if (!this.currentStream) {
await this.initMobileCamera();
}
const connected = await this.testConnection();
if (!connected) {
throw new Error('无法连接到服务器');
}
this.isStreaming = true;
this.updateButtons();
this.log('开始视频传输到服务器', 'success');
this.sendDataLoop();
} catch (error) {
this.log(`启动失败: ${error.message}`, 'error');
this.isStreaming = false;
this.updateButtons();
}
}
stopStreaming() {
this.isStreaming = false;
this.updateButtons();
this.updateConnectionStatus(false);
this.log('停止视频传输', 'info');
}
async sendDataLoop() {
// 🚀 优化使用UI设置的帧率
const frameRate = parseInt(document.getElementById('frameRate').value) || 2;
const interval = 1000 / frameRate; // 根据设置的FPS计算间隔
this.log(`📺 视频传输已优化: ${frameRate} FPS (间隔 ${interval}ms)`, 'success');
while (this.isStreaming) {
try {
await this.captureAndSend();
await new Promise(resolve => setTimeout(resolve, interval));
} catch (error) {
this.log(`发送失败: ${error.message}`, 'error');
this.updateConnectionStatus(false);
await new Promise(resolve => setTimeout(resolve, 5000)); // 等待5秒重试
}
}
}
async captureAndSend() {
this.canvas.width = this.videoElement.videoWidth || 640;
this.canvas.height = this.videoElement.videoHeight || 480;
this.ctx.drawImage(this.videoElement, 0, 0);
// 🚀 优化使用UI设置的图像质量
const imageQuality = parseFloat(document.getElementById('imageQuality').value) || 0.7;
const frameData = this.canvas.toDataURL('image/jpeg', imageQuality).split(',')[1];
const data = {
device_id: this.deviceId,
device_name: navigator.userAgent.includes('iPhone') ? 'iPhone' :
navigator.userAgent.includes('Android') ? 'Android' : 'Mobile',
timestamp: Date.now(),
frame: frameData,
gps: this.currentPosition,
battery: await this.getBatteryLevel(),
camera_info: {
width: this.canvas.width,
height: this.canvas.height,
quality: imageQuality,
// 🚀 添加压缩信息
original_size: frameData.length,
fps: frameRate
}
};
const response = await fetch(`${this.baseURL}/mobile/upload`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// 🚀 添加压缩头部
'Accept-Encoding': 'gzip, deflate',
'Cache-Control': 'no-cache'
},
body: JSON.stringify(data)
});
if (response.ok) {
this.frameCount++;
this.dataAmount += JSON.stringify(data).length;
// 🚀 性能监控和自适应优化
const currentTime = Date.now();
const uploadTime = currentTime - this.lastSendTime;
this.lastSendTime = currentTime;
if (uploadTime > 0) {
this.performanceStats.push(uploadTime);
if (this.performanceStats.length > 10) {
this.performanceStats.shift(); // 保持最近10次记录
}
this.averageUploadTime = this.performanceStats.reduce((a, b) => a + b, 0) / this.performanceStats.length;
// 自适应性能提示
if (this.averageUploadTime > 2000) {
this.log(`⚠️ 网络较慢 (平均${this.averageUploadTime.toFixed(0)}ms),建议降低帧率或图像质量`, 'warning');
} else if (this.averageUploadTime < 200) {
this.log(`🚀 网络良好 (平均${this.averageUploadTime.toFixed(0)}ms),可提高画质`, 'success');
}
}
this.updateStats();
this.updateConnectionStatus(true);
} else {
throw new Error(`HTTP ${response.status}`);
}
}
async getBatteryLevel() {
try {
if ('getBattery' in navigator) {
const battery = await navigator.getBattery();
return Math.round(battery.level * 100);
}
} catch (error) { }
return 100;
}
// 文件捕获方法(用于兼容性极差的浏览器)
tryFileCapture() {
this.log('📷 尝试文件捕获方法...', 'info');
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*,video/*';
input.capture = 'camera'; // 直接启动摄像头
input.style.position = 'absolute';
input.style.top = '-1000px';
document.body.appendChild(input);
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
this.log(`✅ 获取文件: ${file.name} (${(file.size / 1024).toFixed(1)}KB)`, 'success');
if (file.type.startsWith('image/')) {
// 处理图片
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
// 将图片绘制到canvas
this.canvas.width = img.width;
this.canvas.height = img.height;
this.ctx.drawImage(img, 0, 0);
// 显示在video元素上
const dataURL = this.canvas.toDataURL('image/jpeg', 0.8);
this.videoElement.style.backgroundImage = `url(${dataURL})`;
this.videoElement.style.backgroundSize = 'contain';
this.videoElement.style.backgroundRepeat = 'no-repeat';
this.videoElement.style.backgroundPosition = 'center';
this.log('✅ 图片已显示,可以开始传输', 'success');
// 启用开始按钮
document.getElementById('startBtn').disabled = false;
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
} else if (file.type.startsWith('video/')) {
// 处理视频
const url = URL.createObjectURL(file);
this.videoElement.src = url;
this.videoElement.style.backgroundImage = 'none';
this.videoElement.onloadeddata = () => {
this.log('✅ 视频已加载,可以开始传输', 'success');
document.getElementById('startBtn').disabled = false;
};
}
}
document.body.removeChild(input);
};
input.onerror = () => {
this.log('❌ 文件选择失败', 'error');
document.body.removeChild(input);
};
// 触发文件选择
input.click();
this.log('📱 已触发文件选择(在某些浏览器中会直接启动摄像头)', 'info');
}
// 实时视频流捕获方法
async tryRealTimeCapture() {
this.log('🚀 启动底层实时摄像头系统...', 'info');
try {
// 首先尝试底层API
const success = await this.initRealTimeCamera();
if (success) {
return;
}
} catch (error) {
this.log(`底层API错误: ${error.message}`, 'warning');
}
// 如果失败,启动完全自定义的摄像头系统
this.log('🔧 启动自定义摄像头系统...', 'warning');
this.initCustomCameraSystem();
}
// 底层实时摄像头API
async initRealTimeCamera() {
this.log('🎥 初始化底层摄像头API...', 'info');
// 创建底层媒体约束
const constraints = {
video: {
facingMode: { ideal: 'environment' },
width: { min: 320, ideal: 640, max: 1280 },
height: { min: 240, ideal: 480, max: 720 },
frameRate: { min: 15, ideal: 30, max: 60 }
},
audio: false
};
try {
// 底层获取媒体流
const stream = await this.acquireMediaStream(constraints);
if (stream) {
await this.setupRealTimeVideo(stream);
this.startRealTimeProcessing();
return true;
}
} catch (error) {
this.log(`底层摄像头API失败: ${error.message}`, 'error');
return false;
}
return false;
}
async acquireMediaStream(constraints) {
this.log('📡 获取底层媒体流...', 'info');
// 方法1: 现代MediaDevices API
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
try {
this.log('🔄 使用MediaDevices.getUserMedia()', 'info');
const stream = await navigator.mediaDevices.getUserMedia(constraints);
this.log('✅ MediaDevices API成功', 'success');
return stream;
} catch (error) {
this.log(`MediaDevices失败: ${error.name} - ${error.message}`, 'warning');
}
}
// 方法2: 传统getUserMedia with Promises
const legacyGetUserMedia = navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia;
if (legacyGetUserMedia) {
try {
this.log('🔄 使用传统getUserMedia', 'info');
return await new Promise((resolve, reject) => {
legacyGetUserMedia.call(navigator, constraints, resolve, reject);
});
} catch (error) {
this.log(`传统API失败: ${error.message}`, 'warning');
}
}
throw new Error('无可用的媒体流API');
}
async setupRealTimeVideo(stream) {
this.log('🎬 设置实时视频显示...', 'info');
// 设置视频元素
this.videoElement.srcObject = stream;
this.videoElement.style.display = 'block';
this.videoElement.autoplay = true;
this.videoElement.playsInline = true;
this.videoElement.muted = true;
// 等待视频元数据加载
await new Promise((resolve) => {
this.videoElement.onloadedmetadata = () => {
this.log(`📺 视频流规格: ${this.videoElement.videoWidth}x${this.videoElement.videoHeight}`, 'success');
resolve();
};
});
// 播放视频
await this.videoElement.play();
// 设置canvas用于帧处理
this.canvas.width = this.videoElement.videoWidth;
this.canvas.height = this.videoElement.videoHeight;
this.currentStream = stream;
this.isRealTimeActive = true;
// 显示控制按钮
document.getElementById('stopStreamBtn').style.display = 'inline-block';
document.getElementById('cameraInfoBtn').style.display = 'inline-block';
document.getElementById('qualityBtn').style.display = 'inline-block';
document.getElementById('startBtn').disabled = false;
document.getElementById('videoPlaceholder').style.display = 'none';
this.log('✅ 实时视频流已启动', 'success');
}
startRealTimeProcessing() {
this.log('⚡ 启动实时帧处理引擎...', 'success');
let frameCount = 0;
let lastTime = performance.now();
let fps = 0;
const processFrame = (currentTime) => {
if (!this.isRealTimeActive || !this.currentStream) {
return;
}
// 计算FPS
const deltaTime = currentTime - lastTime;
if (deltaTime >= 1000) { // 每秒更新一次FPS
fps = Math.round((frameCount * 1000) / deltaTime);
this.log(`📊 实时处理: ${fps} FPS, 帧数: ${frameCount}`, 'info');
frameCount = 0;
lastTime = currentTime;
}
// 绘制当前帧到canvas
this.ctx.drawImage(this.videoElement, 0, 0, this.canvas.width, this.canvas.height);
frameCount++;
// 请求下一帧
requestAnimationFrame(processFrame);
};
// 开始处理循环
requestAnimationFrame(processFrame);
}
async initCustomCameraSystem() {
this.log('🎨 初始化自定义摄像头系统...', 'info');
// 第一步:尝试视频文件捕获
if (await this.tryCustomVideoCapture()) {
return;
}
// 第二步创建WebGL模拟摄像头
if (this.createWebGLCamera()) {
return;
}
// 第三步创建Canvas模拟摄像头
this.createCanvasCamera();
}
async tryCustomVideoCapture() {
this.log('📹 尝试自定义视频捕获...', 'info');
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'video/*';
input.capture = 'camcorder';
input.style.display = 'none';
let resolved = false;
input.onchange = (e) => {
const file = e.target.files[0];
if (file && file.type.startsWith('video/')) {
this.log('✅ 视频文件捕获成功!', 'success');
this.setupCustomVideoPlayer(file);
resolved = true;
resolve(true);
}
document.body.removeChild(input);
if (!resolved) resolve(false);
};
input.onerror = () => {
document.body.removeChild(input);
if (!resolved) resolve(false);
};
document.body.appendChild(input);
// 自动触发文件选择
setTimeout(() => {
input.click();
// 如果5秒内没有响应认为失败
setTimeout(() => {
if (!resolved) {
if (input.parentNode) document.body.removeChild(input);
resolve(false);
}
}, 5000);
}, 100);
});
}
setupCustomVideoPlayer(videoFile) {
this.log('🎬 设置自定义视频播放器...', 'info');
const url = URL.createObjectURL(videoFile);
this.videoElement.src = url;
this.videoElement.style.display = 'block';
this.videoElement.autoplay = true;
this.videoElement.loop = true;
this.videoElement.muted = true;
this.videoElement.controls = true;
document.getElementById('videoPlaceholder').style.display = 'none';
this.videoElement.onloadedmetadata = () => {
this.log(`📺 自定义视频: ${this.videoElement.videoWidth}x${this.videoElement.videoHeight}`, 'success');
this.startCustomVideoProcessing();
this.showCustomControls();
};
this.customVideoURL = url;
}
startCustomVideoProcessing() {
this.log('⚡ 启动自定义视频处理...', 'success');
this.canvas.width = this.videoElement.videoWidth || 640;
this.canvas.height = this.videoElement.videoHeight || 480;
let frameCount = 0;
this.isCustomProcessing = true;
const processFrame = () => {
if (!this.isCustomProcessing || this.videoElement.paused || this.videoElement.ended) {
return;
}
// 绘制当前帧
this.ctx.drawImage(this.videoElement, 0, 0, this.canvas.width, this.canvas.height);
frameCount++;
if (frameCount % 30 === 0) {
this.log(`🎬 自定义处理: 帧 ${frameCount}`, 'info');
}
requestAnimationFrame(processFrame);
};
processFrame();
}
createWebGLCamera() {
this.log('🎮 创建WebGL模拟摄像头...', 'info');
try {
const canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 480;
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) {
throw new Error('WebGL不可用');
}
this.log('✅ WebGL上下文已创建', 'success');
// 插入到页面
this.videoElement.style.display = 'none';
canvas.style.width = '100%';
canvas.style.maxWidth = '640px';
canvas.style.border = '2px solid #4CAF50';
const videoContainer = this.videoElement.parentNode;
videoContainer.appendChild(canvas);
document.getElementById('videoPlaceholder').style.display = 'none';
this.log('🎮 WebGL摄像头已创建', 'success');
// 启动WebGL渲染
this.startWebGLRendering(gl, canvas);
this.showCustomControls();
this.webglCanvas = canvas;
return true;
} catch (error) {
this.log(`WebGL摄像头失败: ${error.message}`, 'warning');
return false;
}
}
startWebGLRendering(gl, canvas) {
this.log('⚡ 启动WebGL渲染...', 'success');
let frameCount = 0;
this.isWebGLActive = true;
const render = () => {
if (!this.isWebGLActive) return;
const time = Date.now() * 0.001;
frameCount++;
// 渐变背景效果
gl.clearColor(
0.2 + 0.3 * Math.sin(time * 0.5),
0.3 + 0.3 * Math.cos(time * 0.7),
0.6 + 0.4 * Math.sin(time * 0.3),
1.0
);
gl.clear(gl.COLOR_BUFFER_BIT);
if (frameCount % 60 === 0) {
this.log(`🎮 WebGL渲染: ${frameCount}`, 'info');
}
requestAnimationFrame(render);
};
render();
}
createCanvasCamera() {
this.log('🎨 创建Canvas模拟摄像头...', 'info');
// 使用现有的canvas作为模拟摄像头
this.canvas.width = 640;
this.canvas.height = 480;
this.canvas.style.width = '100%';
this.canvas.style.maxWidth = '640px';
this.canvas.style.border = '2px solid #2196F3';
this.canvas.style.background = '#000';
this.canvas.style.display = 'block';
const videoContainer = this.videoElement.parentNode;
videoContainer.appendChild(this.canvas);
this.videoElement.style.display = 'none';
document.getElementById('videoPlaceholder').style.display = 'none';
this.log('✅ Canvas摄像头已创建', 'success');
// 启动Canvas渲染
this.startCanvasRendering();
this.showCustomControls();
}
startCanvasRendering() {
this.log('🎯 启动Canvas渲染引擎...', 'success');
let frameCount = 0;
this.isCanvasActive = true;
const render = () => {
if (!this.isCanvasActive) return;
const time = Date.now() * 0.001;
frameCount++;
// 清空画布
this.ctx.fillStyle = '#000';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 绘制模拟摄像头画面
this.ctx.fillStyle = '#333';
this.ctx.fillRect(50, 50, this.canvas.width - 100, this.canvas.height - 100);
// 动态元素
this.ctx.fillStyle = `hsl(${(time * 50) % 360}, 70%, 50%)`;
this.ctx.beginPath();
this.ctx.arc(
this.canvas.width / 2 + 100 * Math.cos(time),
this.canvas.height / 2 + 50 * Math.sin(time * 2),
30,
0,
Math.PI * 2
);
this.ctx.fill();
// 显示信息
this.ctx.fillStyle = '#fff';
this.ctx.font = '16px Arial';
this.ctx.fillText(`模拟摄像头 - 帧: ${frameCount}`, 20, 30);
this.ctx.fillText(`设备: ${this.deviceId.substr(0, 12)}`, 20, 50);
this.ctx.fillText(`时间: ${new Date().toLocaleTimeString()}`, 20, 70);
if (frameCount % 60 === 0) {
this.log(`🎯 Canvas渲染: ${frameCount}`, 'info');
}
requestAnimationFrame(render);
};
render();
}
showCustomControls() {
// 显示自定义控制按钮
document.getElementById('stopStreamBtn').style.display = 'inline-block';
document.getElementById('startBtn').disabled = false;
this.log('🎛️ 自定义摄像头控制已启用', 'success');
}
// 更新停止方法以处理自定义摄像头
stopRealTimeCamera() {
this.log('🛑 停止所有摄像头系统...', 'info');
// 停止底层摄像头
this.isRealTimeActive = false;
if (this.currentStream) {
this.currentStream.getTracks().forEach(track => {
track.stop();
this.log(`⏹️ 停止轨道: ${track.kind} (${track.label})`, 'info');
});
this.currentStream = null;
}
// 停止自定义系统
this.isCustomProcessing = false;
this.isWebGLActive = false;
this.isCanvasActive = false;
// 清理自定义视频
if (this.customVideoURL) {
URL.revokeObjectURL(this.customVideoURL);
this.customVideoURL = null;
}
// 清理WebGL画布
if (this.webglCanvas && this.webglCanvas.parentNode) {
this.webglCanvas.parentNode.removeChild(this.webglCanvas);
this.webglCanvas = null;
}
// 清理Canvas
if (this.canvas.parentNode && this.canvas.parentNode !== document.body) {
this.canvas.parentNode.removeChild(this.canvas);
}
// 重置视频元素
this.videoElement.srcObject = null;
this.videoElement.src = '';
this.videoElement.style.display = 'none';
// 重置UI
document.getElementById('videoPlaceholder').style.display = 'block';
document.getElementById('stopStreamBtn').style.display = 'none';
document.getElementById('cameraInfoBtn').style.display = 'none';
document.getElementById('qualityBtn').style.display = 'none';
this.log('✅ 所有摄像头系统已停止', 'success');
}
// 获取摄像头能力信息
async getCameraCapabilities() {
if (!this.currentStream) {
this.log('❌ 无活跃视频流', 'warning');
return null;
}
const videoTrack = this.currentStream.getVideoTracks()[0];
if (!videoTrack) {
this.log('❌ 无视频轨道', 'warning');
return null;
}
try {
const capabilities = videoTrack.getCapabilities();
const settings = videoTrack.getSettings();
this.log('📋 摄像头能力信息:', 'info');
this.log(`- 分辨率: ${settings.width}x${settings.height}`, 'info');
this.log(`- 帧率: ${settings.frameRate} FPS`, 'info');
this.log(`- 设备ID: ${settings.deviceId}`, 'info');
this.log(`- 朝向: ${settings.facingMode || '未知'}`, 'info');
return { capabilities, settings };
} catch (error) {
this.log(`获取能力信息失败: ${error.message}`, 'warning');
return null;
}
}
// 动态调整视频质量
async adjustVideoQuality(width, height, frameRate) {
if (!this.currentStream) {
this.log('❌ 无活跃视频流可调整', 'warning');
return false;
}
const videoTrack = this.currentStream.getVideoTracks()[0];
if (!videoTrack) {
this.log('❌ 无视频轨道可调整', 'warning');
return false;
}
try {
await videoTrack.applyConstraints({
width: { ideal: width },
height: { ideal: height },
frameRate: { ideal: frameRate }
});
this.log(`✅ 视频质量已调整: ${width}x${height} @ ${frameRate}FPS`, 'success');
return true;
} catch (error) {
this.log(`调整质量失败: ${error.message}`, 'error');
return false;
}
}
// 重写原有的实时捕获方法
async tryRealTimeCapture() {
this.log('🚀 启动底层实时摄像头系统...', 'info');
try {
// 首先尝试底层API
const success = await this.initRealTimeCamera();
if (success) {
return;
}
} catch (error) {
this.log(`底层API错误: ${error.message}`, 'warning');
}
// 如果失败,启动完全自定义的摄像头系统
this.log('🔧 启动自定义摄像头系统...', 'warning');
this.initCustomCameraSystem();
}
stopVideoStream() {
// 调用底层停止方法
this.stopRealTimeCamera();
// 移除简单输入按钮(兼容旧模式)
if (this.removeSimpleInput) {
this.removeSimpleInput();
}
}
updateConnectionStatus(connected) {
const indicator = document.getElementById('connectionIndicator');
const status = document.getElementById('connectionStatus');
if (connected) {
indicator.classList.add('connected');
status.textContent = '在线';
status.style.color = '#4CAF50';
} else {
indicator.classList.remove('connected');
status.textContent = '离线';
status.style.color = '#f44336';
}
}
updateButtons() {
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
startBtn.disabled = this.isStreaming;
stopBtn.disabled = !this.isStreaming;
}
updateStats() {
document.getElementById('frameCount').textContent = this.frameCount;
document.getElementById('dataAmount').textContent = `${(this.dataAmount / 1024).toFixed(1)} KB`;
// 🚀 显示详细性能统计
if (this.averageUploadTime > 0) {
const performanceInfo = document.querySelector('.performance-info');
if (!performanceInfo) {
// 创建性能显示区域
const stats = document.querySelector('.stats');
const perfDiv = document.createElement('div');
perfDiv.className = 'performance-info';
perfDiv.style.marginTop = '10px';
perfDiv.style.fontSize = '12px';
perfDiv.style.color = '#666';
stats.appendChild(perfDiv);
}
const frameRate = parseInt(document.getElementById('frameRate').value) || 2;
const quality = parseInt(parseFloat(document.getElementById('imageQuality').value) * 100) || 70;
document.querySelector('.performance-info').innerHTML = `
<div>📊 当前设置: ${frameRate} FPS, ${quality}% 质量</div>
<div>⏱️ 平均延迟: ${this.averageUploadTime.toFixed(0)}ms</div>
<div>📈 网络状态: ${this.averageUploadTime < 500 ? '🟢 良好' : this.averageUploadTime < 2000 ? '🟡 一般' : '🔴 较慢'}</div>
`;
}
}
log(message, type = 'info') {
const logPanel = document.getElementById('logPanel');
const timestamp = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.className = `log-entry log-${type}`;
entry.textContent = `${timestamp} - ${message}`;
logPanel.appendChild(entry);
logPanel.scrollTop = logPanel.scrollHeight;
while (logPanel.children.length > 50) {
logPanel.removeChild(logPanel.firstChild);
}
}
async reconnect() {
this.log('正在重新连接...', 'info');
await this.testConnection();
// 同时检查权限状态
await this.checkAndRecoverPermissions();
}
async checkAndRecoverPermissions() {
this.log('正在检查权限状态...', 'info');
try {
// 检查摄像头权限
const cameraPermission = await this.checkCameraPermission();
if (cameraPermission === 'granted') {
this.log('✅ 摄像头权限正常', 'success');
// 如果当前没有活跃的视频流,尝试重新扫描设备
if (!this.currentStream && this.availableDevices.length === 0) {
await this.scanDevices();
}
} else if (cameraPermission === 'denied') {
this.log('❌ 摄像头权限被拒绝', 'error');
this.log('请在浏览器设置中重新允许摄像头权限', 'error');
} else {
this.log('⚠️ 摄像头权限状态未知,可能需要重新授权', 'info');
}
// 检查位置权限状态
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(
() => this.log('✅ GPS权限正常', 'success'),
(error) => {
if (error.code === error.PERMISSION_DENIED) {
this.log('❌ GPS权限被拒绝', 'error');
this.log('请在浏览器设置中重新允许位置权限', 'error');
}
},
{ timeout: 5000 }
);
}
} catch (error) {
this.log(`权限检查失败: ${error.message}`, 'error');
}
}
// ========== 设备管理方法 ==========
async switchToDevice(deviceId, deviceInfo) {
this.log(`正在切换到设备: ${deviceInfo.label || deviceInfo.name}`, 'info');
try {
// 🌟 设备选择时自动配置GPS和朝向
this.log('🤖 启动设备选择时的自动配置...', 'info');
await this.autoConfigureDeviceOnSelection();
// 停止当前流
if (this.currentStream) {
this.currentStream.getTracks().forEach(track => track.stop());
this.currentStream = null;
}
// 停止远程视频流
this.stopRemoteVideoStream();
if (deviceInfo.isRemote) {
// 远程设备
this.selectedDevice = {
deviceId: deviceId,
label: deviceInfo.name || deviceInfo.label,
isRemote: true
};
// 隐藏本地视频,启动远程视频流
this.videoElement.style.display = 'none';
document.getElementById('videoPlaceholder').style.display = 'block';
this.startRemoteVideoStream(deviceId);
} else {
// 本地设备
const constraints = {
video: {
deviceId: { exact: deviceId },
width: { ideal: 640 },
height: { ideal: 480 }
},
audio: false
};
this.currentStream = await navigator.mediaDevices.getUserMedia(constraints);
this.videoElement.srcObject = this.currentStream;
this.videoElement.style.display = 'block';
document.getElementById('videoPlaceholder').style.display = 'none';
this.selectedDevice = {
deviceId: deviceId,
label: deviceInfo.label,
isRemote: false
};
}
// 更新UI显示
document.getElementById('selectedDeviceInfo').textContent =
`${deviceInfo.isRemote ? '🌐' : '📱'} ${deviceInfo.label || deviceInfo.name}`;
this.log(`设备切换成功: ${this.selectedDevice.label}`, 'success');
} catch (error) {
this.log(`设备切换失败: ${error.message}`, 'error');
throw error;
}
}
// 🌟 移动端设备选择时自动配置GPS和朝向
async autoConfigureDeviceOnSelection() {
try {
this.log('🤖 启动移动端自动配置...', 'info');
// 1. 自动请求GPS权限
const gpsResult = await this.autoRequestGPSPermission();
if (!gpsResult.success) {
this.log('⚠️ GPS获取失败使用默认位置', 'warning');
}
// 2. 自动请求朝向权限
const orientationResult = await this.autoRequestOrientationPermission();
if (!orientationResult.success) {
this.log('⚠️ 朝向获取失败,使用默认朝向', 'warning');
}
// 3. 如果GPS和朝向都成功自动配置摄像头
if (gpsResult.success && orientationResult.success) {
this.log('📍 GPS和朝向都获取成功开始自动配置...', 'info');
await this.autoConfigureCamera(gpsResult.location, orientationResult.heading);
this.log('🎯 移动端位置和朝向自动配置完成!', 'success');
} else if (gpsResult.success) {
this.log('📍 仅GPS成功使用默认朝向配置...', 'info');
await this.autoConfigureCameraWithGPSOnly(gpsResult.location);
this.log('📍 移动端位置配置完成(使用默认朝向)', 'info');
}
} catch (error) {
this.log(`❌ 自动配置过程出错: ${error.message}`, 'error');
}
}
// 移动端自动请求GPS权限
async autoRequestGPSPermission() {
try {
this.log('📍 移动端自动请求GPS权限...', 'info');
if (!('geolocation' in navigator)) {
throw new Error('设备不支持GPS定位');
}
const options = {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 30000
};
const position = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(
resolve,
reject,
options
);
});
const location = {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy
};
// 更新移动端GPS状态显示
document.getElementById('gpsStatus').textContent =
`GPS: ${position.coords.latitude.toFixed(6)}, ${position.coords.longitude.toFixed(6)}${position.coords.accuracy.toFixed(0)}m)`;
this.log(`✅ 移动端GPS获取成功: ${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}`, 'success');
return { success: true, location: location };
} catch (error) {
this.log(`⚠️ 移动端GPS获取失败: ${error.message}`, 'warning');
return { success: false, error: error.message };
}
}
// 移动端自动请求朝向权限
async autoRequestOrientationPermission() {
try {
this.log('🧭 移动端自动请求朝向权限...', 'info');
// 检查设备朝向支持
if (!window.DeviceOrientationEvent) {
throw new Error('设备不支持朝向检测');
}
// 对于iOS 13+,需要请求权限
if (typeof DeviceOrientationEvent.requestPermission === 'function') {
const permission = await DeviceOrientationEvent.requestPermission();
if (permission !== 'granted') {
throw new Error('朝向权限被拒绝');
}
}
// 等待朝向数据
const orientationData = await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
window.removeEventListener('deviceorientation', orientationHandler);
reject(new Error('朝向检测超时'));
}, 8000);
function orientationHandler(event) {
if (event.alpha !== null && event.alpha !== undefined) {
clearTimeout(timeout);
window.removeEventListener('deviceorientation', orientationHandler);
resolve(event);
}
}
window.addEventListener('deviceorientation', orientationHandler);
});
let heading = orientationData.alpha;
if (heading !== null && heading !== undefined) {
// 处理不同设备的朝向数据
if (heading < 0) heading += 360;
heading = Math.round(heading);
this.log(`✅ 移动端朝向获取成功: ${heading}°`, 'success');
return { success: true, heading: heading };
} else {
throw new Error('无法获取有效的朝向数据');
}
} catch (error) {
this.log(`⚠️ 移动端朝向获取失败: ${error.message}`, 'warning');
return { success: false, error: error.message };
}
}
// 移动端自动配置摄像头GPS+朝向)
async autoConfigureCamera(location, heading) {
try {
this.log('🎯 移动端正在配置摄像头...', 'info');
const response = await fetch(`${this.baseURL}/api/orientation/auto_configure`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
gps_location: [location.latitude, location.longitude],
user_heading: heading,
apply_config: true
})
});
const result = await response.json();
if (result.success) {
this.log(`✅ 移动端摄像头配置成功: 朝向${result.camera_heading}°`, 'success');
} else {
throw new Error(result.error || '配置失败');
}
} catch (error) {
this.log(`❌ 移动端摄像头配置失败: ${error.message}`, 'error');
}
}
// 移动端仅使用GPS配置摄像头
async autoConfigureCameraWithGPSOnly(location) {
try {
this.log('📍 移动端使用GPS位置配置摄像头...', 'info');
const response = await fetch(`${this.baseURL}/api/orientation/auto_configure`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
gps_location: [location.latitude, location.longitude],
user_heading: 0, // 默认朝向北方
apply_config: true
})
});
const result = await response.json();
if (result.success) {
this.log('✅ 移动端GPS配置成功', 'success');
} else {
throw new Error(result.error || '配置失败');
}
} catch (error) {
this.log(`❌ 移动端GPS配置失败: ${error.message}`, 'error');
}
}
async initMobileCamera() {
try {
// 🌟 移动端摄像头启动时自动配置GPS和朝向
this.log('🤖 启动移动端摄像头时的自动配置...', 'info');
await this.autoConfigureDeviceOnSelection();
// 停止当前流
if (this.currentStream) {
this.currentStream.getTracks().forEach(track => track.stop());
this.currentStream = null;
}
// 移动端默认使用后置摄像头
const constraints = {
video: {
facingMode: 'environment', // 优先使用后置摄像头
width: { ideal: 640 },
height: { ideal: 480 },
frameRate: { ideal: 30, max: 30 }
},
audio: false
};
this.log('正在启动移动设备摄像头...', 'info');
// 先检查摄像头权限状态
const permissionState = await this.checkCameraPermission();
if (permissionState === 'denied') {
throw new Error('摄像头权限被拒绝,请在浏览器设置中重新允许');
}
this.currentStream = await navigator.mediaDevices.getUserMedia(constraints);
this.videoElement.srcObject = this.currentStream;
// 等待视频流准备就绪
await new Promise((resolve, reject) => {
this.videoElement.onloadedmetadata = resolve;
this.videoElement.onerror = reject;
// 设置超时防止无限等待
setTimeout(() => reject(new Error('视频加载超时')), 10000);
});
// 显示视频,隐藏占位符
this.videoElement.style.display = 'block';
document.getElementById('videoPlaceholder').style.display = 'none';
// 更新状态显示
document.getElementById('videoStatus').textContent = '摄像头已就绪';
document.getElementById('videoStatus').style.color = '#4CAF50';
this.log('移动设备摄像头启动成功', 'success');
} catch (error) {
let errorMsg = error.message;
if (error.name === 'NotAllowedError') {
errorMsg = '设备权限被拒绝,请允许访问摄像头';
} else if (error.name === 'NotFoundError') {
errorMsg = '设备未找到或已被占用';
}
this.log(`设备启动失败: ${errorMsg}`, 'error');
this.selectedDevice = null;
document.getElementById('startBtn').disabled = true;
}
}
async connectToRemoteDevice(deviceId, deviceInfo) {
try {
// 这里可以实现连接到远程设备的逻辑
// 例如通过WebRTC或其他方式接收远程视频流
this.log(`尝试连接远程设备: ${deviceInfo.name}`, 'info');
// 模拟远程连接
const response = await fetch(`${this.baseURL}/api/connect_remote_device`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
device_id: deviceId,
client_id: this.deviceId
})
});
if (response.ok) {
this.selectedDevice = { deviceId, ...deviceInfo, isRemote: true };
this.log(`远程设备连接成功: ${deviceInfo.name}`, 'success');
// 更新设备选择按钮和状态显示
const deviceSelectBtn = document.getElementById('deviceSelectBtn');
deviceSelectBtn.textContent = `🌐 ${deviceInfo.name || '远程设备'}`;
deviceSelectBtn.style.background = '#ff9800'; // 橙色表示远程设备
const deviceInfo_display = document.getElementById('selectedDeviceInfo');
deviceInfo_display.textContent = `远程: ${deviceInfo.name || '设备'}`;
deviceInfo_display.style.color = '#ff9800';
document.getElementById('startBtn').disabled = false;
// 启动远程视频流接收
this.startRemoteVideoStream(deviceId);
} else {
throw new Error(`连接失败: HTTP ${response.status}`);
}
} catch (error) {
this.log(`远程设备连接失败: ${error.message}`, 'error');
}
}
startRemoteVideoStream(deviceId) {
// 实现接收远程视频流的逻辑
this.log(`开始接收远程设备 ${deviceId} 的视频流`, 'info');
// 显示远程视频占位符
this.videoElement.style.display = 'none';
const placeholder = document.getElementById('videoPlaceholder');
placeholder.style.display = 'block';
placeholder.innerHTML = `<div style="padding: 20px;">🌐 正在接收远程设备视频...</div>`;
// 启动远程视频流轮询
this.remoteStreamInterval = setInterval(async () => {
try {
const response = await fetch(`${this.baseURL}/api/remote_device/stream/${deviceId}`);
if (response.ok) {
const data = await response.json();
if (data.status === 'success' && data.frame) {
// 显示远程设备的视频帧
placeholder.innerHTML = `
<img src="data:image/jpeg;base64,${data.frame}"
style="width: 100%; height: auto; border-radius: 8px;"
alt="远程设备视频">
<div style="font-size: 12px; color: #ccc; margin-top: 10px;">
远程设备: ${deviceId}
</div>
`;
}
}
} catch (error) {
this.log(`获取远程视频失败: ${error.message}`, 'error');
}
}, 500); // 每500ms更新一次
}
stopRemoteVideoStream() {
if (this.remoteStreamInterval) {
clearInterval(this.remoteStreamInterval);
this.remoteStreamInterval = null;
}
}
async refreshRemoteDevices() {
try {
const response = await fetch(`${this.baseURL}/api/mobile/devices`);
if (response.ok) {
const devices = await response.json();
this.remoteDevices = devices.filter(device => device.device_id !== this.deviceId);
this.log(`发现 ${this.remoteDevices.length} 个远程设备`, 'info');
}
} catch (error) {
this.log(`获取远程设备失败: ${error.message}`, 'error');
}
}
// 简单直接的Web摄像头接口
async startWebCamera() {
this.log('🎥 启动简单Web摄像头接口...', 'info');
try {
// 最简单的摄像头调用
const stream = await this.getSimpleCamera();
if (stream) {
this.setupSimpleVideoDisplay(stream);
return true;
}
} catch (error) {
this.log(`摄像头启动失败: ${error.message}`, 'error');
}
// 如果失败使用HTML5文件输入作为后备
this.log('🔄 启动简单文件输入接口...', 'info');
this.startSimpleFileInput();
return false;
}
async getSimpleCamera() {
// 尝试最基本的摄像头调用
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
this.log('📹 尝试基础getUserMedia...', 'info');
return await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' },
audio: false
});
}
// 尝试老式API
const getUserMedia = navigator.getUserMedia ||
navigator.webkitGetUserMedia ||
navigator.mozGetUserMedia ||
navigator.msGetUserMedia;
if (getUserMedia) {
this.log('📹 尝试老式getUserMedia...', 'info');
return new Promise((resolve, reject) => {
getUserMedia.call(navigator, {
video: { facingMode: 'environment' },
audio: false
}, resolve, reject);
});
}
throw new Error('浏览器不支持摄像头访问');
}
setupSimpleVideoDisplay(stream) {
this.log('✅ 摄像头启动成功!', 'success');
// 直接设置视频流
this.videoElement.srcObject = stream;
this.videoElement.style.display = 'block';
this.videoElement.play();
this.currentStream = stream;
// 显示控制按钮
document.getElementById('stopStreamBtn').style.display = 'inline-block';
document.getElementById('startBtn').disabled = false;
this.log('🎬 实时视频已开始显示', 'success');
// 开始简单的帧捕获
this.startSimpleFrameCapture();
}
startSimpleFrameCapture() {
if (!this.currentStream) return;
let frameCount = 0;
const capture = () => {
if (!this.currentStream) return;
// 简单的帧处理
this.canvas.width = this.videoElement.videoWidth || 640;
this.canvas.height = this.videoElement.videoHeight || 480;
this.ctx.drawImage(this.videoElement, 0, 0);
frameCount++;
if (frameCount % 30 === 0) {
this.log(`📊 处理帧数: ${frameCount}`, 'info');
}
requestAnimationFrame(capture);
};
capture();
}
startSimpleFileInput() {
this.log('📷 启动简单文件输入模式...', 'info');
// 创建一个简单的文件输入按钮
const inputBtn = document.createElement('button');
inputBtn.textContent = '📸 点击拍照';
inputBtn.className = 'btn';
inputBtn.style.background = '#2196F3';
inputBtn.style.margin = '10px';
inputBtn.style.fontSize = '16px';
inputBtn.style.padding = '12px 20px';
// 插入到控制面板
const controlPanel = document.querySelector('.controls');
controlPanel.appendChild(inputBtn);
let captureCount = 0;
inputBtn.onclick = () => {
// 创建隐藏的文件输入
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/*';
input.capture = 'camera';
input.style.display = 'none';
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
captureCount++;
this.log(`📸 拍照 ${captureCount}: ${file.name}`, 'success');
// 显示图片
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
// 在canvas上显示
this.canvas.width = img.width;
this.canvas.height = img.height;
this.ctx.drawImage(img, 0, 0);
// 在video元素上显示
this.videoElement.style.backgroundImage = `url(${event.target.result})`;
this.videoElement.style.backgroundSize = 'contain';
this.videoElement.style.backgroundRepeat = 'no-repeat';
this.videoElement.style.backgroundPosition = 'center';
this.videoElement.style.display = 'block';
document.getElementById('videoPlaceholder').style.display = 'none';
document.getElementById('startBtn').disabled = false;
this.log('✅ 照片已更新到显示框', 'success');
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
}
document.body.removeChild(input);
};
document.body.appendChild(input);
input.click();
};
this.log('📱 简单拍照模式已启用', 'success');
// 提供移除按钮的方法
this.removeSimpleInput = () => {
if (inputBtn.parentNode) {
inputBtn.parentNode.removeChild(inputBtn);
}
};
}
systemDiagnosis() {
console.log("=".repeat(80));
console.log("📱 移动端系统诊断开始");
console.log("=".repeat(80));
// 1. 基础信息
console.log("📊 基础系统信息:");
console.log(" - 设备ID:", this.deviceId);
console.log(" - 浏览器:", navigator.userAgent);
console.log(" - 当前时间:", new Date().toLocaleString());
console.log(" - 页面URL:", window.location.href);
console.log(" - 服务器地址:", this.baseURL);
// 2. GPS相关检查
console.log("🛰️ GPS支持检查:");
console.log(" - geolocation支持:", 'geolocation' in navigator);
console.log(" - permissions支持:", 'permissions' in navigator);
console.log(" - 当前位置:", this.currentPosition || '未获取');
console.log(" - GPS监听ID:", this.gpsWatchId || '未启动');
// 3. 摄像头相关检查
console.log("📹 摄像头支持检查:");
console.log(" - mediaDevices支持:", !!navigator.mediaDevices);
console.log(" - getUserMedia支持:", !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia));
console.log(" - 当前视频流:", this.currentStream ? '已获取' : '未获取');
console.log(" - 视频元素状态:");
if (this.videoElement) {
console.log(" - videoWidth:", this.videoElement.videoWidth);
console.log(" - videoHeight:", this.videoElement.videoHeight);
console.log(" - readyState:", this.videoElement.readyState);
console.log(" - srcObject:", !!this.videoElement.srcObject);
}
// 4. 传输状态
console.log("📡 传输状态:");
console.log(" - 正在传输:", this.isStreaming);
console.log(" - 已发送帧数:", this.frameCount);
console.log(" - 数据量:", this.dataAmount, "bytes");
console.log(" - 平均延迟:", this.averageUploadTime || 0, "ms");
// 5. UI元素检查
console.log("🎨 UI元素检查:");
const gpsStatus = document.getElementById('gpsStatus');
const batteryStatus = document.getElementById('batteryStatus');
const frameRateSelect = document.getElementById('frameRate');
const imageQualitySelect = document.getElementById('imageQuality');
console.log(" - GPS状态元素:", gpsStatus ? gpsStatus.textContent : '未找到');
console.log(" - 电池状态元素:", batteryStatus ? batteryStatus.textContent : '未找到');
console.log(" - 帧率设置:", frameRateSelect ? frameRateSelect.value : '未找到');
console.log(" - 图像质量设置:", imageQualitySelect ? imageQualitySelect.value : '未找到');
// 6. 权限检查
console.log("🔐 权限检查:");
if ('permissions' in navigator) {
navigator.permissions.query({ name: 'geolocation' }).then((result) => {
console.log(" - GPS权限状态:", result.state);
}).catch((error) => {
console.log(" - GPS权限查询失败:", error);
});
navigator.permissions.query({ name: 'camera' }).then((result) => {
console.log(" - 摄像头权限状态:", result.state);
}).catch((error) => {
console.log(" - 摄像头权限查询失败:", error);
});
} else {
console.log(" - 浏览器不支持权限查询API");
}
console.log("=".repeat(80));
console.log("📱 移动端系统诊断完成 - 请查看以上详细信息");
console.log("=".repeat(80));
this.log('系统诊断完成,请查看浏览器控制台详细信息', 'info');
}
}
let mobileClient;
window.addEventListener('load', () => {
mobileClient = new MobileClient();
});
// ========== 移动端专用功能(已简化) ==========
</script>
</body>
</html>