第一部分:Web前端应用层架构分析

为了更直观地展示整个前端应用的技术栈、模块划分和核心数据流,梳理了以下架构图:

1. 模块化架构与功能解耦分析
index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>USB摄像头监控系统</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <link rel="stylesheet" href="styles.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
</head>
<body class="bg-gray-100 min-h-screen">
    <!-- 导航栏 -->
    <nav class="bg-white shadow-lg">
        <div class="max-w-7xl mx-auto px-4">
            <div class="flex justify-between items-center py-4">
                <div class="flex items-center space-x-2">
                    <i class="fas fa-camera text-blue-500 text-2xl"></i>
                    <h1 class="text-2xl font-bold text-gray-800">USB摄像头监控系统</h1>
                </div>
                <div class="flex items-center space-x-4">
                    <span id="deviceStatusText" class="text-sm text-gray-600">
                        <i class="fas fa-circle text-green-500 mr-1"></i>
                        <span>设备就绪: /dev/video0</span>
                    </span>
                    <button id="settingsBtn" class="p-2 text-gray-600 hover:text-blue-500 transition-colors">
                        <i class="fas fa-cog text-xl"></i>
                    </button>
                </div>
            </div>
        </div>
    </nav>
​
    <!-- 主内容区域 -->
    <main class="max-w-7xl mx-auto px-4 py-8">
        <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
            <!-- 视频监控区域 -->
            <div class="lg:col-span-2">
                <div class="bg-white rounded-2xl shadow-xl overflow-hidden">
                    <div class="video-container p-4">
                        <div class="bg-black rounded-xl overflow-hidden aspect-video relative">
                            <video id="videoPreview" class="w-full h-full object-cover" autoplay muted></video>
                            <div id="statusOverlay" class="absolute top-4 left-4 bg-black bg-opacity-50 text-white px-3 py-1 rounded-full text-sm">
                                <i class="fas fa-circle text-green-500 mr-1"></i>
                                <span>就绪</span>
                            </div>
                            <div id="recordingIndicator" class="absolute top-4 right-4 bg-red-500 text-white px-3 py-1 rounded-full text-sm hidden">
                                <i class="fas fa-circle mr-1"></i>
                                <span>录制中</span>
                            </div>
                        </div>
                    </div>
                    
                    <!-- 控制面板 -->
                    <div class="p-6 bg-white">
                        <div class="flex flex-wrap gap-4 justify-center">
                            <button id="startBtn" class="btn-primary px-6 py-3 rounded-full text-white font-semibold flex items-center space-x-2">
                                <i class="fas fa-play"></i>
                                <span>启动摄像头</span>
                            </button>
                            <button id="captureBtn" class="bg-green-500 hover:bg-green-600 px-6 py-3 rounded-full text-white font-semibold flex items-center space-x-2 transition-all disabled:opacity-50" disabled>
                                <i class="fas fa-camera"></i>
                                <span>拍摄截图</span>
                            </button>
                            <button id="recordBtn" class="bg-red-500 hover:bg-red-600 px-6 py-3 rounded-full text-white font-semibold flex items-center space-x-2 transition-all disabled:opacity-50" disabled>
                                <i class="fas fa-video"></i>
                                <span>开始录制</span>
                            </button>
                            <button id="stopBtn" class="bg-gray-500 hover:bg-gray-600 px-6 py-3 rounded-full text-white font-semibold flex items-center space-x-2 transition-all">
                                <i class="fas fa-stop"></i>
                                <span>停止</span>
                            </button>
                        </div>
                        
                        <!-- 设置选项 -->
                        <div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
                            <div>
                                <label class="block text-sm font-medium text-gray-700 mb-2">视频分辨率</label>
                                <select id="resolutionSelect" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
                                    <option value="640x480">640x480 (VGA)</option>
                                    <option value="1280x720" selected>1280x720 (HD)</option>
                                    <option value="1920x1080">1920x1080 (Full HD)</option>
                                </select>
                            </div>
                            <div>
                                <label class="block text-sm font-medium text-gray-700 mb-2">帧率设置</label>
                                <select id="frameRateSelect" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
                                    <option value="15">15 FPS</option>
                                    <option value="30" selected>30 FPS</option>
                                    <option value="60">60 FPS</option>
                                </select>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
​
            <!-- 侧边栏 - 截图管理 -->
            <div class="lg:col-span-1">
                <div class="bg-white rounded-2xl shadow-xl p-6 h-full">
                    <div class="flex items-center justify-between mb-6">
                        <h2 class="text-xl font-bold text-gray-800 flex items-center">
                            <i class="fas fa-images text-purple-500 mr-2"></i>
                            截图库
                        </h2>
                        <span id="screenshotCount" class="bg-blue-100 text-blue-800 text-sm px-3 py-1 rounded-full">0 张</span>
                    </div>
                    
                    <div id="screenshotList" class="space-y-4 max-h-96 overflow-y-auto pr-2">
                        <div class="text-center py-8 text-gray-500">
                            <i class="fas fa-camera text-4xl mb-2 opacity-50"></i>
                            <p>暂无截图</p>
                            <p class="text-sm">点击"拍摄截图"按钮开始收集</p>
                        </div>
                    </div>
                    
                    <div class="mt-6 space-y-2">
                        <button id="clearAllBtn" class="w-full bg-gray-100 hover:bg-gray-200 py-2 rounded-lg text-gray-700 transition-colors disabled:opacity-50" disabled>
                            <i class="fas fa-trash mr-2"></i>清空所有截图
                        </button>
                        <button id="exportAllBtn" class="w-full bg-blue-100 hover:bg-blue-200 py-2 rounded-lg text-blue-700 transition-colors disabled:opacity-50" disabled>
                            <i class="fas fa-download mr-2"></i>导出全部截图
                        </button>
                    </div>
                </div>
            </div>
        </div>
​
        <!-- 统计信息 -->
        <div class="mt-8 grid grid-cols-1 md:grid-cols-4 gap-6">
            <div class="bg-white rounded-xl p-6 shadow-lg">
                <div class="flex items-center justify-between">
                    <div>
                        <p class="text-gray-600 text-sm">运行时间</p>
                        <p id="uptime" class="text-2xl font-bold text-gray-800">00:00:00</p>
                    </div>
                    <i class="fas fa-clock text-3xl text-blue-500"></i>
                </div>
            </div>
            <div class="bg-white rounded-xl p-6 shadow-lg">
                <div class="flex items-center justify-between">
                    <div>
                        <p class="text-gray-600 text-sm">截图数量</p>
                        <p id="totalCaptures" class="text-2xl font-bold text-green-800">0</p>
                    </div>
                    <i class="fas fa-camera text-3xl text-green-500"></i>
                </div>
            </div>
            <div class="bg-white rounded-xl p-6 shadow-lg">
                <div class="flex items-center justify-between">
                    <div>
                        <p class="text-gray-600 text-sm">录制状态</p>
                        <p id="recordStatus" class="text-2xl font-bold text-red-800">未开始</p>
                    </div>
                    <i class="fas fa-video text-3xl text-red-500"></i>
                </div>
            </div>
            <div class="bg-white rounded-xl p-6 shadow-lg">
                <div class="flex items-center justify-between">
                    <div>
                        <p class="text-gray-600 text-sm">设备状态</p>
                        <p id="deviceStatus" class="text-2xl font-bold text-purple-800">就绪</p>
                    </div>
                    <i class="fas fa-plug text-3xl text-purple-500"></i>
                </div>
            </div>
        </div>
    </main>
​
    <!-- 模态框 - 设置 -->
    <div id="settingsModal" class="modal-overlay hidden">
        <div class="modal-content">
            <div class="modal-header">
                <h3 class="modal-title">系统设置</h3>
                <button id="closeSettings" class="modal-close-btn">
                    <i class="fas fa-times"></i>
                </button>
            </div>
            <div class="modal-body">
                <div class="space-y-4">
                    <div>
                        <label class="form-label">摄像头设备</label>
                        <select id="cameraDeviceSelect" class="form-select">
                            <option value="default">默认摄像头</option>
                        </select>
                    </div>
                    <div>
                        <label class="form-label">视频质量</label>
                        <select id="videoQuality" class="form-select">
                            <option value="high">高质量</option>
                            <option value="medium" selected>平衡</option>
                            <option value="low">低质量</option>
                        </select>
                    </div>
                    <div class="form-checkbox-group">
                        <input type="checkbox" id="autoStart" class="form-checkbox" checked>
                        <label for="autoStart" class="form-checkbox-label">页面加载时自动启动摄像头</label>
                    </div>
                    <div class="form-checkbox-group">
                        <input type="checkbox" id="motionDetection" class="form-checkbox">
                        <label for="motionDetection" class="form-checkbox-label">启用运动检测</label>
                    </div>
                    <div class="form-checkbox-group">
                        <input type="checkbox" id="autoSave" class="form-checkbox" checked>
                        <label for="autoSave" class="form-checkbox-label">自动保存截图到本地存储</label>
                    </div>
                </div>
            </div>
            <div class="modal-footer">
                <button id="cancelSettings" class="btn-secondary">取消</button>
                <button id="saveSettings" class="btn-primary">保存设置</button>
            </div>
        </div>
    </div>
​
    <!-- 截图查看器模态框 -->
    <div id="screenshotViewerModal" class="modal-overlay hidden">
        <div class="modal-content max-w-4xl">
            <div class="modal-header">
                <h3 class="modal-title">查看截图</h3>
                <button id="closeViewer" class="modal-close-btn">
                    <i class="fas fa-times"></i>
                </button>
            </div>
            <div class="modal-body">
                <div id="viewerContent" class="text-center">
                    <img id="fullSizeScreenshot" class="max-w-full max-h-96 mx-auto rounded-lg" src="" alt="全尺寸截图">
                    <div class="mt-4 text-sm text-gray-600">
                        <p id="screenshotInfo"></p>
                    </div>
                </div>
            </div>
            <div class="modal-footer">
                <button id="downloadScreenshot" class="btn-primary">
                    <i class="fas fa-download mr-2"></i>下载图片
                </button>
                <button id="closeViewerBtn" class="btn-secondary">关闭</button>
            </div>
        </div>
    </div>
​
    <!-- 通知容器 -->
    <div id="notificationContainer" class="fixed top-4 right-4 z-50 space-y-2"></div>
​
    <script src="app.js" type="module"></script>
</body>
</html>
app.js
/**
 * USB摄像头监控系统 - 主应用模块 (app.js)
 * 重新设计版本 - 解决黑屏、卡顿问题
 * 采用模块化设计,逐行注解说明
 */
​
// ============================================
// 工具类:性能监控器
// ============================================
class PerformanceMonitor {
    constructor() {
        this.markers = new Map(); // 性能标记点
        this.frameStats = {
            count: 0,
            lastTime: performance.now(),
            fps: 0,
            avgProcessingTime: 0
        };
        this.memoryStats = {
            screenshots: 0,
            totalSize: 0
        };
        this.isMonitoring = false;
        this.monitorInterval = null;
    }
​
    /**
     * 开始性能监控
     */
    startMonitoring() {
        if (this.isMonitoring) return;
        
        this.isMonitoring = true;
        console.log('[性能监控] 开始监控系统性能');
        
        // 每2秒更新一次性能数据
        this.monitorInterval = setInterval(() => {
            this.updateFrameRate();
            this.logMemoryUsage();
        }, 2000);
    }
​
    /**
     * 停止性能监控
     */
    stopMonitoring() {
        if (!this.isMonitoring) return;
        
        this.isMonitoring = false;
        clearInterval(this.monitorInterval);
        console.log('[性能监控] 停止监控');
    }
​
    /**
     * 标记性能测量点开始
     * @param {string} name - 标记名称
     */
    markStart(name) {
        this.markers.set(name, {
            startTime: performance.now(),
            endTime: null,
            duration: null
        });
    }
​
    /**
     * 标记性能测量点结束并计算时长
     * @param {string} name - 标记名称
     * @returns {number} 执行时长(毫秒)
     */
    markEnd(name) {
        const marker = this.markers.get(name);
        if (!marker) {
            console.warn(`[性能监控] 未找到标记: ${name}`);
            return 0;
        }
​
        marker.endTime = performance.now();
        marker.duration = marker.endTime - marker.startTime;
        
        // 记录平均处理时间
        if (name.includes('frame')) {
            this.frameStats.avgProcessingTime = 
                (this.frameStats.avgProcessingTime * 0.7 + marker.duration * 0.3);
        }
        
        // 如果耗时超过阈值,发出警告
        if (marker.duration > 16.67) { // 超过60fps的一帧时间
            console.warn(`[性能警告] ${name} 耗时 ${marker.duration.toFixed(2)}ms`);
        }
        
        return marker.duration;
    }
​
    /**
     * 更新帧率统计
     */
    updateFrameRate() {
        const now = performance.now();
        const elapsed = now - this.frameStats.lastTime;
        
        if (elapsed >= 1000) {
            this.frameStats.fps = Math.round(
                (this.frameStats.count * 1000) / elapsed
            );
            this.frameStats.count = 0;
            this.frameStats.lastTime = now;
            
            // 更新UI显示(如果存在)
            this.updateFpsDisplay();
        }
    }
​
    /**
     * 增加帧计数
     */
    incrementFrameCount() {
        this.frameStats.count++;
    }
​
    /**
     * 更新FPS显示
     */
    updateFpsDisplay() {
        const fpsElement = document.getElementById('fpsDisplay');
        if (fpsElement) {
            fpsElement.textContent = `${this.frameStats.fps} FPS`;
            fpsElement.className = this.frameStats.fps < 20 ? 'fps-low' : 
                                  this.frameStats.fps > 25 ? 'fps-good' : 'fps-normal';
        }
    }
​
    /**
     * 记录内存使用情况
     */
    logMemoryUsage() {
        const memory = this.memoryStats;
        console.log(`[内存使用] 截图: ${memory.screenshots}张, 大小: ${(memory.totalSize / 1024).toFixed(2)}KB`);
    }
​
    /**
     * 更新内存统计
     * @param {number} screenshotCount - 截图数量
     * @param {number} totalSize - 总大小(字节)
     */
    updateMemoryStats(screenshotCount, totalSize) {
        this.memoryStats.screenshots = screenshotCount;
        this.memoryStats.totalSize = totalSize;
    }
​
    /**
     * 获取性能报告
     * @returns {Object} 性能报告
     */
    getReport() {
        return {
            fps: this.frameStats.fps,
            avgFrameTime: this.frameStats.avgProcessingTime.toFixed(2),
            memory: { ...this.memoryStats }
        };
    }
}
​
// ============================================
// 工具类:截图管理器
// ============================================
class ScreenshotManager {
    constructor() {
        this.screenshots = [];
        this.maxCount = 100; // 最大保存数量
        this.storageKey = 'camera_screenshots_v2';
        this.loadFromStorage();
    }
​
    /**
     * 从本地存储加载截图
     */
    loadFromStorage() {
        try {
            const stored = localStorage.getItem(this.storageKey);
            if (stored) {
                this.screenshots = JSON.parse(stored);
                console.log(`[截图管理] 从存储加载 ${this.screenshots.length} 张截图`);
            }
        } catch (error) {
            console.error('[截图管理] 加载存储失败:', error);
            this.screenshots = [];
        }
    }
​
    /**
     * 保存到本地存储
     */
    saveToStorage() {
        try {
            const data = JSON.stringify(this.screenshots.slice(-50)); // 只保存最近50张
            localStorage.setItem(this.storageKey, data);
        } catch (error) {
            console.error('[截图管理] 保存到存储失败:', error);
        }
    }
​
    /**
     * 添加截图
     * @param {Object} screenshot - 截图数据
     * @param {string} screenshot.dataUrl - 图片数据URL
     * @param {number} screenshot.width - 宽度
     * @param {number} screenshot.height - 高度
     * @param {string} screenshot.format - 格式(jpeg/png)
     * @returns {string} 截图ID
     */
    addScreenshot(screenshot) {
        const screenshotData = {
            id: `ss_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
            dataUrl: screenshot.dataUrl,
            width: screenshot.width,
            height: screenshot.height,
            format: screenshot.format || 'jpeg',
            timestamp: Date.now(),
            dateString: new Date().toLocaleString('zh-CN'),
            size: Math.floor(screenshot.dataUrl.length * 0.75) // 估算字节数
        };
​
        // 添加到开头(最新在前)
        this.screenshots.unshift(screenshotData);
        
        // 限制数量
        if (this.screenshots.length > this.maxCount) {
            this.screenshots = this.screenshots.slice(0, this.maxCount);
        }
        
        // 异步保存到存储
        setTimeout(() => this.saveToStorage(), 0);
        
        console.log(`[截图管理] 新增截图: ${screenshotData.id} (${screenshotData.width}x${screenshotData.height})`);
        return screenshotData.id;
    }
​
    /**
     * 获取截图
     * @param {string} id - 截图ID
     * @returns {Object|null} 截图数据
     */
    getScreenshot(id) {
        return this.screenshots.find(ss => ss.id === id) || null;
    }
​
    /**
     * 删除截图
     * @param {string} id - 截图ID
     * @returns {boolean} 是否删除成功
     */
    deleteScreenshot(id) {
        const initialLength = this.screenshots.length;
        this.screenshots = this.screenshots.filter(ss => ss.id !== id);
        const deleted = initialLength > this.screenshots.length;
        
        if (deleted) {
            this.saveToStorage();
            console.log(`[截图管理] 删除截图: ${id}`);
        }
        
        return deleted;
    }
​
    /**
     * 清空所有截图
     */
    clearAll() {
        const count = this.screenshots.length;
        this.screenshots = [];
        localStorage.removeItem(this.storageKey);
        console.log(`[截图管理] 清空所有截图,共 ${count} 张`);
    }
​
    /**
     * 获取截图数量
     * @returns {number} 截图数量
     */
    getCount() {
        return this.screenshots.length;
    }
​
    /**
     * 获取所有截图
     * @returns {Array} 截图数组
     */
    getAll() {
        return [...this.screenshots]; // 返回副本
    }
​
    /**
     * 批量导出截图
     * @returns {Promise<Blob>} ZIP文件Blob
     */
    async exportToZip() {
        if (this.screenshots.length === 0) {
            throw new Error('没有可导出的截图');
        }
​
        // 动态导入JSZip库(如果已全局加载则跳过)
        let JSZip;
        if (typeof window.JSZip !== 'undefined') {
            JSZip = window.JSZip;
        } else {
            // 这里可以添加动态加载JSZip的代码
            throw new Error('JSZip库未加载,请确保在页面中引入');
        }
​
        const zip = new JSZip();
        const folder = zip.folder('screenshots');
        
        // 添加截图文件
        this.screenshots.forEach((ss, index) => {
            const base64Data = ss.dataUrl.split(',')[1];
            const filename = `screenshot_${index + 1}_${ss.width}x${ss.height}.${ss.format}`;
            folder.file(filename, base64Data, { base64: true });
        });
        
        // 添加元数据文件
        const metadata = {
            exportDate: new Date().toISOString(),
            totalScreenshots: this.screenshots.length,
            appVersion: '2.0.0',
            resolutions: this.screenshots.map(ss => `${ss.width}x${ss.height}`)
        };
        folder.file('metadata.json', JSON.stringify(metadata, null, 2));
        
        return await zip.generateAsync({ type: 'blob' });
    }
}
​
// ============================================
// 核心类:摄像头控制器
// ============================================
class CameraController {
    constructor(videoElement) {
        // 视频元素引用
        this.videoElement = videoElement;
        
        // 媒体流状态
        this.mediaStream = null;
        this.videoTrack = null;
        this.isStreaming = false;
        
        // 录制状态
        this.mediaRecorder = null;
        this.recordedChunks = [];
        this.isRecording = false;
        
        // 配置
        this.constraints = {
            video: {
                width: { ideal: 1280 },
                height: { ideal: 720 },
                frameRate: { ideal: 30 },
                // 不指定设备ID,让浏览器选择
                facingMode: 'environment'
            },
            audio: false
        };
        
        // 支持的约束优先级(根据v4l2输出)
        this.formatPriority = [
            { width: 1280, height: 720, frameRate: 30 },
            { width: 640, height: 480, frameRate: 30 },
            { width: 1280, height: 1024, frameRate: 30 },
            { width: 1920, height: 1080, frameRate: 15 }
        ];
        
        console.log('[摄像头控制器] 初始化完成');
    }
    
    /**
     * 获取摄像头设备列表
     * @returns {Promise<Array>} 设备列表
     */
    async getDevices() {
        try {
            // 先请求权限,否则可能无法获取设备标签
            await navigator.mediaDevices.getUserMedia({ video: true });
            
            const devices = await navigator.mediaDevices.enumerateDevices();
            const videoDevices = devices.filter(device => device.kind === 'videoinput');
            
            console.log(`[摄像头控制器] 找到 ${videoDevices.length} 个视频设备`);
            return videoDevices;
        } catch (error) {
            console.error('[摄像头控制器] 获取设备失败:', error);
            return [];
        }
    }
    
    /**
     * 启动摄像头
     * @param {Object} options - 启动选项
     * @param {string} options.deviceId - 设备ID
     * @param {number} options.width - 宽度
     * @param {number} options.height - 高度
     * @param {number} options.frameRate - 帧率
     * @returns {Promise<MediaStream>} 媒体流
     */
    async start(options = {}) {
        console.log('[摄像头控制器] 启动摄像头...');
        
        // 停止现有的流
        if (this.isStreaming) {
            this.stop();
        }
        
        try {
            // 构建约束条件
            const constraints = this.buildConstraints(options);
            console.log('[摄像头控制器] 使用约束:', JSON.stringify(constraints.video));
            
            // 请求摄像头访问权限
            this.mediaStream = await navigator.mediaDevices.getUserMedia(constraints);
            console.log('[摄像头控制器] 摄像头访问权限已获取');
            
            // 获取视频轨道
            const tracks = this.mediaStream.getVideoTracks();
            if (tracks.length === 0) {
                throw new Error('未获取到视频轨道');
            }
            
            this.videoTrack = tracks[0];
            this.isStreaming = true;
            
            // 获取实际的视频设置
            const settings = this.videoTrack.getSettings();
            console.log('[摄像头控制器] 实际视频设置:', settings);
            
            // 将流连接到视频元素
            this.videoElement.srcObject = this.mediaStream;
            
            // 等待视频元素就绪
            await this.waitForVideoReady();
            
            console.log('[摄像头控制器] 摄像头启动成功');
            return this.mediaStream;
            
        } catch (error) {
            console.error('[摄像头控制器] 启动失败:', error);
            this.handleStartError(error);
            throw error;
        }
    }
    
    /**
     * 构建约束条件
     * @param {Object} options - 用户选项
     * @returns {Object} 媒体约束
     */
    buildConstraints(options) {
        const constraints = JSON.parse(JSON.stringify(this.constraints));
        
        // 应用用户选项
        if (options.deviceId) {
            constraints.video.deviceId = { exact: options.deviceId };
        }
        
        if (options.width && options.height) {
            constraints.video.width = { ideal: options.width };
            constraints.video.height = { ideal: options.height };
        }
        
        if (options.frameRate) {
            constraints.video.frameRate = { ideal: options.frameRate };
        }
        
        return constraints;
    }
    
    /**
     * 等待视频元素就绪
     * @returns {Promise<void>}
     */
    waitForVideoReady() {
        return new Promise((resolve, reject) => {
            // 设置超时
            const timeout = setTimeout(() => {
                reject(new Error('视频加载超时'));
            }, 5000);
            
            // 检查视频是否已有元数据
            if (this.videoElement.readyState >= 1) { // HAVE_METADATA
                clearTimeout(timeout);
                resolve();
                return;
            }
            
            // 监听元数据加载事件
            const onLoadedMetadata = () => {
                clearTimeout(timeout);
                this.videoElement.removeEventListener('loadedmetadata', onLoadedMetadata);
                this.videoElement.removeEventListener('error', onError);
                resolve();
            };
            
            // 监听错误事件
            const onError = (error) => {
                clearTimeout(timeout);
                this.videoElement.removeEventListener('loadedmetadata', onLoadedMetadata);
                this.videoElement.removeEventListener('error', onError);
                reject(error);
            };
            
            this.videoElement.addEventListener('loadedmetadata', onLoadedMetadata);
            this.videoElement.addEventListener('error', onError);
        });
    }
    
    /**
     * 处理启动错误
     * @param {Error} error - 错误对象
     */
    handleStartError(error) {
        let userMessage = '摄像头启动失败';
        
        switch (error.name) {
            case 'NotAllowedError':
                userMessage = '请允许摄像头访问权限';
                break;
            case 'NotFoundError':
                userMessage = '未找到摄像头设备';
                break;
            case 'NotReadableError':
                userMessage = '摄像头被其他程序占用';
                break;
            case 'OverconstrainedError':
                userMessage = '请求的视频参数不被支持';
                break;
            case 'TypeError':
                userMessage = '无效的参数';
                break;
        }
        
        console.error(`[摄像头控制器] 用户错误: ${userMessage}`, error);
    }
    
    /**
     * 停止摄像头
     */
    stop() {
        console.log('[摄像头控制器] 停止摄像头...');
        
        // 停止录制
        if (this.isRecording) {
            this.stopRecording();
        }
        
        // 停止所有轨道
        if (this.mediaStream) {
            this.mediaStream.getTracks().forEach(track => {
                track.stop();
                console.log(`[摄像头控制器] 停止轨道: ${track.kind}`);
            });
            
            this.mediaStream = null;
            this.videoTrack = null;
        }
        
        // 清空视频元素
        if (this.videoElement) {
            this.videoElement.srcObject = null;
        }
        
        this.isStreaming = false;
        console.log('[摄像头控制器] 摄像头已停止');
    }
    
    /**
     * 捕获当前帧
     * @param {Object} options - 捕获选项
     * @param {string} options.format - 图片格式(image/jpeg, image/png)
     * @param {number} options.quality - JPEG质量(0-1)
     * @returns {Promise<string>} 图片数据URL
     */
    captureFrame(options = {}) {
        if (!this.isStreaming) {
            throw new Error('摄像头未启动');
        }
        
        return new Promise((resolve, reject) => {
            try {
                // 创建画布
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext('2d');
                
                // 设置画布尺寸与视频一致
                canvas.width = this.videoElement.videoWidth;
                canvas.height = this.videoElement.videoHeight;
                
                // 绘制当前帧
                ctx.drawImage(this.videoElement, 0, 0);
                
                // 生成图片数据
                const format = options.format || 'image/jpeg';
                const quality = options.quality || 0.92;
                
                const dataUrl = canvas.toDataURL(format, quality);
                
                console.log(`[摄像头控制器] 捕获帧: ${canvas.width}x${canvas.height} (${format})`);
                resolve(dataUrl);
                
            } catch (error) {
                console.error('[摄像头控制器] 捕获帧失败:', error);
                reject(error);
            }
        });
    }
    
    /**
     * 开始录制视频
     * @returns {boolean} 是否成功开始
     */
    startRecording() {
        if (!this.isStreaming || this.isRecording) {
            return false;
        }
        
        try {
            // 重置录制数据
            this.recordedChunks = [];
            
            // 创建媒体录制器
            const mimeType = this.getSupportedMimeType();
            const options = { mimeType };
            
            this.mediaRecorder = new MediaRecorder(this.mediaStream, options);
            
            // 设置数据可用回调
            this.mediaRecorder.ondataavailable = (event) => {
                if (event.data && event.data.size > 0) {
                    this.recordedChunks.push(event.data);
                    console.log(`[摄像头控制器] 录制数据块: ${event.data.size}字节`);
                }
            };
            
            // 设置录制停止回调
            this.mediaRecorder.onstop = () => {
                console.log('[摄像头控制器] 录制停止,准备保存');
                this.saveRecording();
            };
            
            // 开始录制
            this.mediaRecorder.start(1000); // 每1秒收集一次数据
            this.isRecording = true;
            
            console.log('[摄像头控制器] 开始录制视频');
            return true;
            
        } catch (error) {
            console.error('[摄像头控制器] 开始录制失败:', error);
            return false;
        }
    }
    
    /**
     * 停止录制视频
     */
    stopRecording() {
        if (!this.isRecording || !this.mediaRecorder) {
            return;
        }
        
        try {
            if (this.mediaRecorder.state !== 'inactive') {
                this.mediaRecorder.stop();
            }
            this.isRecording = false;
            console.log('[摄像头控制器] 停止录制');
        } catch (error) {
            console.error('[摄像头控制器] 停止录制失败:', error);
        }
    }
    
    /**
     * 获取支持的MIME类型
     * @returns {string} MIME类型
     */
    getSupportedMimeType() {
        const mimeTypes = [
            'video/webm;codecs=vp9',
            'video/webm;codecs=vp8',
            'video/webm;codecs=h264',
            'video/webm',
            'video/mp4'
        ];
        
        for (const mimeType of mimeTypes) {
            if (MediaRecorder.isTypeSupported(mimeType)) {
                console.log(`[摄像头控制器] 使用MIME类型: ${mimeType}`);
                return mimeType;
            }
        }
        
        console.warn('[摄像头控制器] 未找到支持的MIME类型,使用默认值');
        return '';
    }
    
    /**
     * 保存录制视频
     */
    saveRecording() {
        if (this.recordedChunks.length === 0) {
            console.warn('[摄像头控制器] 没有录制数据');
            return;
        }
        
        try {
            // 合并数据块
            const blob = new Blob(this.recordedChunks, {
                type: this.mediaRecorder.mimeType || 'video/webm'
            });
            
            // 创建下载链接
            const url = URL.createObjectURL(blob);
            const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
            const filename = `recording_${timestamp}.webm`;
            
            // 触发下载
            const a = document.createElement('a');
            a.href = url;
            a.download = filename;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            
            // 释放URL
            setTimeout(() => URL.revokeObjectURL(url), 1000);
            
            console.log(`[摄像头控制器] 视频已保存: ${filename} (${blob.size}字节)`);
            
        } catch (error) {
            console.error('[摄像头控制器] 保存录制失败:', error);
        }
    }
    
    /**
     * 获取当前状态
     * @returns {Object} 状态信息
     */
    getStatus() {
        return {
            isStreaming: this.isStreaming,
            isRecording: this.isRecording,
            videoTrack: this.videoTrack ? {
                label: this.videoTrack.label,
                settings: this.videoTrack.getSettings()
            } : null,
            stream: this.mediaStream ? {
                active: this.mediaStream.active,
                id: this.mediaStream.id
            } : null
        };
    }
    
    /**
     * 切换摄像头状态
     * @returns {Promise<boolean>} 切换后的状态
     */
    async toggle() {
        if (this.isStreaming) {
            this.stop();
            return false;
        } else {
            await this.start();
            return true;
        }
    }
}
​
// ============================================
// 主应用类:USB摄像头监控系统
// ============================================
class CameraMonitorApp {
    constructor() {
        console.log('[主应用] 开始初始化 CameraMonitorApp...');
        
        // -----------------------------
        // 核心组件初始化
        // -----------------------------
        // 1. 获取视频元素 - 用于显示摄像头画面
        this.videoElement = document.getElementById('videoPreview');
        if (!this.videoElement) {
            throw new Error('未找到视频元素 #videoPreview');
        }
        
        // 2. 初始化性能监控器 - 监控帧率、内存等性能指标
        this.performance = new PerformanceMonitor();
        
        // 3. 初始化截图管理器 - 管理截图的存储、加载、删除
        this.screenshotManager = new ScreenshotManager();
        
        // 4. 初始化摄像头控制器 - 控制摄像头启停、录制等核心功能
        this.cameraController = new CameraController(this.videoElement);
        
        // -----------------------------
        // 应用状态变量
        // -----------------------------
        // 当前应用状态:'idle'(空闲), 'starting'(启动中), 'streaming'(流传输中), 'recording'(录制中)
        this.appState = 'idle';
        
        // 应用配置:从本地存储加载或使用默认值
        this.config = {
            autoStart: false,           // 是否自动启动摄像头
            defaultResolution: '1280x720',  // 默认分辨率
            defaultFrameRate: 30,       // 默认帧率
            videoQuality: 0.92,         // 视频/截图质量 (0-1)
            enableNotifications: true,  // 是否启用通知
            maxScreenshots: 100         // 最大截图数量
        };
        
        // 可用摄像头设备列表
        this.availableDevices = [];
        
        // 当前选中的摄像头设备ID
        this.selectedDeviceId = null;
        
        // 运行时间计数器(秒)
        this.uptimeSeconds = 0;
        this.uptimeInterval = null;
        
        console.log('[主应用] 基础组件初始化完成');
    }
    
    /**
     * 初始化应用 - 入口方法
     * 顺序执行所有初始化步骤,确保依赖关系正确
     */
    async initialize() {
        console.log('[主应用] 开始完整初始化流程...');
        
        try {
            // 第一步:加载配置
            // 从localStorage加载用户保存的配置,如果不存在则使用默认配置
            await this.loadConfiguration();
            
            // 第二步:初始化UI组件
            // 设置所有界面元素的状态和事件监听器
            this.initializeUIComponents();
            
            // 第三步:更新设备列表
            // 获取系统中可用的摄像头设备
            await this.updateDeviceList();
            
            // 第四步:加载截图数据
            // 从本地存储加载之前保存的截图
            this.loadScreenshots();
            
            // 第五步:恢复应用状态
            // 根据配置恢复上次的应用状态(如自动启动)
            this.restoreAppState();
            
            // 第六步:开始性能监控
            // 启动帧率监控和性能统计
            this.performance.startMonitoring();
            
            // 第七步:更新运行时间显示
            // 启动计时器,每秒更新运行时间
            this.startUptimeCounter();
            
            console.log('[主应用] 应用初始化完成,当前状态:', this.appState);
            
            // 显示初始化完成通知
            this.showNotification('应用初始化完成', 'success');
            
        } catch (error) {
            console.error('[主应用] 初始化失败:', error);
            this.showNotification(`初始化失败: ${error.message}`, 'error');
            throw error;
        }
    }
    
    /**
     * 从本地存储加载配置
     * 如果本地没有保存的配置,则使用默认配置
     */
    async loadConfiguration() {
        console.log('[主应用] 加载配置...');
        
        try {
            // 尝试从localStorage读取配置
            const savedConfig = localStorage.getItem('cameraMonitorConfig');
            
            if (savedConfig) {
                // 解析保存的配置
                const parsedConfig = JSON.parse(savedConfig);
                
                // 合并配置:保存的配置覆盖默认配置
                this.config = {
                    ...this.config,          // 默认配置
                    ...parsedConfig          // 用户保存的配置
                };
                
                console.log('[主应用] 配置加载成功:', this.config);
            } else {
                console.log('[主应用] 无保存的配置,使用默认配置');
            }
            
            // 更新截图管理器最大数量限制
            this.screenshotManager.maxCount = this.config.maxScreenshots;
            
        } catch (error) {
            console.warn('[主应用] 配置加载失败,使用默认配置:', error);
            // 配置加载失败不影响应用运行,继续使用默认配置
        }
    }
    
    /**
     * 保存配置到本地存储
     * 将当前配置以JSON格式保存到localStorage
     */
    saveConfiguration() {
        try {
            // 将配置对象转换为JSON字符串
            const configJson = JSON.stringify(this.config);
            
            // 保存到localStorage
            localStorage.setItem('cameraMonitorConfig', configJson);
            
            console.log('[主应用] 配置已保存');
            
        } catch (error) {
            console.error('[主应用] 保存配置失败:', error);
            this.showNotification('保存配置失败,存储空间可能不足', 'error');
        }
    }
    
    /**
     * 初始化所有UI组件
     * 设置初始状态、绑定事件监听器、更新显示
     */
    initializeUIComponents() {
        console.log('[主应用] 初始化UI组件...');
        
        // -----------------------------
        // 控制按钮初始化
        // -----------------------------
        // 启动/停止按钮
        const startBtn = document.getElementById('startBtn');
        const stopBtn = document.getElementById('stopBtn');
        
        if (startBtn) {
            // 绑定点击事件,使用箭头函数保持this指向
            startBtn.addEventListener('click', () => this.startCamera());
            console.log('[主应用] 启动按钮初始化完成');
        }
        
        if (stopBtn) {
            stopBtn.addEventListener('click', () => this.stopCamera());
            console.log('[主应用] 停止按钮初始化完成');
        }
        
        // 截图按钮
        const captureBtn = document.getElementById('captureBtn');
        if (captureBtn) {
            captureBtn.addEventListener('click', () => this.captureScreenshot());
            // 初始状态禁用,摄像头启动后启用
            captureBtn.disabled = true;
            console.log('[主应用] 截图按钮初始化完成');
        }
        
        // 录制按钮
        const recordBtn = document.getElementById('recordBtn');
        if (recordBtn) {
            recordBtn.addEventListener('click', () => this.toggleRecording());
            recordBtn.disabled = true;
            console.log('[主应用] 录制按钮初始化完成');
        }
        
        // 清空截图按钮
        const clearAllBtn = document.getElementById('clearAllBtn');
        if (clearAllBtn) {
            clearAllBtn.addEventListener('click', () => this.clearAllScreenshots());
            console.log('[主应用] 清空截图按钮初始化完成');
        }
        
        // 导出截图按钮
        const exportAllBtn = document.getElementById('exportAllBtn');
        if (exportAllBtn) {
            exportAllBtn.addEventListener('click', () => this.exportAllScreenshots());
            console.log('[主应用] 导出截图按钮初始化完成');
        }
        
        // 设置按钮
        const settingsBtn = document.getElementById('settingsBtn');
        if (settingsBtn) {
            settingsBtn.addEventListener('click', () => this.showSettingsModal());
            console.log('[主应用] 设置按钮初始化完成');
        }
        
        // -----------------------------
        // 设置相关元素初始化
        // -----------------------------
        // 分辨率选择框
        const resolutionSelect = document.getElementById('resolutionSelect');
        if (resolutionSelect) {
            // 设置默认选中值
            resolutionSelect.value = this.config.defaultResolution;
            // 监听变化事件
            resolutionSelect.addEventListener('change', (e) => {
                this.config.defaultResolution = e.target.value;
                this.saveConfiguration();
                this.showNotification(`分辨率设置为: ${e.target.value}`, 'info');
            });
            console.log('[主应用] 分辨率选择框初始化完成');
        }
        
        // 帧率选择框
        const frameRateSelect = document.getElementById('frameRateSelect');
        if (frameRateSelect) {
            frameRateSelect.value = this.config.defaultFrameRate;
            frameRateSelect.addEventListener('change', (e) => {
                this.config.defaultFrameRate = parseInt(e.target.value);
                this.saveConfiguration();
                this.showNotification(`帧率设置为: ${e.target.value} FPS`, 'info');
            });
            console.log('[主应用] 帧率选择框初始化完成');
        }
        
        // 设置模态框关闭按钮
        const closeSettings = document.getElementById('closeSettings');
        if (closeSettings) {
            closeSettings.addEventListener('click', () => this.hideSettingsModal());
            console.log('[主应用] 设置模态框关闭按钮初始化完成');
        }
        
        // 设置保存按钮
        const saveSettings = document.getElementById('saveSettings');
        if (saveSettings) {
            saveSettings.addEventListener('click', () => this.applySettings());
            console.log('[主应用] 设置保存按钮初始化完成');
        }
        
        // -----------------------------
        // 视频元素初始化
        // -----------------------------
        // 设置视频元素属性,优化播放性能
        this.videoElement.muted = true;        // 静音,避免自动播放策略限制
        this.videoElement.playsInline = true;  // 在移动端内联播放
        this.videoElement.setAttribute('playsinline', 'true'); // 兼容性属性
        
        // 监听视频元素错误事件
        this.videoElement.addEventListener('error', (e) => {
            console.error('[主应用] 视频元素错误:', e);
            this.showNotification('视频播放错误,请检查摄像头', 'error');
        });
        
        // 监听视频元素加载元数据完成事件
        this.videoElement.addEventListener('loadedmetadata', () => {
            console.log('[主应用] 视频元数据加载完成:', {
                width: this.videoElement.videoWidth,
                height: this.videoElement.videoHeight,
                duration: this.videoElement.duration
            });
        });
        
        console.log('[主应用] UI组件初始化完成');
    }
    
    /**
     * 更新可用的摄像头设备列表
     * 获取系统所有视频输入设备,并更新设备选择器
     */
    async updateDeviceList() {
        console.log('[主应用] 更新设备列表...');
        
        try {
            // 通过摄像头控制器获取设备列表
            this.availableDevices = await this.cameraController.getDevices();
            
            // 获取设备选择器DOM元素
            const deviceSelect = document.getElementById('cameraDeviceSelect');
            if (!deviceSelect) {
                console.warn('[主应用] 未找到设备选择器元素');
                return;
            }
            
            // 保存当前选中的设备ID
            const currentSelection = deviceSelect.value;
            
            // 清空现有选项(保留第一个默认选项)
            deviceSelect.innerHTML = '<option value="default">默认摄像头</option>';
            
            // 添加所有找到的设备到选择器
            this.availableDevices.forEach((device, index) => {
                // 创建option元素
                const option = document.createElement('option');
                option.value = device.deviceId;
                
                // 设置显示文本:如果有设备标签则使用标签,否则使用序号
                const label = device.label || `摄像头 ${index + 1}`;
                option.textContent = label;
                
                // 添加到选择器
                deviceSelect.appendChild(option);
            });
            
            // 恢复之前的选中状态,如果设备仍然存在
            if (currentSelection && this.availableDevices.some(d => d.deviceId === currentSelection)) {
                deviceSelect.value = currentSelection;
            } else if (this.availableDevices.length > 0) {
                // 否则选择第一个设备
                deviceSelect.value = this.availableDevices[0].deviceId;
            }
            
            // 保存选中的设备ID到配置
            this.selectedDeviceId = deviceSelect.value === 'default' ? null : deviceSelect.value;
            
            console.log(`[主应用] 设备列表更新完成,找到 ${this.availableDevices.length} 个设备`);
            
        } catch (error) {
            console.error('[主应用] 更新设备列表失败:', error);
            // 失败不影响主要功能,继续使用默认设备
        }
    }
    
    /**
     * 从本地存储加载截图数据
     * 显示截图数量和预览
     */
    loadScreenshots() {
        console.log('[主应用] 加载截图数据...');
        
        // 截图管理器已经在构造函数中加载了数据
        // 这里只需要更新UI显示
        const count = this.screenshotManager.getCount();
        
        // 更新截图数量显示
        const countElement = document.getElementById('screenshotCount');
        if (countElement) {
            countElement.textContent = `${count} 张`;
        }
        
        // 更新统计信息中的截图数量
        const totalCaptures = document.getElementById('totalCaptures');
        if (totalCaptures) {
            totalCaptures.textContent = count;
        }
        
        // 更新截图列表显示
        this.updateScreenshotList();
        
        // 更新内存统计
        const totalSize = this.screenshotManager.getAll().reduce((sum, ss) => sum + (ss.size || 0), 0);
        this.performance.updateMemoryStats(count, totalSize);
        
        console.log(`[主应用] 截图数据加载完成,共 ${count} 张,总大小 ${totalSize} 字节`);
    }
    
    /**
     * 恢复应用状态
     * 根据配置决定是否自动启动摄像头等
     */
    restoreAppState() {
        console.log('[主应用] 恢复应用状态...');
        
        // 检查是否启用自动启动
        if (this.config.autoStart) {
            console.log('[主应用] 配置了自动启动,将在500ms后启动摄像头');
            
            // 延迟启动,确保页面完全加载
            setTimeout(() => {
                this.startCamera().catch(error => {
                    console.warn('[主应用] 自动启动失败:', error);
                });
            }, 500);
        }
        
        // 更新UI状态显示
        this.updateUIState();
        
        console.log('[主应用] 应用状态恢复完成');
    }
    
    /**
     * 启动运行时间计数器
     * 每秒更新运行时间显示
     */
    startUptimeCounter() {
        console.log('[主应用] 启动运行时间计数器...');
        
        // 清除已有的计时器
        if (this.uptimeInterval) {
            clearInterval(this.uptimeInterval);
        }
        
        // 重置运行时间
        this.uptimeSeconds = 0;
        
        // 创建新的计时器,每秒更新一次
        this.uptimeInterval = setInterval(() => {
            this.uptimeSeconds++;
            this.updateUptimeDisplay();
        }, 1000);
        
        // 立即更新一次显示
        this.updateUptimeDisplay();
        
        console.log('[主应用] 运行时间计数器已启动');
    }
    
    /**
     * 更新运行时间显示
     * 将秒数转换为 HH:MM:SS 格式
     */
    updateUptimeDisplay() {
        const uptimeElement = document.getElementById('uptime');
        if (!uptimeElement) return;
        
        // 计算小时、分钟、秒
        const hours = Math.floor(this.uptimeSeconds / 3600);
        const minutes = Math.floor((this.uptimeSeconds % 3600) / 60);
        const seconds = this.uptimeSeconds % 60;
        
        // 格式化为两位数
        const format = (num) => num.toString().padStart(2, '0');
        
        // 更新显示
        uptimeElement.textContent = `${format(hours)}:${format(minutes)}:${format(seconds)}`;
    }
    
    /**
     * 启动摄像头
     * 核心功能:请求摄像头权限、开始视频流、更新UI
     */
    async startCamera() {
        console.log('[主应用] 用户请求启动摄像头...');
        
        // 检查是否已经在流传输状态
        if (this.appState === 'streaming' || this.appState === 'recording') {
            console.log('[主应用] 摄像头已经在运行中');
            this.showNotification('摄像头已经在运行中', 'info');
            return;
        }
        
        // 更新应用状态
        this.appState = 'starting';
        this.updateUIState();
        
        // 显示启动状态
        this.showNotification('正在启动摄像头...', 'info');
        
        try {
            // 性能监控:标记开始时间
            this.performance.markStart('camera_start');
            
            // -----------------------------
            // 解析分辨率设置
            // -----------------------------
            // 从配置中获取分辨率字符串(如 "1280x720")
            const resolution = this.config.defaultResolution;
            let width, height;
            
            if (resolution && resolution.includes('x')) {
                // 分割字符串获取宽高
                const [w, h] = resolution.split('x').map(Number);
                width = w;
                height = h;
                console.log(`[主应用] 使用配置的分辨率: ${width}x${height}`);
            } else {
                // 默认分辨率
                width = 1280;
                height = 720;
                console.log(`[主应用] 使用默认分辨率: ${width}x${height}`);
            }
            
            // -----------------------------
            // 构建摄像头启动选项
            // -----------------------------
            const options = {
                deviceId: this.selectedDeviceId,  // 选中的设备ID
                width: width,                     // 视频宽度
                height: height,                   // 视频高度
                frameRate: this.config.defaultFrameRate  // 帧率
            };
            
            console.log('[主应用] 摄像头启动选项:', options);
            
            // -----------------------------
            // 启动摄像头控制器
            // -----------------------------
            // 调用摄像头控制器的start方法
            await this.cameraController.start(options);
            
            // 性能监控:记录启动耗时
            const duration = this.performance.markEnd('camera_start');
            console.log(`[主应用] 摄像头启动耗时: ${duration.toFixed(2)}ms`);
            
            // -----------------------------
            // 更新应用状态
            // -----------------------------
            this.appState = 'streaming';
            
            // 更新UI状态(启用按钮、更新状态显示等)
            this.updateUIState();
            
            // 显示成功通知
            this.showNotification('摄像头启动成功', 'success');
            
            // 性能监控:开始帧率统计
            this.startFrameRateMonitoring();
            
            console.log('[主应用] 摄像头启动流程完成');
            
        } catch (error) {
            // -----------------------------
            // 错误处理
            // -----------------------------
            console.error('[主应用] 启动摄像头失败:', error);
            
            // 恢复应用状态
            this.appState = 'idle';
            this.updateUIState();
            
            // 显示错误通知
            this.showNotification(`启动失败: ${error.message}`, 'error');
            
            // 重新抛出错误,允许调用者处理
            throw error;
        }
    }
    
    /**
     * 停止摄像头
     * 停止视频流、更新UI状态
     */
    async stopCamera() {
        console.log('[主应用] 用户请求停止摄像头...');
        
        // 检查是否在运行状态
        if (this.appState !== 'streaming' && this.appState !== 'recording') {
            console.log('[主应用] 摄像头未在运行');
            return;
        }
        
        // 显示停止状态
        this.showNotification('正在停止摄像头...', 'info');
        
        try {
            // 性能监控:标记开始时间
            this.performance.markStart('camera_stop');
            
            // 停止摄像头控制器
            this.cameraController.stop();
            
            // 停止帧率监控
            this.stopFrameRateMonitoring();
            
            // 更新应用状态
            this.appState = 'idle';
            
            // 更新UI状态
            this.updateUIState();
            
            // 性能监控:记录停止耗时
            const duration = this.performance.markEnd('camera_stop');
            console.log(`[主应用] 摄像头停止耗时: ${duration.toFixed(2)}ms`);
            
            // 显示成功通知
            this.showNotification('摄像头已停止', 'info');
            
            console.log('[主应用] 摄像头停止流程完成');
            
        } catch (error) {
            console.error('[主应用] 停止摄像头失败:', error);
            this.showNotification('停止摄像头时发生错误', 'error');
        }
    }
    
    /**
     * 开始帧率监控
     * 使用requestAnimationFrame监控实际帧率
     */
    startFrameRateMonitoring() {
        console.log('[主应用] 开始帧率监控...');
        
        // 定义帧率监控函数
        const monitorFrameRate = () => {
            // 只在摄像头运行时监控
            if (this.appState === 'streaming' || this.appState === 'recording') {
                // 增加帧计数
                this.performance.incrementFrameCount();
                
                // 继续下一帧监控
                requestAnimationFrame(monitorFrameRate);
            }
        };
        
        // 启动监控
        requestAnimationFrame(monitorFrameRate);
        
        console.log('[主应用] 帧率监控已启动');
    }
    
    /**
     * 停止帧率监控
     */
    stopFrameRateMonitoring() {
        console.log('[主应用] 停止帧率监控');
        // 帧率监控通过appState控制,无需额外操作
    }
    
    /**
     * 更新UI状态
     * 根据当前应用状态更新所有UI元素的显示和状态
     */
    updateUIState() {
        console.log(`[主应用] 更新UI状态,当前状态: ${this.appState}`);
        
        // -----------------------------
        // 控制按钮状态
        // -----------------------------
        const startBtn = document.getElementById('startBtn');
        const stopBtn = document.getElementById('stopBtn');
        const captureBtn = document.getElementById('captureBtn');
        const recordBtn = document.getElementById('recordBtn');
        
        if (this.appState === 'starting') {
            // 启动中状态
            if (startBtn) {
                startBtn.disabled = true;
                startBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 启动中...';
            }
            if (stopBtn) stopBtn.style.display = 'none';
            if (captureBtn) captureBtn.disabled = true;
            if (recordBtn) recordBtn.disabled = true;
            
        } else if (this.appState === 'streaming' || this.appState === 'recording') {
            // 运行中状态
            if (startBtn) startBtn.style.display = 'none';
            if (stopBtn) {
                stopBtn.style.display = 'inline-flex';
                stopBtn.disabled = false;
            }
            if (captureBtn) captureBtn.disabled = false;
            if (recordBtn) recordBtn.disabled = false;
            
            // 如果是录制状态,更新录制按钮样式
            if (this.appState === 'recording' && recordBtn) {
                recordBtn.innerHTML = '<i class="fas fa-stop"></i> 停止录制';
                recordBtn.classList.add('recording');
            } else if (recordBtn) {
                recordBtn.innerHTML = '<i class="fas fa-video"></i> 开始录制';
                recordBtn.classList.remove('recording');
            }
            
        } else {
            // 空闲状态
            if (startBtn) {
                startBtn.style.display = 'inline-flex';
                startBtn.disabled = false;
                startBtn.innerHTML = '<i class="fas fa-play"></i> 启动摄像头';
            }
            if (stopBtn) stopBtn.style.display = 'none';
            if (captureBtn) captureBtn.disabled = true;
            if (recordBtn) {
                recordBtn.disabled = true;
                recordBtn.classList.remove('recording');
                recordBtn.innerHTML = '<i class="fas fa-video"></i> 开始录制';
            }
        }
        
        // -----------------------------
        // 状态覆盖层显示
        // -----------------------------
        const statusOverlay = document.getElementById('statusOverlay');
        if (statusOverlay) {
            switch (this.appState) {
                case 'starting':
                    statusOverlay.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 启动中...';
                    break;
                case 'streaming':
                    statusOverlay.innerHTML = '<i class="fas fa-circle text-green-500"></i> 实时监控中';
                    break;
                case 'recording':
                    statusOverlay.innerHTML = '<i class="fas fa-circle text-red-500"></i> 录制中';
                    break;
                default:
                    statusOverlay.innerHTML = '<i class="fas fa-circle text-gray-500"></i> 已停止';
            }
        }
        
        // -----------------------------
        // 录制指示器显示
        // -----------------------------
        const recordingIndicator = document.getElementById('recordingIndicator');
        if (recordingIndicator) {
            if (this.appState === 'recording') {
                recordingIndicator.classList.remove('hidden');
            } else {
                recordingIndicator.classList.add('hidden');
            }
        }
        
        // -----------------------------
        // 统计信息更新
        // -----------------------------
        const recordStatus = document.getElementById('recordStatus');
        if (recordStatus) {
            recordStatus.textContent = this.appState === 'recording' ? '录制中' : '未开始';
            recordStatus.className = this.appState === 'recording' ? 'text-red-600' : 'text-gray-600';
        }
        
        const deviceStatus = document.getElementById('deviceStatus');
        if (deviceStatus) {
            deviceStatus.textContent = 
                this.appState === 'streaming' || this.appState === 'recording' ? '运行中' : '就绪';
            deviceStatus.className = 
                this.appState === 'streaming' || this.appState === 'recording' ? 'text-green-600' : 'text-gray-600';
        }
        
        console.log('[主应用] UI状态更新完成');
    }
    
    /**
     * 截图功能
     * 捕获当前视频帧并保存
     */
    async captureScreenshot() {
        console.log('[主应用] 用户请求截图...');
        
        // 检查摄像头状态
        if (this.appState !== 'streaming' && this.appState !== 'recording') {
            this.showNotification('请先启动摄像头', 'warning');
            return;
        }
        
        // 性能监控:标记开始时间
        this.performance.markStart('capture_screenshot');
        
        try {
            // 显示截图状态
            this.showNotification('正在截图...', 'info');
            
            // -----------------------------
            // 捕获当前视频帧
            // -----------------------------
            // 使用摄像头控制器捕获帧
            const dataUrl = await this.cameraController.captureFrame({
                format: 'image/jpeg',           // 使用JPEG格式,压缩率高
                quality: this.config.videoQuality  // 使用配置的质量参数
            });
            
            // 获取视频实际尺寸
            const width = this.videoElement.videoWidth;
            const height = this.videoElement.videoHeight;
            
            console.log(`[主应用] 截图捕获完成: ${width}x${height}`);
            
            // -----------------------------
            // 保存截图
            // -----------------------------
            const screenshot = {
                dataUrl: dataUrl,      // 图片数据URL
                width: width,          // 图片宽度
                height: height,        // 图片高度
                format: 'jpeg'         // 图片格式
            };
            
            // 添加到截图管理器
            const screenshotId = this.screenshotManager.addScreenshot(screenshot);
            
            // -----------------------------
            // 更新UI显示
            // -----------------------------
            // 更新截图列表
            this.updateScreenshotList();
            
            // 更新截图数量显示
            const count = this.screenshotManager.getCount();
            const countElement = document.getElementById('screenshotCount');
            if (countElement) {
                countElement.textContent = `${count} 张`;
            }
            
            // 更新统计信息
            const totalCaptures = document.getElementById('totalCaptures');
            if (totalCaptures) {
                totalCaptures.textContent = count;
            }
            
            // 更新内存统计
            const totalSize = this.screenshotManager.getAll().reduce((sum, ss) => sum + (ss.size || 0), 0);
            this.performance.updateMemoryStats(count, totalSize);
            
            // 更新按钮状态(如果有截图则启用清除/导出按钮)
            this.updateScreenshotButtons();
            
            // 性能监控:记录截图耗时
            const duration = this.performance.markEnd('capture_screenshot');
            console.log(`[主应用] 截图耗时: ${duration.toFixed(2)}ms`);
            
            // 显示成功通知
            this.showNotification(`截图保存成功 (${width}x${height})`, 'success');
            
            console.log('[主应用] 截图流程完成,ID:', screenshotId);
            
        } catch (error) {
            console.error('[主应用] 截图失败:', error);
            this.showNotification(`截图失败: ${error.message}`, 'error');
        }
    }
    
    /**
     * 更新截图列表显示
     * 在侧边栏显示所有截图的缩略图
     */
    updateScreenshotList() {
        console.log('[主应用] 更新截图列表显示...');
        
        const container = document.getElementById('screenshotList');
        if (!container) {
            console.warn('[主应用] 未找到截图列表容器');
            return;
        }
        
        // 获取所有截图
        const screenshots = this.screenshotManager.getAll();
        const count = screenshots.length;
        
        // 如果没有截图,显示空状态
        if (count === 0) {
            container.innerHTML = `
                <div class="text-center py-8 text-gray-500">
                    <i class="fas fa-camera text-4xl mb-2 opacity-50"></i>
                    <p>暂无截图</p>
                    <p class="text-sm">点击"拍摄截图"按钮开始收集</p>
                </div>
            `;
            console.log('[主应用] 截图列表为空,显示空状态');
            return;
        }
        
        // 清空容器
        container.innerHTML = '';
        
        // 创建文档片段,提高DOM操作性能
        const fragment = document.createDocumentFragment();
        
        // 为每个截图创建显示项
        screenshots.forEach((screenshot, index) => {
            // 创建截图项容器
            const item = document.createElement('div');
            item.className = 'screenshot-item bg-gray-50 rounded-lg p-3 border border-gray-200 mb-3';
            item.dataset.id = screenshot.id;
            
            // 截图的HTML结构
            item.innerHTML = `
                <div class="flex items-center space-x-3">
                    <!-- 缩略图 -->
                    <img src="${screenshot.dataUrl}" 
                         alt="摄像头截图 ${index + 1}"
                         class="w-16 h-16 object-cover rounded-lg cursor-pointer screenshot-thumb"
                         data-id="${screenshot.id}"
                         title="点击查看大图">
                    
                    <!-- 截图信息 -->
                    <div class="flex-1 min-w-0">
                        <p class="text-sm font-medium text-gray-800 truncate">
                            截图 ${screenshot.id.substring(3, 11)}...
                        </p>
                        <p class="text-xs text-gray-500 mt-1">
                            ${screenshot.dateString}
                        </p>
                        <p class="text-xs text-gray-500">
                            ${screenshot.width} × ${screenshot.height}
                        </p>
                    </div>
                    
                    <!-- 操作按钮 -->
                    <div class="flex space-x-1">
                        <!-- 查看按钮 -->
                        <button class="p-2 text-blue-500 hover:bg-blue-100 rounded transition-colors screenshot-view"
                                data-id="${screenshot.id}"
                                title="查看大图">
                            <i class="fas fa-eye"></i>
                        </button>
                        
                        <!-- 删除按钮 -->
                        <button class="p-2 text-red-500 hover:bg-red-100 rounded transition-colors screenshot-delete"
                                data-id="${screenshot.id}"
                                title="删除截图">
                            <i class="fas fa-trash"></i>
                        </button>
                    </div>
                </div>
            `;
            
            // 添加到文档片段
            fragment.appendChild(item);
        });
        
        // 一次性添加到容器
        container.appendChild(fragment);
        
        // 为缩略图添加点击事件(事件委托)
        container.addEventListener('click', (e) => {
            const target = e.target;
            
            // 如果点击的是缩略图
            if (target.classList.contains('screenshot-thumb')) {
                const id = target.dataset.id;
                this.viewScreenshot(id);
            }
            
            // 如果点击的是查看按钮
            else if (target.classList.contains('screenshot-view') || 
                     target.closest('.screenshot-view')) {
                const btn = target.classList.contains('screenshot-view') ? target : target.closest('.screenshot-view');
                const id = btn.dataset.id;
                this.viewScreenshot(id);
            }
            
            // 如果点击的是删除按钮
            else if (target.classList.contains('screenshot-delete') || 
                     target.closest('.screenshot-delete')) {
                const btn = target.classList.contains('screenshot-delete') ? target : target.closest('.screenshot-delete');
                const id = btn.dataset.id;
                this.deleteScreenshot(id);
            }
        });
        
        console.log(`[主应用] 截图列表更新完成,显示 ${count} 张截图`);
    }
    
    /**
     * 更新截图相关按钮状态
     * 根据是否有截图来启用/禁用按钮
     */
    updateScreenshotButtons() {
        console.log('[主应用] 更新截图按钮状态...');
        
        const count = this.screenshotManager.getCount();
        const hasScreenshots = count > 0;
        
        // 清空所有按钮
        const clearAllBtn = document.getElementById('clearAllBtn');
        if (clearAllBtn) {
            clearAllBtn.disabled = !hasScreenshots;
        }
        
        // 导出全部按钮
        const exportAllBtn = document.getElementById('exportAllBtn');
        if (exportAllBtn) {
            exportAllBtn.disabled = !hasScreenshots;
        }
        
        console.log(`[主应用] 截图按钮状态更新: 有截图=${hasScreenshots}`);
    }
​
// ============================================
// CameraMonitorApp 类继续 - 剩余功能实现
// ============================================
    
    /**
     * 查看截图 - 在大图模态框中显示截图
     * @param {string} id - 要查看的截图ID
     */
    viewScreenshot(id) {
        console.log(`[主应用] 用户请求查看截图: ${id}`);
        
        // 从截图管理器获取截图数据
        const screenshot = this.screenshotManager.getScreenshot(id);
        if (!screenshot) {
            console.warn(`[主应用] 未找到截图: ${id}`);
            this.showNotification('截图不存在或已被删除', 'warning');
            return;
        }
        
        // 获取模态框相关元素
        const modal = document.getElementById('screenshotViewerModal');
        const fullSizeImage = document.getElementById('fullSizeScreenshot');
        const screenshotInfo = document.getElementById('screenshotInfo');
        
        if (!modal || !fullSizeImage || !screenshotInfo) {
            console.error('[主应用] 截图查看器元素缺失');
            return;
        }
        
        // 设置大图
        fullSizeImage.src = screenshot.dataUrl;
        fullSizeImage.alt = `截图 ${id}`;
        
        // 设置图片信息
        screenshotInfo.textContent = 
            `拍摄时间: ${screenshot.dateString} | 分辨率: ${screenshot.width}x${screenshot.height} | 格式: ${screenshot.format.toUpperCase()}`;
        
        // 设置下载按钮的ID
        const downloadBtn = document.getElementById('downloadScreenshot');
        if (downloadBtn) {
            downloadBtn.dataset.screenshotId = id;
        }
        
        // 显示模态框
        modal.classList.remove('hidden');
        
        console.log(`[主应用] 截图查看器已打开: ${id}`);
    }
    
    /**
     * 关闭截图查看器
     */
    hideScreenshotViewer() {
        console.log('[主应用] 关闭截图查看器');
        
        const modal = document.getElementById('screenshotViewerModal');
        if (modal) {
            modal.classList.add('hidden');
        }
    }
    
    /**
     * 下载当前查看的截图
     */
    downloadCurrentScreenshot() {
        // 获取当前查看的截图ID
        const downloadBtn = document.getElementById('downloadScreenshot');
        if (!downloadBtn || !downloadBtn.dataset.screenshotId) {
            console.warn('[主应用] 未找到当前查看的截图ID');
            return;
        }
        
        const screenshotId = downloadBtn.dataset.screenshotId;
        console.log(`[主应用] 用户请求下载截图: ${screenshotId}`);
        
        // 获取截图数据
        const screenshot = this.screenshotManager.getScreenshot(screenshotId);
        if (!screenshot) {
            this.showNotification('截图不存在', 'error');
            return;
        }
        
        try {
            // 创建下载链接
            const link = document.createElement('a');
            link.href = screenshot.dataUrl;
            
            // 生成文件名: screenshot_时间戳_分辨率.格式
            const timestamp = new Date(screenshot.timestamp)
                .toISOString()
                .replace(/[:.]/g, '-')
                .substring(0, 19);
            const filename = `screenshot_${timestamp}_${screenshot.width}x${screenshot.height}.${screenshot.format}`;
            
            link.download = filename;
            
            // 触发下载
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            
            console.log(`[主应用] 截图下载完成: ${filename}`);
            this.showNotification(`截图已下载: ${filename}`, 'success');
            
        } catch (error) {
            console.error('[主应用] 下载截图失败:', error);
            this.showNotification('下载失败: ' + error.message, 'error');
        }
    }
    
    /**
     * 删除截图
     * @param {string} id - 要删除的截图ID
     */
    deleteScreenshot(id) {
        console.log(`[主应用] 用户请求删除截图: ${id}`);
        
        // 确认对话框
        if (!confirm('确定要删除这张截图吗?此操作不可撤销。')) {
            console.log('[主应用] 用户取消了删除操作');
            return;
        }
        
        // 执行删除
        const deleted = this.screenshotManager.deleteScreenshot(id);
        
        if (deleted) {
            // 更新UI
            this.updateScreenshotList();
            
            // 更新截图数量显示
            const count = this.screenshotManager.getCount();
            const countElement = document.getElementById('screenshotCount');
            if (countElement) {
                countElement.textContent = `${count} 张`;
            }
            
            // 更新统计信息
            const totalCaptures = document.getElementById('totalCaptures');
            if (totalCaptures) {
                totalCaptures.textContent = count;
            }
            
            // 更新按钮状态
            this.updateScreenshotButtons();
            
            // 更新内存统计
            const totalSize = this.screenshotManager.getAll().reduce((sum, ss) => sum + (ss.size || 0), 0);
            this.performance.updateMemoryStats(count, totalSize);
            
            console.log(`[主应用] 截图已删除: ${id}`);
            this.showNotification('截图已删除', 'info');
            
        } else {
            console.warn(`[主应用] 删除失败,未找到截图: ${id}`);
            this.showNotification('截图不存在或删除失败', 'error');
        }
    }
    
    /**
     * 清空所有截图
     */
    clearAllScreenshots() {
        console.log('[主应用] 用户请求清空所有截图');
        
        const count = this.screenshotManager.getCount();
        if (count === 0) {
            this.showNotification('没有可清空的截图', 'info');
            return;
        }
        
        // 确认对话框
        if (!confirm(`确定要清空所有截图吗?将删除 ${count} 张截图,此操作不可撤销。`)) {
            console.log('[主应用] 用户取消了清空操作');
            return;
        }
        
        // 执行清空
        this.screenshotManager.clearAll();
        
        // 更新UI
        this.updateScreenshotList();
        
        // 更新截图数量显示
        const countElement = document.getElementById('screenshotCount');
        if (countElement) {
            countElement.textContent = '0 张';
        }
        
        // 更新统计信息
        const totalCaptures = document.getElementById('totalCaptures');
        if (totalCaptures) {
            totalCaptures.textContent = '0';
        }
        
        // 更新按钮状态
        this.updateScreenshotButtons();
        
        // 更新内存统计
        this.performance.updateMemoryStats(0, 0);
        
        console.log(`[主应用] 已清空所有截图,共 ${count} 张`);
        this.showNotification(`已清空所有截图 (${count}张)`, 'info');
    }
    
    /**
     * 导出所有截图为ZIP文件
     */
    async exportAllScreenshots() {
        console.log('[主应用] 用户请求导出所有截图');
        
        const count = this.screenshotManager.getCount();
        if (count === 0) {
            this.showNotification('没有可导出的截图', 'warning');
            return;
        }
        
        try {
            // 显示导出进度
            this.showNotification(`正在打包 ${count} 张截图...`, 'info');
            
            // 禁用导出按钮,防止重复点击
            const exportBtn = document.getElementById('exportAllBtn');
            const originalText = exportBtn ? exportBtn.innerHTML : '';
            if (exportBtn) {
                exportBtn.disabled = true;
                exportBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 打包中...';
            }
            
            // 性能监控:标记开始时间
            this.performance.markStart('export_screenshots');
            
            // -----------------------------
            // 生成ZIP文件
            // -----------------------------
            const zipBlob = await this.screenshotManager.exportToZip();
            
            // 性能监控:记录导出耗时
            const duration = this.performance.markEnd('export_screenshots');
            console.log(`[主应用] 截图导出耗时: ${duration.toFixed(2)}ms`);
            
            // -----------------------------
            // 创建下载链接
            // -----------------------------
            const url = URL.createObjectURL(zipBlob);
            
            // 生成文件名: screenshots_时间戳.zip
            const timestamp = new Date().toISOString()
                .replace(/[:.]/g, '-')
                .substring(0, 19);
            const filename = `screenshots_${timestamp}.zip`;
            
            // 创建下载链接并触发下载
            const link = document.createElement('a');
            link.href = url;
            link.download = filename;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            
            // 释放URL对象(延迟执行)
            setTimeout(() => {
                URL.revokeObjectURL(url);
                console.log(`[主应用] URL对象已释放: ${url.substring(0, 50)}...`);
            }, 1000);
            
            // 恢复按钮状态
            if (exportBtn) {
                setTimeout(() => {
                    exportBtn.disabled = false;
                    exportBtn.innerHTML = originalText;
                }, 1000);
            }
            
            console.log(`[主应用] 截图导出完成: ${filename} (${(zipBlob.size / 1024 / 1024).toFixed(2)}MB)`);
            this.showNotification(`导出成功: ${filename}`, 'success');
            
        } catch (error) {
            console.error('[主应用] 导出截图失败:', error);
            
            // 恢复按钮状态
            const exportBtn = document.getElementById('exportAllBtn');
            if (exportBtn) {
                exportBtn.disabled = false;
                exportBtn.innerHTML = '<i class="fas fa-download mr-2"></i>导出全部截图';
            }
            
            let errorMessage = '导出失败';
            if (error.message.includes('JSZip库未加载')) {
                errorMessage = '导出需要JSZip库支持,请确保已加载该库';
            } else {
                errorMessage = `导出失败: ${error.message}`;
            }
            
            this.showNotification(errorMessage, 'error');
        }
    }
    
    /**
     * 切换录制状态
     * 开始或停止视频录制
     */
    toggleRecording() {
        console.log('[主应用] 用户请求切换录制状态');
        
        // 检查摄像头状态
        if (this.appState !== 'streaming' && this.appState !== 'recording') {
            this.showNotification('请先启动摄像头', 'warning');
            return;
        }
        
        // 如果当前正在录制,则停止录制
        if (this.appState === 'recording') {
            this.stopRecording();
        } 
        // 如果当前未在录制,则开始录制
        else {
            this.startRecording();
        }
    }
    
    /**
     * 开始录制视频
     */
    startRecording() {
        console.log('[主应用] 开始录制视频...');
        
        try {
            // 性能监控:标记开始时间
            this.performance.markStart('start_recording');
            
            // 通过摄像头控制器开始录制
            const success = this.cameraController.startRecording();
            
            if (success) {
                // 更新应用状态
                this.appState = 'recording';
                
                // 更新UI状态
                this.updateUIState();
                
                // 性能监控:记录开始录制耗时
                const duration = this.performance.markEnd('start_recording');
                console.log(`[主应用] 开始录制耗时: ${duration.toFixed(2)}ms`);
                
                this.showNotification('开始录制视频', 'info');
                
            } else {
                throw new Error('开始录制失败');
            }
            
        } catch (error) {
            console.error('[主应用] 开始录制失败:', error);
            this.showNotification('开始录制失败: ' + error.message, 'error');
        }
    }
    
    /**
     * 停止录制视频
     */
    stopRecording() {
        console.log('[主应用] 停止录制视频...');
        
        try {
            // 性能监控:标记开始时间
            this.performance.markStart('stop_recording');
            
            // 通过摄像头控制器停止录制
            this.cameraController.stopRecording();
            
            // 更新应用状态
            this.appState = 'streaming';
            
            // 更新UI状态
            this.updateUIState();
            
            // 性能监控:记录停止录制耗时
            const duration = this.performance.markEnd('stop_recording');
            console.log(`[主应用] 停止录制耗时: ${duration.toFixed(2)}ms`);
            
            this.showNotification('录制已停止,视频正在保存...', 'info');
            
        } catch (error) {
            console.error('[主应用] 停止录制失败:', error);
            this.showNotification('停止录制失败: ' + error.message, 'error');
        }
    }
    
    /**
     * 显示设置模态框
     */
    showSettingsModal() {
        console.log('[主应用] 显示设置模态框');
        
        const modal = document.getElementById('settingsModal');
        if (!modal) {
            console.error('[主应用] 未找到设置模态框');
            return;
        }
        
        // 更新模态框中的设置值
        this.updateSettingsModalValues();
        
        // 显示模态框
        modal.classList.remove('hidden');
        
        // 设置模态框显示时的动画效果
        modal.style.opacity = '0';
        setTimeout(() => {
            modal.style.opacity = '1';
            modal.style.transition = 'opacity 0.3s ease';
        }, 10);
    }
    
    /**
     * 更新设置模态框中的值
     */
    updateSettingsModalValues() {
        console.log('[主应用] 更新设置模态框值');
        
        // 摄像头设备选择
        const cameraDeviceSelect = document.getElementById('cameraDeviceSelect');
        if (cameraDeviceSelect) {
            cameraDeviceSelect.value = this.selectedDeviceId || 'default';
        }
        
        // 视频质量选择
        const videoQuality = document.getElementById('videoQuality');
        if (videoQuality) {
            videoQuality.value = this.config.videoQuality === 0.92 ? 'high' : 
                                this.config.videoQuality === 0.8 ? 'medium' : 'low';
        }
        
        // 自动启动复选框
        const autoStart = document.getElementById('autoStart');
        if (autoStart) {
            autoStart.checked = this.config.autoStart;
        }
        
        // 运动检测复选框
        const motionDetection = document.getElementById('motionDetection');
        if (motionDetection) {
            motionDetection.checked = this.config.enableMotionDetection || false;
        }
        
        // 自动保存复选框
        const autoSave = document.getElementById('autoSave');
        if (autoSave) {
            autoSave.checked = this.config.autoSave !== false; // 默认为true
        }
    }
    
    /**
     * 隐藏设置模态框
     */
    hideSettingsModal() {
        console.log('[主应用] 隐藏设置模态框');
        
        const modal = document.getElementById('settingsModal');
        if (modal) {
            // 添加淡出动画
            modal.style.opacity = '0';
            modal.style.transition = 'opacity 0.3s ease';
            
            // 延迟隐藏,让动画完成
            setTimeout(() => {
                modal.classList.add('hidden');
                modal.style.opacity = ''; // 清除内联样式
                modal.style.transition = ''; // 清除内联样式
            }, 300);
        }
    }
    
    /**
     * 应用设置(保存设置)
     */
    applySettings() {
        console.log('[主应用] 用户应用设置');
        
        try {
            // -----------------------------
            // 从表单获取设置值
            // -----------------------------
            
            // 摄像头设备
            const cameraDeviceSelect = document.getElementById('cameraDeviceSelect');
            if (cameraDeviceSelect) {
                this.selectedDeviceId = cameraDeviceSelect.value === 'default' ? null : cameraDeviceSelect.value;
                console.log(`[主应用] 设置摄像头设备: ${this.selectedDeviceId || '默认'}`);
            }
            
            // 视频质量
            const videoQuality = document.getElementById('videoQuality');
            if (videoQuality) {
                switch (videoQuality.value) {
                    case 'high':
                        this.config.videoQuality = 0.95;
                        break;
                    case 'medium':
                        this.config.videoQuality = 0.85;
                        break;
                    case 'low':
                        this.config.videoQuality = 0.75;
                        break;
                    default:
                        this.config.videoQuality = 0.92;
                }
                console.log(`[主应用] 设置视频质量: ${videoQuality.value} (${this.config.videoQuality})`);
            }
            
            // 自动启动
            const autoStart = document.getElementById('autoStart');
            if (autoStart) {
                this.config.autoStart = autoStart.checked;
                console.log(`[主应用] 设置自动启动: ${this.config.autoStart}`);
            }
            
            // 运动检测
            const motionDetection = document.getElementById('motionDetection');
            if (motionDetection) {
                this.config.enableMotionDetection = motionDetection.checked;
                console.log(`[主应用] 设置运动检测: ${this.config.enableMotionDetection}`);
                
                // 如果启用了运动检测,可以在这里初始化相关功能
                if (this.config.enableMotionDetection) {
                    this.initializeMotionDetection();
                }
            }
            
            // 自动保存
            const autoSave = document.getElementById('autoSave');
            if (autoSave) {
                this.config.autoSave = autoSave.checked;
                console.log(`[主应用] 设置自动保存: ${this.config.autoSave}`);
            }
            
            // -----------------------------
            // 保存配置
            // -----------------------------
            this.saveConfiguration();
            
            // -----------------------------
            // 关闭模态框并显示通知
            // -----------------------------
            this.hideSettingsModal();
            this.showNotification('设置已保存', 'success');
            
        } catch (error) {
            console.error('[主应用] 应用设置失败:', error);
            this.showNotification('保存设置失败: ' + error.message, 'error');
        }
    }
    
    /**
     * 初始化运动检测功能
     * 注意:这是一个高级功能,需要额外的算法实现
     */
    initializeMotionDetection() {
        console.log('[主应用] 初始化运动检测功能');
        
        // 这里可以添加运动检测的初始化代码
        // 例如:设置画布、初始化图像处理算法等
        
        // 由于运动检测是高级功能,这里只记录日志
        console.warn('[主应用] 运动检测功能需要额外实现,当前为占位实现');
        
        // 可以在这里添加后续的运动检测逻辑
        // this.startMotionDetection();
    }
    
    /**
     * 显示通知消息
     * @param {string} message - 通知消息内容
     * @param {string} type - 通知类型: 'success', 'error', 'warning', 'info'
     */
    showNotification(message, type = 'info') {
        // 如果配置中禁用了通知,则直接返回
        if (!this.config.enableNotifications) {
            return;
        }
        
        console.log(`[主应用] 显示通知: [${type}] ${message}`);
        
        // 获取通知容器
        const container = document.getElementById('notificationContainer');
        if (!container) {
            console.warn('[主应用] 未找到通知容器,跳过显示通知');
            return;
        }
        
        // 确定图标和颜色
        let icon, bgColor, textColor;
        switch (type) {
            case 'success':
                icon = 'fa-check-circle';
                bgColor = 'bg-green-500';
                textColor = 'text-white';
                break;
            case 'error':
                icon = 'fa-exclamation-circle';
                bgColor = 'bg-red-500';
                textColor = 'text-white';
                break;
            case 'warning':
                icon = 'fa-exclamation-triangle';
                bgColor = 'bg-yellow-500';
                textColor = 'text-yellow-900';
                break;
            default: // info
                icon = 'fa-info-circle';
                bgColor = 'bg-blue-500';
                textColor = 'text-white';
        }
        
        // 创建通知元素
        const notification = document.createElement('div');
        notification.className = `notification ${bgColor} ${textColor} px-4 py-3 rounded-lg shadow-lg mb-2 transform translate-x-full opacity-0 transition-all duration-300`;
        notification.innerHTML = `
            <div class="flex items-center">
                <i class="fas ${icon} mr-2"></i>
                <span>${message}</span>
            </div>
        `;
        
        // 添加到容器
        container.appendChild(notification);
        
        // 触发入场动画
        requestAnimationFrame(() => {
            notification.classList.remove('translate-x-full', 'opacity-0');
            notification.classList.add('translate-x-0', 'opacity-100');
        });
        
        // 设置自动移除
        const duration = type === 'error' ? 5000 : 3000; // 错误消息显示5秒,其他3秒
        setTimeout(() => {
            // 触发退场动画
            notification.classList.remove('translate-x-0', 'opacity-100');
            notification.classList.add('translate-x-full', 'opacity-0');
            
            // 动画完成后移除元素
            setTimeout(() => {
                if (notification.parentNode) {
                    notification.parentNode.removeChild(notification);
                }
            }, 300);
        }, duration);
    }
    
    /**
     * 获取应用状态信息
     * @returns {Object} 应用状态对象
     */
    getAppStatus() {
        return {
            appState: this.appState,
            uptime: this.uptimeSeconds,
            screenshotCount: this.screenshotManager.getCount(),
            cameraStatus: this.cameraController.getStatus(),
            performance: this.performance.getReport(),
            config: { ...this.config } // 返回副本,避免外部修改
        };
    }
    
    /**
     * 销毁应用,释放资源
     * 在页面卸载或应用关闭时调用
     */
    destroy() {
        console.log('[主应用] 销毁应用,释放资源...');
        
        // 停止摄像头
        if (this.appState === 'streaming' || this.appState === 'recording') {
            this.stopCamera();
        }
        
        // 停止运行时间计数器
        if (this.uptimeInterval) {
            clearInterval(this.uptimeInterval);
            this.uptimeInterval = null;
        }
        
        // 停止性能监控
        this.performance.stopMonitoring();
        
        // 保存配置
        this.saveConfiguration();
        
        // 清理截图管理器的临时数据
        // 注意:这里不清除本地存储的数据
        
        console.log('[主应用] 应用已销毁');
    }
    
    /**
     * 调试方法:打印详细状态信息
     * 用于开发调试
     */
    debug() {
        console.group('[主应用] 调试信息');
        
        // 应用状态
        console.log('应用状态:', this.appState);
        console.log('运行时间:', this.uptimeSeconds, '秒');
        
        // 摄像头状态
        const cameraStatus = this.cameraController.getStatus();
        console.log('摄像头状态:', cameraStatus);
        
        // 截图信息
        console.log('截图数量:', this.screenshotManager.getCount());
        
        // 配置信息
        console.log('应用配置:', this.config);
        
        // 性能信息
        const perfReport = this.performance.getReport();
        console.log('性能报告:', perfReport);
        
        // 视频元素状态
        if (this.videoElement) {
            console.log('视频元素:', {
                readyState: this.videoElement.readyState,
                videoWidth: this.videoElement.videoWidth,
                videoHeight: this.videoElement.videoHeight,
                srcObject: this.videoElement.srcObject ? '已设置' : '未设置'
            });
        }
        
        console.groupEnd();
    }
}
​
// ============================================
// 全局辅助函数
// ============================================
​
/**
 * 检查浏览器兼容性
 * @returns {Object} 兼容性检查结果
 */
function checkBrowserCompatibility() {
    console.log('[全局] 检查浏览器兼容性...');
    
    const compatibility = {
        mediaDevices: !!navigator.mediaDevices,
        getUserMedia: !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
        mediaRecorder: !!window.MediaRecorder,
        localstorage: !!window.localStorage,
        canvas: !!window.HTMLCanvasElement,
        webrtc: !!(window.RTCPeerConnection || window.webkitRTCPeerConnection)
    };
    
    // 检查所有必需功能
    const required = ['mediaDevices', 'getUserMedia', 'localstorage', 'canvas'];
    const missing = required.filter(feature => !compatibility[feature]);
    
    if (missing.length > 0) {
        console.error('[全局] 浏览器不兼容,缺少功能:', missing);
        return {
            supported: false,
            missing: missing,
            details: compatibility
        };
    }
    
    console.log('[全局] 浏览器兼容性检查通过:', compatibility);
    return {
        supported: true,
        details: compatibility
    };
}
​
/**
 * 初始化应用
 * 页面加载完成后调用
 */
async function initializeApplication() {
    console.log('[全局] 开始初始化摄像头监控应用...');
    
    // 检查浏览器兼容性
    const compatibility = checkBrowserCompatibility();
    if (!compatibility.supported) {
        const errorMsg = `浏览器不兼容,缺少功能: ${compatibility.missing.join(', ')}。请使用现代浏览器如 Chrome、Firefox 或 Edge。`;
        console.error('[全局] ' + errorMsg);
        alert(errorMsg);
        return;
    }
    
    try {
        // 创建应用实例
        window.app = new CameraMonitorApp();
        
        // 初始化应用
        await window.app.initialize();
        
        // 将调试方法暴露给全局
        window.debugApp = () => window.app.debug();
        
        console.log('[全局] 应用初始化完成,实例已保存到 window.app');
        
        // 显示欢迎消息
        setTimeout(() => {
            if (window.app && window.app.showNotification) {
                window.app.showNotification('摄像头监控系统已就绪', 'success');
            }
        }, 1000);
        
    } catch (error) {
        console.error('[全局] 应用初始化失败:', error);
        
        // 显示错误信息
        const errorElement = document.createElement('div');
        errorElement.className = 'fixed inset-0 flex items-center justify-center bg-red-50 z-50';
        errorElement.innerHTML = `
            <div class="bg-white p-6 rounded-lg shadow-xl max-w-md">
                <h2 class="text-xl font-bold text-red-600 mb-4">应用初始化失败</h2>
                <p class="text-gray-700 mb-4">${error.message}</p>
                <p class="text-sm text-gray-500 mb-4">请检查浏览器控制台获取详细信息。</p>
                <button οnclick="location.reload()" class="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600">
                    重新加载页面
                </button>
            </div>
        `;
        
        document.body.appendChild(errorElement);
    }
}
​
/**
 * 页面卸载前的清理工作
 */
function handlePageUnload() {
    console.log('[全局] 页面正在卸载,执行清理...');
    
    if (window.app && typeof window.app.destroy === 'function') {
        window.app.destroy();
    }
}
​
// ============================================
// 页面事件监听器
// ============================================
​
// DOM内容加载完成后初始化应用
document.addEventListener('DOMContentLoaded', () => {
    console.log('[全局] DOMContentLoaded 事件触发');
    
    // 延迟初始化,确保所有DOM元素都已加载
    setTimeout(initializeApplication, 100);
});
​
// 页面卸载前的清理
window.addEventListener('beforeunload', handlePageUnload);
​
// 页面可见性变化处理(标签页切换)
document.addEventListener('visibilitychange', () => {
    console.log(`[全局] 页面可见性变化: ${document.visibilityState}`);
    
    if (window.app) {
        // 如果页面隐藏且摄像头正在运行,暂停视频以节省资源
        if (document.hidden && window.app.appState === 'streaming') {
            window.app.videoElement.pause();
        }
        // 如果页面恢复显示且摄像头正在运行,恢复视频播放
        else if (!document.hidden && window.app.appState === 'streaming') {
            window.app.videoElement.play().catch(e => {
                console.warn('[全局] 恢复视频播放失败:', e);
            });
        }
    }
});
​
// ============================================
// 全局错误处理
// ============================================
​
// 全局未捕获的Promise错误处理
window.addEventListener('unhandledrejection', (event) => {
    console.error('[全局] 未处理的Promise错误:', event.reason);
    
    // 显示错误通知
    if (window.app && window.app.showNotification) {
        window.app.showNotification(`发生错误: ${event.reason.message}`, 'error');
    }
});
​
// 全局JavaScript错误处理
window.addEventListener('error', (event) => {
    console.error('[全局] JavaScript错误:', event.error);
    
    // 如果不是来自应用本身的错误,显示通知
    if (window.app && window.app.showNotification && 
        !event.message.includes('CameraMonitorApp')) {
        window.app.showNotification('发生JavaScript错误,请查看控制台', 'error');
    }
    
    // 阻止错误冒泡到浏览器默认处理
    event.preventDefault();
});
​
// ============================================
// 全局实用函数
// ============================================
​
/**
 * 下载数据为文件
 * @param {Blob|string} data - 要下载的数据
 * @param {string} filename - 文件名
 */
window.downloadFile = function(data, filename) {
    try {
        // 创建Blob(如果是字符串)
        const blob = typeof data === 'string' ? 
            new Blob([data], { type: 'application/octet-stream' }) : data;
        
        // 创建下载URL
        const url = URL.createObjectURL(blob);
        
        // 创建下载链接
        const link = document.createElement('a');
        link.href = url;
        link.download = filename;
        
        // 触发下载
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
        
        // 清理URL
        setTimeout(() => URL.revokeObjectURL(url), 1000);
        
        console.log(`[全局] 文件下载完成: ${filename}`);
        
    } catch (error) {
        console.error('[全局] 下载文件失败:', error);
        throw error;
    }
};
​
/**
 * 格式化文件大小
 * @param {number} bytes - 字节数
 * @returns {string} 格式化后的文件大小
 */
window.formatFileSize = function(bytes) {
    if (bytes === 0) return '0 B';
    
    const units = ['B', 'KB', 'MB', 'GB', 'TB'];
    const i = Math.floor(Math.log(bytes) / Math.log(1024));
    
    return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
};
​
/**
 * 获取查询参数
 * @param {string} name - 参数名
 * @returns {string|null} 参数值
 */
window.getQueryParam = function(name) {
    const urlParams = new URLSearchParams(window.location.search);
    return urlParams.get(name);
};
​
// ============================================
// 导出模块(如果使用模块系统)
// ============================================
// 注意:这是一个直接执行的脚本文件,不是ES6模块
// 如果需要在模块系统中使用,可以取消下面的注释
​
/*
if (typeof module !== 'undefined' && module.exports) {
    // Node.js环境
    module.exports = {
        CameraMonitorApp,
        CameraController,
        ScreenshotManager,
        PerformanceMonitor,
        checkBrowserCompatibility
    };
} else if (typeof define === 'function' && define.amd) {
    // AMD环境
    define([], function() {
        return {
            CameraMonitorApp,
            CameraController,
            ScreenshotManager,
            PerformanceMonitor,
            checkBrowserCompatibility
        };
    });
}
*/
​
// ============================================
// 应用初始化完成标志
// ============================================
console.log('[全局] app.js 加载完成,等待DOMContentLoaded事件...');
​
// 立即执行的代码
(function() {
    // 检查当前环境
    const isLocalhost = window.location.hostname === 'localhost' || 
                        window.location.hostname === '127.0.0.1';
    
    const isHttps = window.location.protocol === 'https:';
    
    // 如果不是localhost且不是HTTPS,显示警告
    if (!isLocalhost && !isHttps) {
        console.warn('[全局] 警告: 摄像头功能通常需要在HTTPS或localhost环境下工作');
        
        // 可以在这里添加一个非侵入式的警告
        setTimeout(() => {
            const warning = document.createElement('div');
            warning.className = 'fixed bottom-4 left-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4 rounded shadow-lg max-w-sm z-40';
            warning.innerHTML = `
                <div class="flex">
                    <div class="flex-shrink-0">
                        <i class="fas fa-exclamation-triangle"></i>
                    </div>
                    <div class="ml-3">
                        <p class="text-sm">
                            摄像头功能在非HTTPS环境下可能受限。建议在localhost或HTTPS环境下使用。
                        </p>
                    </div>
                </div>
            `;
            document.body.appendChild(warning);
            
            // 10秒后自动移除警告
            setTimeout(() => {
                if (warning.parentNode) {
                    warning.parentNode.removeChild(warning);
                }
            }, 10000);
        }, 2000);
    }
})();
​
styles.css
/* 自定义样式 */
.video-container {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
​
.btn-primary {
    background: linear-gradient(45deg, #4facfe 0%, #00f2fe 100%);
    transition: all 0.3s ease;
}
​
.btn-primary:hover {
    transform: translateY(-2px);
    box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
​
.btn-secondary {
    background: #6b7280;
    color: white;
    padding: 0.5rem 1rem;
    border-radius: 0.5rem;
    transition: all 0.3s ease;
}
​
.btn-secondary:hover {
    background: #4b5563;
}
​
.screenshot-item {
    transition: all 0.3s ease;
}
​
.screenshot-item:hover {
    transform: scale(1.02);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
​
@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.7; }
}
​
.recording {
    animation: pulse 1.5s infinite;
}
​
/* 模态框样式 */
.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 50;
    backdrop-filter: blur(4px);
}
​
.modal-overlay.hidden {
    display: none;
}
​
.modal-content {
    background: white;
    border-radius: 1rem;
    padding: 1.5rem;
    max-width: 32rem;
    width: 100%;
    margin: 1rem;
    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
}
​
.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 1.5rem;
}
​
.modal-title {
    font-size: 1.25rem;
    font-weight: bold;
    color: #1f2937;
}
​
.modal-close-btn {
    color: #6b7280;
    padding: 0.5rem;
    border-radius: 0.5rem;
    transition: all 0.2s ease;
}
​
.modal-close-btn:hover {
    color: #374151;
    background: #f3f4f6;
}
​
.modal-body {
    margin-bottom: 1.5rem;
}
​
.modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: 0.75rem;
    padding-top: 1.5rem;
    border-top: 1px solid #e5e7eb;
}
​
/* 表单样式 */
.form-label {
    display: block;
    font-size: 0.875rem;
    font-weight: 500;
    color: #374151;
    margin-bottom: 0.5rem;
}
​
.form-select {
    width: 100%;
    padding: 0.5rem 0.75rem;
    border: 1px solid #d1d5db;
    border-radius: 0.5rem;
    background: white;
    color: #374151;
    transition: all 0.2s ease;
}
​
.form-select:focus {
    outline: none;
    border-color: #3b82f6;
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
​
.form-checkbox-group {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}
​
.form-checkbox {
    width: 1rem;
    height: 1rem;
    border-radius: 0.25rem;
    border: 1px solid #d1d5db;
    background: white;
    cursor: pointer;
}
​
.form-checkbox:checked {
    background-color: #3b82f6;
    border-color: #3b82f6;
}
​
.form-checkbox-label {
    font-size: 0.875rem;
    color: #374151;
    cursor: pointer;
}
​
/* 滚动条样式 */
::-webkit-scrollbar {
    width: 8px;
}
​
::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 4px;
}
​
::-webkit-scrollbar-thumb {
    background: #c1c1c1;
    border-radius: 4px;
}
​
::-webkit-scrollbar-thumb:hover {
    background: #a8a8a8;
}
​
/* 响应式调整 */
@media (max-width: 768px) {
    .modal-content {
        margin: 0.5rem;
        padding: 1rem;
    }
    
    .modal-footer {
        flex-direction: column;
    }
    
    .modal-footer button {
        width: 100%;
    }
}
​
/* 暗色模式支持 */
@media (prefers-color-scheme: dark) {
    body {
        background-color: #111827;
        color: #f3f4f6;
    }
    
    .modal-content {
        background: #1f2937;
        color: #f3f4f6;
    }
    
    .modal-title {
        color: #f3f4f6;
    }
    
    .form-select {
        background: #374151;
        border-color: #4b5563;
        color: #f3f4f6;
    }
    
    .form-checkbox {
        background: #374151;
        border-color: #4b5563;
    }
}
​
/* 视频元素硬件加速优化 */
#videoPreview {
    transform: translateZ(0);
    backface-visibility: hidden;
    perspective: 1000;
    will-change: transform;
}
​
/* 性能统计面板 */
#performanceStats {
    position: fixed;
    bottom: 20px;
    right: 20px;
    background: rgba(0, 0, 0, 0.7);
    color: white;
    padding: 10px 15px;
    border-radius: 8px;
    font-family: monospace;
    font-size: 12px;
    z-index: 1000;
}
​
.stats-item {
    display: flex;
    justify-content: space-between;
    margin: 5px 0;
}
​
.stats-label {
    color: #aaa;
}
​
.stats-value {
    color: #4CAF50;
    font-weight: bold;
}
​
.stats-value.low-frame-rate {
    color: #ff5252;
}
​
.stats-value.good-frame-rate {
    color: #4CAF50;
}
​
/* 通知样式优化 */
.notification {
    transition: all 0.3s ease;
    animation: slideIn 0.3s ease-out;
}
​
@keyframes slideIn {
    from { transform: translateX(100%); opacity: 0; }
    to { transform: translateX(0); opacity: 1; }
}
​
/* 控制按钮状态 */
.control-panel button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}
​
.control-panel button.loading {
    position: relative;
    color: transparent !important;
}
​
.control-panel button.loading::after {
    content: '';
    position: absolute;
    left: 50%;
    top: 50%;
    width: 16px;
    height: 16px;
    margin: -8px 0 0 -8px;
    border: 2px solid rgba(255, 255, 255, 0.3);
    border-top-color: white;
    border-radius: 50%;
    animation: buttonSpin 0.6s linear infinite;
}
​
@keyframes buttonSpin {
    to { transform: rotate(360deg); }
}
​

代码通过四个核心类实现了清晰的功能划分,这正是高内聚、低耦合思想的体现:

  • CameraMonitorApp (主控制器):作为协调中枢,它不直接操作硬件或数据,而是初始化并管理其他三个专业模块。它处理UI事件、协调startCameracaptureScreenshot等工作流,并维护应用整体状态(appState)。这确保了业务逻辑的集中和可维护性。

  • CameraController (硬件交互层):这是与浏览器媒体API交互的唯一通道,实现了与虚拟“UVC驱动层”(浏览器封装)的对接。它封装了getUserMediaMediaRecorder等所有底层操作,将原始视频流转化为MediaStream对象。这种设计将复杂的硬件交互细节隔离,使上层应用无需关心具体实现。

  • ScreenshotManager (数据管理层):独立负责截图数据的存取、序列化和持久化。它处理dataUrllocalStorage和ZIP打包,将数据操作逻辑从UI和硬件控制中彻底解耦。

  • PerformanceMonitor (辅助工具层):专注于性能度量,通过高精度时间API监控帧率和操作耗时,为其他模块提供诊断支持而不干扰其核心功能。

2. 视频数据流与大小分析

数据流是理解此应用的关键。视频数据从摄像头到最终文件,经历了多次形态和位置转换:

  1. 原始采集与封装 (CameraController.start()):

    • 来源: 物理USB摄像头 (通过OS,如 /dev/video0)。

    • 转换: 浏览器通过 navigator.mediaDevices.getUserMedia() 获取原始视频帧。

    • 形态/大小: 被封装为 MediaStream 对象。数据仍在浏览器内核管理的缓冲区中,大小取决于分辨率、帧率和色彩深度(例如,1280x720 @30fps, YUV420格式,理论原始数据流约为 1280*720*1.5*30 ≈ 39.6 MB/s)。

  2. 渲染与显示 (Video 元素):

    • 流向: MediaStream 被赋值给 HTMLVideoElement.srcObject

    • 形态: 由浏览器引擎进行解码(如果是MJPEG/H264)并渲染到页面。此过程数据在GPU内存中。

  3. 帧捕获与编码 (CameraController.captureFrame()):

    • 关键函数:

      ctx.drawImage(this.videoElement, 0, 0); // 从视频元素拷贝帧到Canvas
      const dataUrl = canvas.toDataURL('image/jpeg', quality); // Canvas编码为JPEG
    • 形态/大小转换: GPU帧 -> Canvas位图 -> JPEG编码的Base64字符串。一张1280x720的高质量JPEG截图,dataUrl 字符串长度约为 (文件字节数 * 4/3),一个100KB的图片会产生约133KB的字符串。

  4. 持久化存储 (ScreenshotManager):

    • 存储: dataUrl 连同元数据(ID、时间戳、分辨率)以JSON格式存入 localStorage

    • 大小限制: 受 localStorage 单域名存储限制(通常5-10MB)。代码通过 maxCount 和只保存最近50张的策略进行自我管理。

    • 导出: 使用 JSZip 库,将 dataUrl 解码回二进制,打包成ZIP文件供下载,恢复为紧凑的二进制格式。

3. 与底层技术栈的关联映射

尽管这是一个Web应用,但其功能依赖于浏览器对底层操作系统能力的封装:

  • 对“Web UVC驱动”的调用: 当 getUserMedia({video: true}) 被调用时,浏览器(以Chrome为例)会:

    1. 通过 Linux内核 -> V4L2层 枚举 /dev/video* 设备。

    2. 通过UVC驱动与摄像头协商格式、分辨率、帧率(对应 constraints 对象)。

    3. 建立内存映射,开始将视频帧数据从内核空间读取到用户空间的浏览器进程。

  • 对“内核V4L2框架”的间接使用: 应用中的 分辨率选择帧率设置 最终会转化为对 MediaStream 的约束,并由浏览器翻译成对V4L2的 VIDIOC_S_PARMVIDIOC_S_FMT 等ioctl调用。内核中相关源码主要位于 drivers/media/usb/uvc/ 目录。

  • 数据流路径对应:

    • /dev/video0 -> V4L2缓冲区: 对应 CameraControllergetUserMedia() 成功后的原始数据获取。

    • V4L2缓冲区 -> 用户空间: 对应浏览器内部将数据填充到 MediaStream 轨道。

    • 用户空间 -> 渲染管线: 对应 HTMLVideoElement 的播放和 CanvasdrawImage 操作。

如何用于复杂应用场景

基于当前高内聚、模块化的设计,此应用可以轻松扩展至以下复杂场景:

应用场景 所需扩展模块/修改 设计依据与优势
多摄像头监控看板 创建多个CameraController实例,CameraMonitorApp统一调度,UI分屏显示。 控制器与UI解耦,便于实例化管理。
AI图像分析(如人脸识别) 新增AIAnalyzer类,从Canvas或直接处理Video帧,结果通过事件通知主应用。 数据流清晰(视频帧易于获取),不影响现有截图/录制功能
云端录像与存储 新增CloudStorage类,将MediaRecorder的数据块或ScreenshotManager的图片直接上传。 ScreenshotManager的存储后端可插拔,MediaRecorder数据块易于拦截。
实时视频通讯(P2P) 利用现有MediaStream,集成WebRTC的RTCPeerConnection 硬件访问已封装,流可复用,只需增加信令模块。
工业质检(自动触发) 扩展PerformanceMonitorEventMonitor,分析帧内容,自动调用captureScreenshot 事件驱动架构,功能模块可通过标准接口被触发。

第二部分:浏览器中间件与第三方库层分析

1. 架构位置与作用

此层位于用户态,是Web前端与内核UVC驱动之间的翻译器和协议处理器。其核心职责包括:

  • 硬件抽象:将底层不同的摄像头硬件统一为Web标准API

  • 协议转换:在Web API调用与Linux V4L2系统调用之间进行转换

  • 数据桥接:在内核空间视频缓冲区与用户态JavaScript对象之间建立通道

2. 浏览器媒体子系统架构树形分析

3. 关键组件详细分析

3.1 WebRTC/MediaDevices API实现分解

CameraController.getUserMedia()被调用时,浏览器内部执行以下流程:

Chromium/Blink实现路径树形分析:

media/webrtc/ (Chromium源码目录)
├── media_stream_manager.cc
│   ├── MediaStreamManager::MakeMediaAccessRequest()
│   │   └── 1. 权限检查 (WebRTC权限API)
│   │   └── 2. 设备枚举 (从OS获取设备列表)
│   │   └── 3. 约束处理 (处理width/height/frameRate参数)
│   │
├── video_capture/ 
│   ├── video_capture_impl.cc
│   │   └── VideoCaptureImpl::StartCapture()
│   │       ├── 4. 创建V4L2接口
│   │       ├── 5. 协商视频格式
│   │       └── 6. 分配视频缓冲区
│   │
└── media_stream_video_capturer_source.cc
    └── MediaStreamVideoCapturerSource::StartCaptureImpl()
        └── 7. 启动视频捕获线程

约束处理的具体实现:

// 对应 constraints 对象的处理
void ProcessVideoConstraints(const MediaConstraints& constraints) {
    // 1. 解析理想值 (对应 {ideal: 1280})
    int ideal_width = constraints.basic().width.ideal();
    
    // 2. 查询设备能力
    std::vector<media::VideoCaptureFormat> formats;
    GetDeviceSupportedFormats(device_id, &formats);
    
    // 3. 匹配最佳格式 (参考formatPriority)
    media::VideoCaptureFormat best_format;
    for (const auto& format : formats) {
        // 优先匹配MJPEG/YUYV等UVC常用格式
        if (format.pixel_format == media::PIXEL_FORMAT_MJPEG) {
            best_format = format;
            break;
        }
    }
    
    // 4. 设置V4L2参数
    v4l2_format v4l2_fmt = {};
    v4l2_fmt.fmt.pix.width = best_format.frame_size.width();
    v4l2_fmt.fmt.pix.height = best_format.frame_size.height();
    v4l2_fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG; // UVC常用
    ioctl(fd, VIDIOC_S_FMT, &v4l2_fmt);
}
3.2 第三方库依赖分析

应用直接依赖的库:

第三方库依赖树:
├── Tailwind CSS (UI框架)
│   └── 仅影响表现层,无数据流影响
│
├── JSZip (ZIP打包库)
│   └── 数据流:Base64字符串 → 二进制Blob → ZIP文件
│       ├── 函数调用链:
│       │   ScreenshotManager.exportToZip()
│       │   ├── new JSZip()
│       │   ├── zip.file(filename, base64Data, {base64: true})
│       │   └── zip.generateAsync({type: 'blob'})
│       └── 内存转换:
│           Base64字符串 (约133KB/图片)
│           → Uint8Array二进制 (约100KB/图片)
│           → ZIP压缩数据 (约70-90KB/图片)
│
└── Font Awesome (图标)
    └── 纯UI资源

浏览器内部间接依赖的第三方媒体库:

在Linux桌面环境,浏览器通常依赖以下多媒体框架:

浏览器媒体后端树:
├── Chromium (Linux)
│   ├── 主要:V4L2直接接口
│   ├── 备选:libv4l2 (Video4Linux2兼容库)
│   └── 编码:libvpx (VP8/VP9), libx264 (H.264)
│
├── Firefox (Linux)
│   ├── GStreamer媒体框架 ←─┐
│   │   ├── gstreamer1.0-plugins-good │
│   │   │   ├── v4l2src (V4L2源)      │
│   │   │   ├── jpegdec (MJPEG解码)   │数据流关联
│   │   │   └── videoconvert (格式转换)│
│   │   └── gstreamer1.0-plugins-ugly │
│   │       └── x264enc (H.264编码)    │
│   └── PulseAudio (音频)             │
│                                     │
└── 数据流对应关系:                   │
    MediaRecorder API调用          ↓
    → GStreamer pipeline构建          ↓
    → V4L2设备访问                    ↓
    → 内核UVC驱动                   ←─┘

4. 数据缓冲区与内存管理分析

4.1 视频数据缓冲区流转

从内核到JavaScript的完整数据路径:

缓冲区流转层级树:
├── 层级1:内核空间缓冲区 (drivers/media/usb/uvc/uvc_video.c)
│   ├── uvc_video.c::uvc_alloc_urb_buffers()
│   │   └── 分配USB URB (USB Request Block)
│   │       ├── 大小:根据UVC帧格式计算
│   │       │   ├── MJPEG: 可变大小 (按质量)
│   │       │   ├── YUYV: 固定大小 = width × height × 2
│   │       │   └── H.264: 可变大小 (NAL单元)
│   │       └── 数量:通常3-5个 (双/三缓冲)
│   │
├── 层级2:V4L2缓冲区 (drivers/media/v4l2-core/videobuf2-core.c)
│   ├── vb2_queue_init()
│   │   ├── 内存模式:
│   │   │   ├── MMAP (内存映射):内核分配,用户空间映射
│   │   │   ├── USERPTR (用户指针):用户空间分配
│   │   │   └── DMABUF (DMA缓冲区):硬件加速
│   │   └── 缓冲区数量:4-10个 (避免丢帧)
│   │
├── 层级3:浏览器媒体缓冲区
│   ├── media/base/video_frame.cc (Chromium)
│   │   ├── VideoFrame::WrapExternalData()
│   │   │   └── 包装V4L2缓冲区数据
│   │   ├── 格式转换:
│   │   │   ├── MJPEG → I420 (软件解码)
│   │   │   ├── YUYV → I420 (色彩空间转换)
│   │   │   └── 可启用硬件加速
│   │   └── 内存管理:
│   │       ├── SharedMemory (进程间共享)
│   │       └── GpuMemoryBuffer (GPU内存)
│   │
└── 层级4:JavaScript对象
    ├── ImageData (Canvas API)
    │   └── 从VideoFrame提取RGBA数据
    ├── ArrayBuffer (二进制数据)
    │   └── 用于MediaRecorder数据块
    └── dataURL (Base64编码)
        └── 字符串存储,内存开销约+33%
4.2 缓冲区大小计算示例

以应用默认设置(1280x720 @30fps)为例:

原始数据大小:

  • YUYV格式1280 × 720 × 2字节 × 30fps = 52.9 MB/s

  • MJPEG格式:压缩比约10:1 → ~5.3 MB/s

  • H.264格式:压缩比约50:1 → ~1.1 MB/s

浏览器内存占用:

  • 解码后I420帧1280 × 720 × 1.5字节 = 1.35 MB/帧

  • 缓冲队列(4帧)1.35 MB × 4 = 5.4 MB

  • Canvas RGBA缓冲1280 × 720 × 4字节 = 3.53 MB

  • 总计峰值~9-10 MB RAM

5. 性能优化机制分析

5.1 零拷贝数据路径(Zero-Copy)

现代浏览器针对视频处理优化的数据路径:

// 理想的数据流(Chromium中的实现示例)
class ZeroCopyVideoFramePool {
public:
    scoped_refptr<VideoFrame> GetFrame() {
        // 1. 从V4L2获取DMABUF句柄
        int dmabuf_fd = v4l2_buffer.memory == V4L2_MEMORY_DMABUF ? 
                        v4l2_buffer.m.fd : -1;
        
        // 2. 导入到GPU内存(如果支持)
        if (dmabuf_fd >= 0 && HasGpuAcceleration()) {
            gpu::GpuMemoryBufferHandle gmb_handle;
            gmb_handle.type = gpu::GpuMemoryBufferType::NATIVE_PIXMAP;
            gmb_handle.native_pixmap_handle.planes.push_back(
                gfx::NativePixmapPlane(stride, offset, dmabuf_fd));
            
            // 3. 创建GPU纹理,避免CPU拷贝
            return VideoFrame::WrapExternalGpuMemoryBuffer(...);
        }
        
        // 4. 回退到共享内存路径
        return VideoFrame::WrapExternalSharedMemory(...);
    }
};
5.2 硬件加速支持矩阵
功能 Linux支持状态 使用的API/驱动 性能影响
MJPEG硬件解码 中等(依赖Intel VA-API/NVIDIA VDPAU) libva, VDPAU CPU占用降低30-50%
H.264硬件编码 良好(Intel QuickSync, NVIDIA NVENC) VA-API, NVENC 编码速度提升5-10倍
3D加速渲染 优秀(所有现代GPU) OpenGL/WebGL Canvas绘制提速
内存零拷贝 中等(需要DMABUF支持) DRM/DMA-BUF 内存带宽降低

6. 错误处理与兼容性机制

6.1 格式回退策略

当首选格式不支持时的浏览器内部处理:

// 对应CameraController.formatPriority逻辑
void NegotiateVideoFormat(int fd, const FormatPreference& prefs) {
    // 1. 查询设备支持的所有格式
    v4l2_fmtdesc fmt_desc = {};
    fmt_desc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    
    std::vector<uint32_t> supported_formats;
    while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt_desc) == 0) {
        supported_formats.push_back(fmt_desc.pixelformat);
        fmt_desc.index++;
    }
    
    // 2. 按优先级尝试格式(对应formatPriority)
    const uint32_t format_priority[] = {
        V4L2_PIX_FMT_H264,    // 首选H.264
        V4L2_PIX_FMT_MJPEG,   // 次选MJPEG
        V4L2_PIX_FMT_YUYV,    // 后备YUYV
        V4L2_PIX_FMT_RGB24    // 最后RGB
    };
    
    // 3. 协商最佳匹配
    for (auto preferred_fmt : format_priority) {
        if (std::find(supported_formats.begin(), 
                      supported_formats.end(), 
                      preferred_fmt) != supported_formats.end()) {
            SetFormat(fd, preferred_fmt);
            return;
        }
    }
    
    // 4. 无支持格式,抛出OverconstrainedError
    throw OverconstrainedError("No supported video format found");
}

7. 安全与权限模型

7.1 浏览器安全层架构
浏览器摄像头安全层:
├── 权限提示层 (Permission UI)
│   ├── 显示设备选择器
│   └── 请求用户明确授权
│
├── 来源检查层 (Origin Security)
│   ├── HTTPS要求 (非localhost)
│   ├── 同源策略检查
│   └── 权限持久化存储
│
├── 进程隔离层 (Sandboxing)
│   ├── 媒体进程隔离 (Chromium的--no-sandbox异常)
│   ├── GPU进程隔离
│   └── Seccomp过滤器限制系统调用
│
└── 硬件访问控制
    ├── udev规则检查 (如/dev/video*权限)
    ├── SELinux/AppArmor策略
    └── 用户组权限 (video组)

8. 实际部署的依赖关系

在Linux系统部署Web UVC应用时,需要的系统级依赖:

# Ubuntu/Debian 系统依赖
sudo apt-get install -y \
    # 浏览器媒体后端
    gstreamer1.0-plugins-good \
    gstreamer1.0-plugins-ugly \
    gstreamer1.0-libav \
    libgstreamer-plugins-base1.0-dev \
    
    # V4L2工具和库
    v4l-utils \
    libv4l-dev \
    
    # 硬件加速支持
    libva-dev \
    vainfo \
    
    # 视频编码库
    libx264-dev \
    libvpx-dev \
    
    # 用户组权限
    usermod -a -G video $USER

9. 性能监控与调试接口

PerformanceMonitor类可以扩展以下浏览器内部指标:

// 扩展性能监控以接入浏览器内部指标
class AdvancedPerformanceMonitor extends PerformanceMonitor {
    async getMediaMetrics() {
        if (!performance.mediaMetrics) return null;
        
        // 获取浏览器内部媒体统计(实验性API)
        const metrics = await performance.mediaMetrics.getMetrics();
        return {
            // 视频解码性能
            decodeFps: metrics.video_decode_fps,
            decodeLatency: metrics.video_decode_latency,
            
            // 编码性能(录制时)
            encodeFps: metrics.video_encode_fps,
            encodeBitrate: metrics.video_encode_bitrate,
            
            // 缓冲区健康度
            bufferLevel: metrics.buffer_level,
            bufferStarvation: metrics.buffer_starvation_count,
            
            // 帧丢弃统计(卡顿指标)
            droppedFrames: metrics.dropped_frames,
            corruptedFrames: metrics.corrupted_frames
        };
    }
    
    // 获取V4L2设备信息(通过扩展或本地服务)
    async getV4L2Info() {
        // 可通过WebSocket连接到本地后台服务获取
        const response = await fetch('http://localhost:3000/v4l2-info');
        return response.json();
    }
}

总结:中间件层的关键作用

这一层作为桥梁,实现了以下关键转换:

  1. API转换:Web标准API → Linux V4L2系统调用

  2. 数据格式转换:UVC设备原始数据 → 浏览器标准格式 → Canvas/WebGL可用格式

  3. 内存模型转换:内核DMA缓冲区 → 用户空间内存 → 进程间共享内存 → JavaScript可访问对象

  4. 同步机制转换:硬件中断/轮询 → 事件循环/回调 → Promise/async-await

第三部分:Linux内核UVC驱动与V4L2框架分析

1. 整体内核架构与数据流总览

2. UVC驱动详细源码分析

2.1 设备初始化和探测过程

完整探测调用链树形分析:

drivers/media/usb/uvc/ (内核UVC驱动源码)
├── uvc_driver.c
│   ├── uvc_probe()  ←─ USB设备插入时调用
│   │   ├── 1. 分配UVC设备结构体
│   │   │   ├── struct uvc_device *dev = kzalloc()
│   │   │   ├── 大小: sizeof(struct uvc_device) ≈ 2KB
│   │   │   └── 包含所有设备状态和控制信息
│   │   │
│   │   ├── 2. 解析UVC描述符
│   │   │   ├── uvc_parse_control() ←─ 关键函数
│   │   │   │   ├── 读取USB设备描述符
│   │   │   │   │   ├── 接口关联描述符(IAD)
│   │   │   │   │   ├── 视频控制接口(VC)
│   │   │   │   │   ├── 视频流接口(VS)
│   │   │   │   │   └── 单元/终端描述符
│   │   │   │   │
│   │   │   │   ├── 构建内部表示结构
│   │   │   │   │   ├── struct uvc_entity *entity
│   │   │   │   │   │   ├── 类型: UVC_ITT_CAMERA, UVC_OTT_VENDOR等
│   │   │   │   │   │   └── 大小: 每个实体约200-500字节
│   │   │   │   │   │
│   │   │   │   │   ├── struct uvc_control *ctrl
│   │   │   │   │   │   ├── 支持的控制: 亮度/对比度/饱和度等
│   │   │   │   │   │   └── 大小: 每个控制约100字节
│   │   │   │   │   │
│   │   │   │   │   └── struct uvc_format_desc *format
│   │   │   │   │       ├── 支持的格式: MJPEG, YUYV, H264等
│   │   │   │   │       └── 每个格式描述约50字节
│   │   │   │   │
│   │   │   │   └── 枚举所有支持的分辨率
│   │   │   │       ├── uvc_frame_uncompressed (未压缩格式)
│   │   │   │       ├── uvc_frame_mjpeg (MJPEG格式)
│   │   │   │       └── uvc_frame_frame_based (H264等)
│   │   │   │
│   │   │   └── 建立实体链接图
│   │   │       ├── 输入终端 → 处理单元 → 输出终端
│   │   │       └── 形成视频处理管线
│   │   │
│   │   ├── 3. 初始化视频流接口
│   │   │   ├── uvc_video_init() ←─ 视频流核心初始化
│   │   │   │   ├── 初始化URB(USB Request Block)列表
│   │   │   │   │   ├── 数量: UVC_URBS (默认5个)
│   │   │   │   │   ├── 每个大小: UVC_MAX_PACKETS * size
│   │   │   │   │   └── 总缓冲: 通常1-4MB
│   │   │   │   │
│   │   │   │   ├── 设置USB端点
│   │   │   │   │   ├── 查找批量(Bulk)或同步(Isochronous)端点
│   │   │   │   │   └── 配置端点参数
│   │   │   │   │
│   │   │   │   └── 启动URB提交循环
│   │   │   │       ├── usb_submit_urb() 异步提交
│   │   │   │       └── 建立数据流管道
│   │   │   │
│   │   ├── 4. 向V4L2注册设备
│   │   │   ├── uvc_register_video() ←─ V4L2接口注册
│   │   │   │   ├── video_register_device() ←─ 创建/dev/videoX
│   │   │   │   │   ├── 分配次设备号
│   │   │   │   │   ├── 创建设备节点
│   │   │   │   │   └── 设置文件操作结构
│   │   │   │   │
│   │   │   │   ├── 设置V4L2能力标志
│   │   │   │   │   ├── V4L2_CAP_VIDEO_CAPTURE
│   │   │   │   │   ├── V4L2_CAP_STREAMING
│   │   │   │   │   └── V4L2_CAP_DEVICE_CAPS
│   │   │   │   │
│   │   │   │   └── 填充v4l2_file_operations
│   │   │   │       ├── .open = uvc_v4l2_open
│   │   │   │       ├── .release = uvc_v4l2_release
│   │   │   │       ├── .ioctl = video_ioctl2
│   │   │   │       └── .poll = v4l2_poll
│   │   │   │
│   │   └── 5. 创建设备控制接口
│   │       ├── uvc_ctrl_init() ←─ 控制接口初始化
│   │       │   ├── 创建sysfs属性文件
│   │       │   │   ├── /sys/class/video4linux/videoX/
│   │       │   │   └── 暴露控制参数
│   │       │   │
│   │       │   ├── 初始化控制映射表
│   │       │   │   ├── V4L2_CID_BRIGHTNESS → UVC_PU_BRIGHTNESS_CONTROL
│   │       │   │   ├── V4L2_CID_CONTRAST → UVC_PU_CONTRAST_CONTROL
│   │       │   │   └── 建立控制传递通道
│   │       │   │
│   │       │   └── 设置控制回调函数
│   │       │       ├── .s_ctrl = uvc_ctrl_set_ctrl
│   │       │       └── .g_ctrl = uvc_ctrl_get_ctrl
│   │       │
└── uvc_video.c (视频数据处理核心)
    └── uvc_video_decode() ←─ 数据解码函数
2.2 视频数据流处理核心

uvc_video.c 关键函数详细分析:

/* drivers/media/usb/uvc/uvc_video.c - 核心数据流处理 */
​
/*
 * uvc_video_complete() - URB完成回调函数
 * @urb: 完成的USB请求块
 * 
 * 这是视频数据流的心脏,每个URB传输完成后被USB核心调用
 * 函数逐行注解:
 */
static void uvc_video_complete(struct urb *urb)
{
    struct uvc_video_queue *queue = urb->context;
    struct uvc_buffer *buf;
    unsigned long flags;
    
    /* 1. 获取当前流的状态 */
    spin_lock_irqsave(&queue->irqlock, flags);
    
    /* 2. 检查URB状态: 成功、错误或取消 */
    switch (urb->status) {
    case 0:     /* 成功传输 */
        /* 3. 从队列获取当前活动缓冲区 */
        buf = uvc_queue_get_current_buffer(queue);
        if (likely(buf != NULL)) {
            /* 4. 计算接收到的数据大小 */
            unsigned int len = urb->actual_length;
            
            /* 5. 处理视频数据包 */
            uvc_video_decode_data(uvc, buf, urb->transfer_buffer, len);
            
            /* 6. 检查帧是否完成 */
            if (uvc->completed_size >= uvc->frame_size) {
                /* 7. 帧完成,标记缓冲区就绪 */
                buf->state = UVC_BUF_STATE_READY;
                buf->bytesused = uvc->completed_size;
                
                /* 8. 唤醒等待数据的进程 */
                wake_up(&buf->wait);
                
                /* 9. 准备下一个缓冲区 */
                uvc_queue_next_buffer(queue, buf);
                uvc->completed_size = 0;
            }
        }
        break;
        
    case -ESHUTDOWN:    /* 设备断开 */
    case -ENODEV:
    case -EPROTO:       /* 协议错误 */
        /* 处理错误状态 */
        uvc_queue_cancel(queue, urb->status == -ESHUTDOWN);
        break;
    }
    
    /* 10. 重新提交URB以继续数据流 */
    if (queue->streaming && !queue->flags & UVC_QUEUE_DISCONNECTED) {
        urb->interval = uvc->streaming_ep->desc.bInterval;
        usb_submit_urb(urb, GFP_ATOMIC);
    }
    
    spin_unlock_irqrestore(&queue->irqlock, flags);
}
​
/*
 * uvc_video_decode_data() - 解码视频数据
 * @uvc: UVC设备实例
 * @buf: 目标V4L2缓冲区
 * @data: 原始USB数据
 * @len: 数据长度
 * 
 * 处理不同格式的视频数据解码
 */
static void uvc_video_decode_data(struct uvc_device *uvc,
                  struct uvc_buffer *buf, const u8 *data, int len)
{
    /* 根据格式选择解码器 */
    switch (uvc->streaming->format) {
    case UVC_FORMAT_MJPEG:
        /* MJPEG格式处理 */
        uvc_video_decode_mjpeg(uvc, buf, data, len);
        break;
        
    case UVC_FORMAT_UNCOMPRESSED:
        /* 未压缩格式(YUYV, RGB等) */
        uvc_video_decode_uncompressed(uvc, buf, data, len);
        break;
        
    case UVC_FORMAT_H264:
        /* H.264格式处理 */
        uvc_video_decode_h264(uvc, buf, data, len);
        break;
    }
}
​
/*
 * uvc_video_decode_mjpeg() - MJPEG解码函数
 * 这是最常见UVC摄像头的格式
 */
static void uvc_video_decode_mjpeg(struct uvc_device *uvc,
                   struct uvc_buffer *buf, const u8 *data, int len)
{
    /* 1. 检查JPEG帧头 */
    if (len < 2 || data[0] != 0xff || data[1] != 0xd8) {
        uvc_trace(UVC_TRACE_FRAME, "Invalid JPEG header\n");
        return;
    }
    
    /* 2. 查找帧尾(0xff 0xd9) */
    const u8 *end = data + len;
    const u8 *sof = memchr(data, 0xff, len);
    
    /* 3. 复制数据到V4L2缓冲区 */
    memcpy(buf->mem + buf->bytesused, data, len);
    buf->bytesused += len;
    
    /* 4. 检查SOF(帧开始)标记 */
    if (sof && sof + 1 < end && sof[1] == 0xc0) {
        /* 5. 解析JPEG帧头获取分辨率 */
        uvc_parse_jpeg_header(uvc, sof);
    }
}

3. V4L2核心框架分析

3.1 V4L2 ioctl调用树形分析
drivers/media/v4l2-core/ (V4L2核心源码)
├── v4l2-ioctl.c (用户空间接口核心)
│   ├── video_ioctl2() ←─ 所有V4L2 ioctl的入口
│   │   ├── 1. 参数验证和复制
│   │   │   ├── copy_from_user() / copy_to_user()
│   │   │   └── 安全检查
│   │   │
│   │   ├── 2. 分发到具体处理函数
│   │   │   ├── 命令分类: VIDIOC_xxx
│   │   │   └── 根据文件操作结构调用
│   │   │
│   │   └── 3. 调用驱动注册的处理函数
│   │       ├── vdev->ioctl_ops->xxx()
│   │       └── 对应UVC驱动的操作
│   │
│   └── v4l_ioctl_handler[] (ioctl处理程序表)
│       ├── [VIDIOC_QUERYCAP] = v4l_querycap
│       ├── [VIDIOC_ENUM_FMT] = v4l_enum_fmt
│       ├── [VIDIOC_S_FMT] = v4l_s_fmt
│       ├── [VIDIOC_G_FMT] = v4l_g_fmt
│       ├── [VIDIOC_REQBUFS] = v4l_reqbufs
│       ├── [VIDIOC_QUERYBUF] = v4l_querybuf
│       ├── [VIDIOC_QBUF] = v4l_qbuf
│       ├── [VIDIOC_DQBUF] = v4l_dqbuf
│       ├── [VIDIOC_STREAMON] = v4l_streamon
│       └── [VIDIOC_STREAMOFF] = v4l_streamoff
│
├── v4l2-dev.c (设备管理)
│   └── v4l2_open() / v4l2_release()
│       ├── 打开设备时调用驱动open
│       └── 关闭时清理资源
│
└── videobuf2-core.c (缓冲区管理)
    └── 四种内存模型实现:
        ├── 1. videobuf2-vmalloc.c
        │   ├── vmalloc分配器
        │   └── 小缓冲区,简单应用
        │
        ├── 2. videobuf2-dma-contig.c
        │   ├── 连续DMA内存
        │   └── 硬件编解码要求
        │
        ├── 3. videobuf2-dma-sg.c
        │   ├── 分散/聚集DMA
        │   └── 大缓冲区,高性能
        │
        └── 4. videobuf2-v4l2.c
            ├── V4L2兼容层
            └── 用户空间接口
3.2 关键ioctl流程详述

VIDIOC_S_FMT (设置格式) - 对应分辨率设置:

/* 用户空间调用ioctl(fd, VIDIOC_S_FMT, &format)时的内核路径 */
​
static int v4l_s_fmt(const struct v4l2_ioctl_ops *ops,
             struct file *file, void *fh, void *arg)
{
    struct v4l2_format *fmt = arg;
    struct video_device *vdev = video_devdata(file);
    int ret;
    
    /* 1. 格式参数验证 */
    ret = v4l_enforce_validation(vdev, fmt);
    if (ret)
        return ret;
    
    /* 2. 调用驱动特定的s_fmt函数 */
    if (ops->vidioc_s_fmt_vid_cap) {
        ret = ops->vidioc_s_fmt_vid_cap(file, fh, fmt);
    } else if (ops->vidioc_s_fmt) {
        ret = ops->vidioc_s_fmt(file, fh, fmt);
    }
    
    /* 3. UVC驱动的具体实现 (drivers/media/usb/uvc/uvc_v4l2.c) */
    static int uvc_v4l2_set_format(struct file *file, void *fh,
                     struct v4l2_format *fmt)
    {
        struct uvc_streaming *stream = video_drvdata(file);
        struct uvc_format *format;
        struct uvc_frame *frame;
        unsigned int i;
        
        /* 3.1 查找请求的格式 */
        format = uvc_format_by_guid(stream, 
                    fmt->fmt.pix.pixelformat);
        if (!format)
            return -EINVAL;
        
        /* 3.2 查找支持的分辨率 */
        for (i = 0; i < format->nframes; ++i) {
            frame = &format->frame[i];
            if (frame->wWidth == fmt->fmt.pix.width &&
                frame->wHeight == fmt->fmt.pix.height)
                break;
        }
        if (i == format->nframes)
            return -EINVAL;
        
        /* 3.3 设置格式到硬件 */
        return uvc_video_set_format(stream, format, frame);
    }
    
    /* 4. 更新内部状态 */
    vdev->current_format = *fmt;
    
    return ret;
}

VIDIOC_REQBUFS (请求缓冲区) - 内存分配关键:

/* 用户空间调用ioctl(fd, VIDIOC_REQBUFS, &reqbufs) */
​
static int v4l_reqbufs(const struct v4l2_ioctl_ops *ops,
               struct file *file, void *fh, void *arg)
{
    struct v4l2_requestbuffers *reqbufs = arg;
    struct vb2_queue *q;
    int ret;
    
    /* 1. 获取视频缓冲区队列 */
    q = v4l2_get_vb2_queue(file);
    
    /* 2. 验证参数 */
    if (reqbufs->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
        return -EINVAL;
    
    /* 3. 设置缓冲区内存类型 */
    switch (reqbufs->memory) {
    case V4L2_MEMORY_MMAP:
        q->memory = VB2_MEMORY_MMAP;
        break;
    case V4L2_MEMORY_USERPTR:
        q->memory = VB2_MEMORY_USERPTR;
        break;
    case V4L2_MEMORY_DMABUF:
        q->memory = VB2_MEMORY_DMABUF;
        break;
    default:
        return -EINVAL;
    }
    
    /* 4. 分配缓冲区 (关键步骤) */
    ret = vb2_reqbufs(q, reqbufs);
    
    /* 5. UVC驱动的具体实现 */
    static int uvc_request_buffers(struct uvc_video_queue *queue,
                     struct v4l2_requestbuffers *rb)
    {
        unsigned int size;
        
        /* 5.1 计算每个缓冲区大小 */
        size = queue->format->framesize.width *
               queue->format->framesize.height *
               queue->format->bpp;
        
        /* 5.2 调用videobuf2分配 */
        return vb2_reqbufs(&queue->queue, rb);
    }
    
    return ret;
}

4. 内存映射(MMAP)机制分析

从用户空间mmap到内核缓冲区的完整路径:

用户空间mmap(/dev/video0)调用链:
├── 1. 用户空间调用: mmap(addr, length, prot, flags, fd, offset)
│   └── 进入内核: SYSCALL_DEFINE6(mmap_pgoff, ...)
│
├── 2. 文件系统层处理
│   └── video_fops.mmap = uvc_v4l2_mmap
│
├── 3. V4L2通用mmap处理 (v4l2_mmap)
│   ├── 获取vb2_queue结构
│   ├── 验证映射参数
│   └── 调用vb2_mmap
│
├── 4. videobuf2 mmap实现 (vb2_mmap)
│   ├── 根据缓冲区索引查找vb2_buffer
│   ├── 调用内存分配器的mmap方法
│   └── 建立页表映射
│
├── 5. 具体分配器实现 (以vmalloc为例)
│   └── vb2_vmalloc_mmap()
│       ├── 获取vmalloc分配的虚拟地址
│       ├── remap_vmalloc_range() ←─ 关键函数
│       └── 建立用户空间到内核缓冲区的直接映射
│
└── 6. 映射完成
    ├── 用户空间获得缓冲区的直接指针
    └── 零拷贝数据访问

关键的内存映射实现代码:

/* drivers/media/common/videobuf2/videobuf2-vmalloc.c */
​
static int vb2_vmalloc_mmap(void *buf_priv, struct vm_area_struct *vma)
{
    struct vb2_vmalloc_buf *buf = buf_priv;
    
    /* 1. 验证映射参数 */
    if (vma->vm_end - vma->vm_start > buf->size) {
        pr_err("mmap size exceeds buffer size\n");
        return -EINVAL;
    }
    
    /* 2. 设置VMA标志 */
    vma->vm_ops = &vb2_common_vm_ops;
    vma->vm_private_data = buf;
    vma->vm_flags |= VM_DONTEXPAND | VM_DONTDUMP;
    
    /* 3. 建立映射 - 关键步骤 */
    if (remap_vmalloc_range(vma, buf->vaddr, 0)) {
        pr_err("remap_vmalloc_range failed\n");
        return -EAGAIN;
    }
    
    /* 4. 增加缓冲区引用计数 */
    atomic_inc(&buf->refcount);
    
    return 0;
}
​
/* remap_vmalloc_range内部实现 (mm/vmalloc.c) */
int remap_vmalloc_range(struct vm_area_struct *vma, void *addr,
            unsigned long pgoff)
{
    /* 将vmalloc分配的内核虚拟地址映射到用户空间 */
    unsigned long uaddr = (unsigned long)vma->vm_start;
    unsigned long usize = vma->vm_end - vma->vm_start;
    
    /* 遍历页表,建立映射 */
    for (; usize > 0; uaddr += PAGE_SIZE, addr += PAGE_SIZE,
         usize -= PAGE_SIZE, pgoff++) {
        
        /* 获取内核页帧号 */
        pfn = vmalloc_to_pfn(addr);
        
        /* 映射到用户空间 */
        err = vm_insert_page(vma, uaddr, pfn_to_page(pfn));
        if (err)
            return err;
    }
    
    return 0;
}

5. 视频数据缓冲区在内核中的完整生命周期

/* 缓冲区状态机与数据流 */
​
enum vb2_buffer_state {
    VB2_BUF_STATE_DEQUEUED,    /* 缓冲区在用户空间 */
    VB2_BUF_STATE_PREPARING,   /* 正在准备 */
    VB2_BUF_STATE_QUEUED,      /* 已排队等待填充 */
    VB2_BUF_STATE_ACTIVE,      /* 正在被硬件使用 */
    VB2_BUF_STATE_DONE,        /* 数据就绪,等待读取 */
    VB2_BUF_STATE_ERROR,       /* 错误状态 */
};
​
/* 缓冲区流转过程:
 * 
 * 1. REQBUFS: 分配N个缓冲区 (状态: DEQUEUED)
 *    ├── 每个缓冲区大小 = width × height × bpp
 *    └── 总内存 = N × 缓冲区大小
 * 
 * 2. QBUF: 用户空间将缓冲区提交给驱动 (状态: QUEUED)
 *    ├── 缓冲区加入硬件队列
 *    └── 等待数据填充
 * 
 * 3. 硬件填充数据 (状态: ACTIVE)
 *    ├── UVC驱动接收USB数据
 *    ├── 解码视频帧
 *    └── 写入缓冲区
 * 
 * 4. 数据就绪 (状态: DONE)
 *    ├── 设置bytesused字段
 *    └── 触发可读事件
 * 
 * 5. DQBUF: 用户空间读取数据 (状态: DEQUEUED)
 *    ├── 复制数据或通过mmap直接访问
 *    └── 缓冲区返回用户空间控制
 * 
 * 6. 循环2-5,形成环形缓冲区
 */

6. 性能关键指标与优化点

6.1 内核空间性能计数器
/* 可监控的内核性能指标 */
struct uvc_perf_stats {
    /* 数据流统计 */
    unsigned long frames_received;     /* 接收的总帧数 */
    unsigned long frames_completed;    /* 完整处理的帧数 */
    unsigned long frames_dropped;      /* 丢弃的帧数 */
    
    /* 时间统计 */
    ktime_t last_frame_time;           /* 上一帧时间 */
    u64 avg_frame_interval;            /* 平均帧间隔(ns) */
    
    /* 缓冲区统计 */
    unsigned int buffer_hit_count;     /* 缓冲区命中次数 */
    unsigned int buffer_miss_count;    /* 缓冲区未命中次数 */
    
    /* USB传输统计 */
    unsigned long urb_submit_count;    /* URB提交次数 */
    unsigned long urb_complete_count;  /* URB完成次数 */
    unsigned long urb_error_count;     /* URB错误次数 */
    
    /* DMA统计 */
    dma_addr_t dma_alloc_size;         /* DMA分配的总大小 */
    unsigned int dma_map_count;        /* DMA映射次数 */
};
​
/* 通过debugfs暴露统计信息 */
static int uvc_debugfs_stats_show(struct seq_file *s, void *v)
{
    struct uvc_device *uvc = s->private;
    
    seq_printf(s, "UVC Performance Statistics:\n");
    seq_printf(s, "  Frames received:    %lu\n", 
               uvc->stats.frames_received);
    seq_printf(s, "  Frames completed:   %lu\n", 
               uvc->stats.frames_completed);
    seq_printf(s, "  Frame drop rate:    %.2f%%\n",
               (uvc->stats.frames_dropped * 100.0) /
               max(uvc->stats.frames_received, 1UL));
    seq_printf(s, "  Average FPS:        %.2f\n",
               1e9 / max(uvc->stats.avg_frame_interval, 1ULL));
    seq_printf(s, "  Buffer hit ratio:   %.2f%%\n",
               (uvc->stats.buffer_hit_count * 100.0) /
               max(uvc->stats.buffer_hit_count + 
                   uvc->stats.buffer_miss_count, 1UL));
    
    return 0;
}
6.2 内核优化配置参数
# 针对UVC视频流优化的内核启动参数
# 添加到 /etc/default/grub 的 GRUB_CMDLINE_LINUX
​
# USB相关优化
usbcore.autosuspend=-1      # 禁用USB自动挂起
usbcore.usbfs_memory_mb=256 # 增加USB文件系统内存
usbhid.mousepoll=0          # 减少USB HID轮询干扰
​
# 内存和调度优化
vm.dirty_ratio=10           # 减少脏页比例
vm.dirty_background_ratio=5 # 后台脏页比例
vm.swappiness=10            # 减少交换倾向
​
# 网络和文件系统优化(如果涉及网络传输)
net.core.rmem_max=26214400  # 增加接收缓冲区
net.core.wmem_max=26214400  # 增加发送缓冲区
​
# 实时性优化(对低延迟重要)
kernel.sched_rt_runtime_us=950000
kernel.sched_rt_period_us=1000000

7. 故障排查与调试接口

7.1 内核调试接口使用
# 1. 查看UVC设备信息
cat /sys/kernel/debug/uvcvideo/*/status
​
# 2. 监控V4L2缓冲区状态
v4l2-ctl --device=/dev/video0 --list-formats
v4l2-ctl --device=/dev/video0 --get-fmt-video
v4l2-ctl --device=/dev/video0 --get-input
v4l2-ctl --device=/dev/video0 --set-fmt-video=width=1280,height=720
​
# 3. USB传输监控
cat /sys/kernel/debug/usb/devices  # 查看USB设备树
lsusb -v -d 046d:0825              # 查看特定设备详细信息
cat /sys/kernel/debug/usb/uvcvideo/*/streaming  # UVC流状态
​
# 4. 性能事件追踪
perf record -e 'uvc:*' -a           # 追踪UVC事件
perf record -e 'sched:*' -a         # 追踪调度事件
perf record -e 'irq:*' -a           # 追踪中断事件
​
# 5. Ftrace内核函数追踪
echo 1 > /sys/kernel/debug/tracing/events/uvc/enable
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo uvc_video_complete > /sys/kernel/debug/tracing/set_graph_function
cat /sys/kernel/debug/tracing/trace_pipe  # 查看实时追踪
7.2 内核日志解析
# 典型的内核日志消息及其含义
dmesg | grep -i uvc
​
# 正常初始化日志
[   1.234567] uvcvideo: Found UVC 1.00 device Webcam C930e (046d:0825)
[   1.234568] uvcvideo: UVC non compliance - GET_DEF(PROBE) not supported
[   1.234569] uvcvideo: 2 video input terminals found
[   1.234570] uvcvideo: 3 video output terminals found
[   1.234571] uvcvideo: 1 camera processing unit found
[   1.234572] input: UVC Camera (046d:0825) as /devices/.../input/input15
​
# 流开始日志
[   5.678901] uvcvideo: 4 video streaming interfaces found
[   5.678902] uvcvideo: Streaming with bandwidth 49152 bytes/interval
[   5.678903] uvcvideo: 6 supported frame sizes for MJPEG
[   5.678904] uvcvideo: 1280x720 selected
​
# 错误日志
[  10.123456] uvcvideo: Failed to submit URB (-ENOMEM)
[  10.123457] uvcvideo: Frame 42 too small (1024 bytes)
[  10.123458] uvcvideo: VS request failed (-71)

8. 硬件特定优化(不同UVC芯片组)

不同UVC摄像头芯片的内核驱动特性:

芯片组 驱动模块 特性支持 优化参数
Realtek uvcvideo通用 基础UVC 1.0 默认参数即可
Sunplus uvcvideo + 补丁 可能需要quirks 设置uvcvideo.quirks=128
Microdia uvcvideo + gspca 老式摄像头 优先使用gspca驱动
Sonix sn9c20x专用驱动 较高性能 启用DMA缓冲
Logitech uvcvideo + leds LED控制支持 自动曝光优化

总结:内核层的关键作用与性能瓶颈

通过深入内核源码分析,可以看到:

  1. 数据路径优化:从USB中断到用户空间mmap的完整零拷贝路径已高度优化

  2. 内存管理复杂性:V4L2的四种内存模型适应不同硬件能力

  3. 实时性挑战:USB传输的异步性与视频流的同步性需要精细平衡

  4. 硬件多样性:UVC标准的灵活性导致不同设备的quirks处理

常见性能瓶颈点

  • USB带宽不足(特别是USB2.0上的高清视频)

  • 内核-用户空间上下文切换开销

  • 内存复制操作(如果未使用零拷贝)

  • 调度延迟导致帧丢失

优化建议

  1. 优先使用MMAP内存模型

  2. 适当增加缓冲区数量(4-8个)

  3. 确保使用USB3.0端口

  4. 调整内核调度参数以减少延迟

第四部分:运行与调试

一:立即启动本地服务器并测试

1. 启动一个简易的Web服务器: 在终端项目目录下,运行以下命令:

python3 -m http.server 8080

看到输出 Serving HTTP on 0.0.0.0 port 8080 ... 说明服务器已启动。

2. 打开浏览器测试: 保持终端运行,打开Chrome或Firefox,访问:

http://localhost:8080

二:工作流程树形解析

【用户执行命令】python3 -m http.server 8080
├─ 【Python解释器】加载并执行 `http.server` 模块
│  ├─ 【开源库:socketserver】创建网络服务器框架
│  │  └─ 调用【Linux系统调用:socket()】创建监听套接字(AF_INET, SOCK_STREAM)
│  ├─ 调用【Linux系统调用:bind()】将套接字绑定到 0.0.0.0:8080
│  ├─ 调用【Linux系统调用:listen()】开始监听连接
│  └─ 进入循环,调用【Linux系统调用:accept()】等待客户端连接
│
├─ 【用户操作】浏览器访问 http://localhost:8080
│  ├─ 【浏览器】解析URL,向操作系统发起对 `localhost:8080` 的TCP连接
│  │  └─ 触发【Linux系统调用:connect()】
│  ├─ 【Python http.server】`accept()` 返回,建立连接
│  │  ├─ 创建新线程/进程处理请求(`ThreadingMixIn`)
│  │  ├─ 调用【Linux系统调用:recv()】读取HTTP请求头(GET / HTTP/1.1 ...)
│  │  ├─ 解析请求,定位到 `/home/tang/.../index.html` 文件
│  │  │  └─ 调用【Linux系统调用:open()、read()】读取文件内容
│  │  ├─ 构建HTTP响应头(Content-Type: text/html)
│  │  └─ 调用【Linux系统调用:send()】将响应头和文件内容发回浏览器
│  └─ 【浏览器】接收数据,解析HTML,开始渲染页面
│
├─ 【页面加载】浏览器解析到 <video id="videoPreview">
│  └─ 创建空的HTMLMediaElement对象
│
├─ 【页面加载】浏览器解析到 <script src="app.js">
│  └─ 再次向 `http://localhost:8080` 发起HTTP请求获取JS文件
│      ├─ (重复上述 socket/recv/send 过程)
│      └─ 获取后执行JavaScript代码
│
├─ 【应用初始化】执行 app.js 中的 CameraMonitorApp 类构造函数
│  ├─ 初始化 CameraController、ScreenshotManager
│  ├─ 调用 initializeEventListeners() 绑定按钮事件
│  │  └─ 【开源库:浏览器DOM API】document.getElementById(...).addEventListener(...)
│  └─ 至此,静态页面变为可交互应用
│
└─ 【核心交互】用户点击“启动摄像头”按钮
   ├─ 触发绑定的 onclick 事件,调用 app.startCamera()
   ├─ CameraController.startCamera() 执行
   │  ├─ 调用【浏览器Web API】navigator.mediaDevices.getUserMedia({ video: {...} })
   │  │  ├─ 【浏览器内部】检查页面来源(localhost)安全性(安全上下文)
   │  │  ├─ 弹出摄像头权限请求窗口(用户点击“允许”)
   │  │  └─ 【浏览器】通过底层进程与操作系统交互
   │  │      ├─ 【Linux桌面环境(如GNOME)】可能介入权限管理(通过Portals等)
   │  │      └─ 最终,浏览器进程调用【Linux系统调用:ioctl()】
   │  │          └─ 与 /dev/video0 设备通信,指令为 V4L2(Video4Linux2)协议
   │  ├─ 浏览器获取到视频流(MediaStream对象)
   │  └─ 将流赋值给 videoElement.srcObject
   │      └─ 【浏览器渲染引擎】解码MJPG/YUYV帧,逐帧绘制到 <video> 标签区域
   └─ 用户看到实时视频,调试成功。
  1. V4L2 (Video for Linux 2)

    • 角色:Linux内核的视频捕获框架。

    • 工作原理:当浏览器(通过ioctl)与/dev/video0交互时,内核的V4L2子系统将调用对应的摄像头驱动(很可能是 uvcvideo,一个开源的内核驱动模块,用于支持USB Video Class标准摄像头)。驱动通过USB总线控制硬件,采集压缩的MJPG或原始的YUYV数据,再通过V4L2框架返回给用户空间(浏览器)。之前运行的 v4l2-ctl 正是与这一层交互的用户空间工具。

  2. 浏览器引擎(以Chromium为例)

    • 角色:整个Web应用的运行容器和技术栈整合者。

    • 工作原理

      • 网络栈:处理HTTP(S)请求,使用类似 libcurl 的开源库。

      • 渲染引擎(Blink):解析HTML/CSS,布局页面。

      • JavaScript引擎(V8):执行 app.js

      • WebRTC/MediaStream:这是实现 getUserMedia API的关键内部组件。它作为中间层,将标准化的JavaScript API调用,翻译成对特定操作系统(Linux)媒体子系统的调用。

  3. Linux桌面环境的门户(Portal)系统

    • 角色:在沙盒化应用(如Flatpak Snap或某些浏览器)中统一管理权限。

    • 工作原理:当浏览器运行在沙盒中时,它不能直接访问/dev/video0。此时,它会通过D-Bus向xdg-desktop-portal等服务发起请求,该服务会弹出系统的权限对话框,并在用户授权后,通过一个特殊的文件描述符将摄像头访问权“管道”传递给浏览器。这是一个重要的抽象层。

三: 问题诊断的实际应用

理解了这个树形流程,“按键无反应”问题可能发生在从下至上的多个环节:

  • 叶子节点问题(用户层)app.js 代码中 getUserMedia 的约束条件(constraints)与摄像头能力不匹配(但根据v4l2-ctl的输出,大概率是匹配的)。

  • 分支节点问题(权限/沙盒):浏览器或系统门户没有弹出权限请求,或请求被阻止。

  • 树干问题(驱动/内核):已经用v4l2-ctlcheese排除了这一层。

因此,当前最有效的调试步骤,依然是回到浏览器控制台(Console),执行上一步讨论的诊断命令。这相当于从树顶(JavaScript代码) 直接探测到哪个分支节点出现了阻塞。当执行 navigator.mediaDevices.getUserMedia(...).catch(e => console.error(e)) 时,捕获的 e.namee.message 就是系统反馈、关于哪个环节失败的精确错误码。

如果在之前的操作中已经看到了具体的错误信息(例如 NotAllowedErrorNotFoundError),结合这个树形图,就能立刻明白问题对应在哪一层,以及该如何解决。请尝试并提供错误信息,可以进行最精确的定位。

四:运行结果

第四部分:拓展HTML/JS/CSS相互调用树形分析与Web本地服务架构

1. HTML/JS/CSS调用关系深度树形分析

1.1 应用启动阶段的硬核调用链
应用初始化调用树(冷启动 - DOMContentLoaded事件触发):
┌─ index.html (骨架加载)
│  ├── 同步加载: <script src="https://cdn.tailwindcss.com"></script>
│  │   └── 立即执行: tailwindcss 解析并注入 3MB+ 的CSS规则到内存
│  │       └── 生成CSSOM树: 约8000+个CSS规则类
│  │
│  ├── 同步加载: <link rel="stylesheet" href="https://cdnjs.cloudflare.com...">
│  │   └── Font Awesome CSS: 添加1800+图标类到CSSOM
│  │
│  ├── 同步加载: <link rel="stylesheet" href="styles.css">
│  │   └── 自定义CSS: 注入关键动画和布局规则
│  │       ├── 关键帧: @keyframes pulse, @keyframes slideIn
│  │       ├── GPU加速类: transform: translateZ(0)
│  │       └── 模态框过渡: transition: all 0.3s ease
│  │
│  └── 异步加载: <script src="app.js" type="module"></script> ←─ 关键!type="module"
│      ├── ES6模块特性: 延迟执行,在DOM解析后执行
│      ├── 依赖解析: 无import语句,因此立即执行主体代码
│      └── 执行入口: 底部注释"app.js加载完成,等待DOMContentLoaded事件..."
│
├─ DOMContentLoaded 事件触发 (约100ms后)
│  └── 主初始化流程: initializeApplication()
│      ├── 1. 浏览器兼容性核验: checkBrowserCompatibility()
│      │   ├── 硬性检查: mediaDevices, getUserMedia, localStorage, canvas
│      │   └── 软性检查: MediaRecorder, WebRTC (仅警告)
│      │
│      ├── 2. 创建应用核弹头: window.app = new CameraMonitorApp()
│      │   ├── 构造函数调用链:
│      │   │   ├── new PerformanceMonitor() ←─ 性能监控启动
│      │   │   ├── new ScreenshotManager() ←─ 立即加载localStorage
│      │   │   │   └── loadFromStorage(): 读取camera_screenshots_v2键
│      │   │   │       ├── JSON.parse可能阻塞: 大数据量时主线程卡顿
│      │   │   │       └── 内存分配: 每个截图约 (width*height*0.75 + 500)字节
│      │   │   │
│      │   │   ├── new CameraController(videoElement) ←─ 视频核心初始化
│      │   │   │   └── 设置formatPriority: 硬编码分辨率优先级
│      │   │   │
│      │   │   └── 应用状态初始化: appState = 'idle'
│      │   │
│      │   └── 配置初始化: config = {...}
│      │
│      ├── 3. 完整初始化链: await app.initialize()
│      │   └── 7步初始化过程(同步阻塞链):
│      │       ├── 1. loadConfiguration() ←─ 阻塞读取localStorage
│      │       ├── 2. initializeUIComponents() ←─ 事件监听器绑定风暴
│      │       │   ├── 14个addEventListener同步绑定
│      │       │   ├── 8个DOM元素属性直接设置
│      │       │   └── 潜在性能炸弹: 如果DOM元素不存在则静默失败
│      │       │
│      │       ├── 3. updateDeviceList() ←─ 异步但await阻塞
│      │       │   ├── navigator.mediaDevices.getUserMedia({video:true}) ←─ 权限弹窗!
│      │       │   └── navigator.mediaDevices.enumerateDevices()
│      │       │
│      │       ├── 4. loadScreenshots() ←─ 二次读取localStorage
│      │       ├── 5. restoreAppState() ←─ 配置检查
│      │       ├── 6. performance.startMonitoring() ←─ 启动2秒轮询定时器
│      │       └── 7. startUptimeCounter() ←─ 启动1秒轮询定时器
│      │
│      └── 4. 全局暴露: window.debugApp = () => window.app.debug()
│
└─ window.onload 事件 (所有资源加载完成)
   └── 此时视频流尚未启动,除非autoStart为true
1.2 事件驱动的硬核调用网分析
按钮点击事件调用网(startBtn点击为例):
┌─ DOM事件捕获阶段 (从window向下)
│  └── 无监听器
│
├─ 目标阶段 (startBtn元素)
│  └── startBtn.addEventListener('click', () => this.startCamera())
│      ├── this绑定: 箭头函数确保this指向CameraMonitorApp实例
│      └── 闭包捕获: 完整app上下文被闭包引用
│
└─ 事件冒泡阶段 (向上到document)
   └── 无监听器
​
startCamera()执行调用链(硬核剖析):
1. 状态检查: if (this.appState === 'streaming') → return
2. 状态设置: this.appState = 'starting'
3. UI状态更新: this.updateUIState() ←─ 触发DOM重排/重绘风暴
   ├── 4个按钮样式修改: display, disabled, innerHTML
   ├── 状态覆盖层更新: innerHTML替换
   ├── 录制指示器: classList.add/remove('hidden')
   └── 统计信息更新: textContent修改
   
4. 分辨率解析: const [w, h] = resolution.split('x').map(Number)
   └── 可能异常: 如果resolution格式错误则w/h为NaN
​
5. 构建选项: options对象创建
   ├── deviceId: this.selectedDeviceId (可能为null)
   ├── width/height: 解析后的数值
   └── frameRate: this.config.defaultFrameRate
​
6. 调用摄像头控制器: await this.cameraController.start(options)
   ├── 内部调用: this.cameraController.start() ←─ 真正的硬件交互
   │   ├── this.buildConstraints(options) ←─ 转换为MediaStreamConstraints
   │   │   └── 输出: { video: { deviceId: {exact: id}, width: {ideal: w}, ... } }
   │   │
   │   ├── navigator.mediaDevices.getUserMedia(constraints) ←─ 系统级调用
   │   │   ├── 浏览器内部: 权限对话框弹出(如果未授权)
   │   │   ├── 系统调用: 访问/dev/video0设备文件
   │   │   └── 返回Promise: 可能拒绝(NotAllowedError等)
   │   │
   │   ├── 视频轨道获取: const tracks = this.mediaStream.getVideoTracks()
   │   ├── 视频元素连接: this.videoElement.srcObject = this.mediaStream
   │   └── 等待就绪: this.waitForVideoReady() ←─ 5秒超时Promise
   │
   └── 应用状态更新: this.appState = 'streaming'
​
7. 性能监控: this.performance.markEnd('camera_start')
​
8. UI二次更新: this.updateUIState() ←─ 再次触发DOM操作
​
9. 帧率监控启动: this.startFrameRateMonitoring()
   └── requestAnimationFrame递归调用 ←─ 每16.67ms执行一次
       ├── 性能影响: 即使摄像头关闭也会在下次启动时创建新循环
       └── 内存泄漏风险: 没有对应的cancelAnimationFrame
1.3 CSS对JavaScript的隐形调用与性能影响
CSS动画与JS性能的隐形调用关系:
┌─ CSS动画触发JS事件(开发者常忽略的细节)
│  ├── 动画开始: animationstart 事件
│  ├── 动画迭代: animationiteration 事件
│  └── 动画结束: animationend 事件
│
├─ CSS变换触发的JS重排/重绘链
│  └── 代码中存在的隐形性能问题:
│      ├── .screenshot-item:hover { transform: scale(1.02); }
│      │   └── hover时: 触发GPU层合成,但布局未变 ←─ 性能良好
│      │
│      ├── .btn-primary:hover { transform: translateY(-2px); }
│      │   └── 同上,GPU加速 ←─ 性能良好
│      │
│      └── modal隐藏/显示: classList.add/remove('hidden')
│          ├── display: none 与 opacity: 0 切换
│          ├── 关键问题: 同时修改opacity和transition
│          └── 触发: 样式重计算 → 布局 → 绘制 → 合成 全链路
│
└─ Tailwind CSS的硬核性能影响
   ├── 文件大小: 开发版本3MB+,生产版本压缩后约50KB
   ├── 解析时间: 浏览器需要解析约8000个CSS规则
   ├── 内存占用: CSSOM树占用约2-5MB内存
   └── 关键优化缺失: 未使用PurgeCSS移除未使用的CSS

2. Web本地服务与访问机制架构树形分析

2.1 本地开发服务器架构深度剖析
基于file://协议 vs HTTP本地服务器的硬核对比:
┌─ 方案A: 直接file://协议访问(代码当前状态)
│  ├── URL: file:///home/user/camera-app/index.html
│  ├── 优点: 零配置,直接打开
│  └── 致命限制:
│      ├── 1. Cross-Origin限制: JavaScript无法访问本地文件
│      │   └── 截图保存到localStorage正常,但无法直接保存为文件
│      │
│      ├── 2. 摄像头访问限制: 现代浏览器限制file://访问媒体设备
│      │   └── Chrome策略: file://需要--allow-file-access-from-files标志
│      │
│      ├── 3. Service Worker不支持: 无法实现离线缓存
│      ├── 4. 某些API限制: 如WebUSB、Web蓝牙
│      └── 5. 相对路径问题: <link href="styles.css">可能404
│
┌─ 方案B: Python简易HTTP服务器(推荐开发用)
│  ├── 启动: python3 -m http.server 8080
│  ├── 架构: 单线程同步HTTP/1.1服务器
│  └── 处理流程:
│      ┌─ 浏览器请求: GET http://localhost:8080/index.html
│      │  └── TCP握手: localhost:8080 (环回接口,0.1ms延迟)
│      │
│      ├─ Python http.server处理链:
│      │  ├── 1. socketserver.TCPServer接收连接
│      │  ├── 2. SimpleHTTPRequestHandler.handle()
│      │  │   ├── 解析HTTP头: GET /index.html HTTP/1.1
│      │  │   ├── 安全检查: 路径遍历防护
│      │  │   ├── 文件系统映射: /index.html → ./index.html
│      │  │   ├── MIME类型推断: .html → text/html
│      │  │   └── 发送响应: 文件内容 + HTTP头
│      │  │
│      │  └── 3. 连接保持: 默认HTTP/1.1 keep-alive
│      │
│      └─ 浏览器接收响应:
│         ├── 解析HTML,发现外部资源
│         ├── 并发请求: styles.css, app.js, CDN资源
│         └── 关键: 所有资源同源(localhost:8080),无CORS限制
│
┌─ 方案C: Node.js + Express生产级架构
│  ├── 核心架构:
│  │   server.js
│  │   ├── const express = require('express')
│  │   ├── const app = express()
│  │   ├── app.use(express.static('public')) ←─ 静态文件服务
│  │   ├── app.get('/api/screenshots', ...) ←─ 自定义API
│  │   └── app.listen(3000)
│  │
│  └── 高级特性支持:
│      ├── WebSocket支持: 实时视频流传输
│      ├── 文件上传API: 替代localStorage的截图存储
│      ├── 用户认证: 多用户摄像头访问控制
│      └── 数据库集成: 截图元数据持久化
│
┌─ 方案D: 现代化Vite开发服务器(终极方案)
│  ├── 启动: npx vite --host localhost --port 3000
│  ├── 核心优势: 原生ES模块支持,HMR热更新
│  └── 架构亮点:
│      ├── 基于ESBuild: 比Webpack快10-100倍
│      ├── 按需编译: 只编译请求的模块
│      ├── 即时HMR: 修改CSS/JS无需刷新页面
│      └── 生产构建: Rollup打包,自动代码分割
2.2 浏览器-服务器通信机制硬核分析
HTTP请求-响应全链路剖析(以加载index.html为例):
┌─ 浏览器发起请求阶段
│  ├── 1. URL解析: http://localhost:8080/index.html
│  │   ├── 协议: http
│  │   ├── 主机: localhost (解析为127.0.0.1)
│  │   ├── 端口: 8080
│  │   └── 路径: /index.html
│  │
│  ├── 2. HTTP请求构造
│  │   └── 原始请求数据:
│  │       GET /index.html HTTP/1.1
│  │       Host: localhost:8080
│  │       User-Agent: Mozilla/5.0...
│  │       Accept: text/html,application/xhtml+xml...
│  │       Accept-Encoding: gzip, deflate
│  │       Connection: keep-alive
│  │
│  └── 3. TCP连接建立
│      ├── SYN → SYN-ACK → ACK 三次握手
│      └── 本地环回,延迟<1ms
│
├─ 服务器处理阶段
│  └── Python http.server处理时序:
│      ┌─ 0ms: accept() 接受连接
│      ├─ 1ms: recv() 读取请求头 (约500字节)
│      ├─ 2ms: parse_request() 解析HTTP请求
│      ├─ 3ms: translate_path() 路径安全转换
│      ├─ 4ms: os.stat() 检查文件是否存在
│      ├─ 5ms: guess_type() 根据扩展名猜测MIME类型
│      ├─ 6ms: open() 打开文件 (磁盘IO,约1-10ms)
│      ├─ 16ms: read() 读取文件内容 (约10KB)
│      ├─ 17ms: send_response(200) 发送状态行
│      ├─ 18ms: send_header() 发送Content-Type等头部
│      ├─ 19ms: end_headers() 空行结束头部
│      └─ 20ms: write() 发送文件内容
│
├─ 浏览器接收与解析阶段
│  ├── 21ms: 接收HTTP响应头
│  │   └── 解析关键头:
│  │       HTTP/1.1 200 OK
│  │       Content-Type: text/html; charset=utf-8
│  │       Content-Length: 10240
│  │
│  ├── 22ms: 开始接收响应体
│  ├── 23ms: HTML解析器启动 (预解析扫描<link>/<script>)
│  │   ├── 发现: <link rel="stylesheet" href="styles.css">
│  │   │   └── 高优先级请求: 立即发起CSS请求
│  │   │
│  │   ├── 发现: <script src="app.js" type="module">
│  │   │   └── 低优先级: 延迟到HTML解析后执行
│  │   │
│  │   └── 发现: CDN资源 (tailwindcss, fontawesome)
│  │       └── DNS预解析 + 预连接
│  │
│  ├── 25ms: DOM树构建完成
│  └── 30ms: 触发DOMContentLoaded事件
│
└─ 资源加载瀑布流分析
   ├── 主文档: index.html (20ms)
   ├── CSS阻塞资源:
   │   ├── styles.css (本地, 5ms)
   │   ├── tailwindcss (CDN, 50-100ms) ←─ 网络延迟主导
   │   └── fontawesome (CDN, 50-100ms)
   │
   ├── JavaScript执行:
   │   ├── app.js (本地, 2ms加载, 5ms解析)
   │   ├── 初始化执行: 约100-200ms (包括localStorage读取)
   │   └── 摄像头设备枚举: 约500-1000ms (系统调用慢)
   │
   └── 图片资源:
       └── 无阻塞,异步加载
2.3 CORS与安全机制硬核分析
本地开发中的CORS陷阱与解决方案:
┌─ 问题场景: 应用需要从不同端口获取数据
│  ├── 前端: http://localhost:3000
│  └── API服务器: http://localhost:8080
│
├─ 浏览器CORS检查流程(硬核版本):
│  ├── 简单请求检查:
│  │   ├── 方法: GET, HEAD, POST
│  │   ├── 头部限制: 仅允许安全头部
│  │   └── Content-Type: 仅 text/plain, multipart/form-data, application/x-www-form-urlencoded
│  │
│  ├── 预检请求 (Preflight) 触发条件:
│  │   ├── 非简单方法: PUT, DELETE, PATCH
│  │   ├── 自定义头部: X-API-Key, Authorization
│  │   └── 非标准Content-Type: application/json
│  │
│  └── CORS错误示例(会遇到的):
│      Access to fetch at 'http://localhost:8080/api/screenshots' from origin 
│      'http://localhost:3000' has been blocked by CORS policy: 
│      No 'Access-Control-Allow-Origin' header is present on the requested resource.
│
├─ 解决方案1: 开发服务器代理(Vite示例)
│  └── vite.config.js
│      export default {
│        server: {
│          proxy: {
│            '/api': {
│              target: 'http://localhost:8080',
│              changeOrigin: true,
│              rewrite: (path) => path.replace(/^\/api/, '')
│            }
│          }
│        }
│      }
│
├─ 解决方案2: 服务器添加CORS头部(Node.js示例)
│  └── Express中间件:
│      app.use((req, res, next) => {
│        res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
│        res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
│        res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
│        if (req.method === 'OPTIONS') return res.sendStatus(200);
│        next();
│      });
│
└─ 解决方案3: 浏览器禁用安全(仅开发)
    └── Chrome启动参数:
        chrome.exe --disable-web-security --user-data-dir=/tmp/chrome-test
        ⚠️ 高危: 不要在生产环境中使用
2.4 Service Worker与离线缓存架构
PWA离线缓存机制(摄像头应用可离线使用):
┌─ 注册Service Worker
│  └── app.js新增代码:
│      if ('serviceWorker' in navigator) {
│        navigator.serviceWorker.register('/sw.js')
│          .then(reg => console.log('SW registered:', reg))
│          .catch(err => console.log('SW registration failed:', err));
│      }
│
├─ Service Worker生命周期与缓存策略
│  └── sw.js 实现:
│      const CACHE_NAME = 'camera-app-v1';
│      const urlsToCache = [
│        '/', '/index.html', '/styles.css', '/app.js',
│        'https://cdn.tailwindcss.com',
│        'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'
│      ];
│
│      // 安装阶段 - 预缓存关键资源
│      self.addEventListener('install', event => {
│        event.waitUntil(
│          caches.open(CACHE_NAME)
│            .then(cache => cache.addAll(urlsToCache))
│        );
│      });
│
│      // 激活阶段 - 清理旧缓存
│      self.addEventListener('activate', event => {
│        event.waitUntil(
│          caches.keys().then(cacheNames => {
│            return Promise.all(
│              cacheNames.map(cacheName => {
│                if (cacheName !== CACHE_NAME) {
│                  return caches.delete(cacheName);
│                }
│              })
│            );
│          })
│        );
│      });
│
│      // 拦截请求 - 缓存优先策略
│      self.addEventListener('fetch', event => {
│        event.respondWith(
│          caches.match(event.request)
│            .then(response => {
│              // 缓存命中
│              if (response) return response;
│              
│              // 网络请求
│              return fetch(event.request).then(response => {
│                // 动态缓存(可选)
│                if (!response || response.status !== 200 || 
│                    response.type !== 'basic') {
│                  return response;
│                }
│                
│                const responseToCache = response.clone();
│                caches.open(CACHE_NAME)
│                  .then(cache => {
│                    cache.put(event.request, responseToCache);
│                  });
│                
│                return response;
│              });
│            })
│        );
│      });
│
└─ 离线功能增强
   ├── 截图缓存: 即使离线,已保存截图仍可查看
   ├── 设置持久化: localStorage在离线时正常工作
   └── 离线提示: 检测navigator.onLine状态
2.5 WebSocket实时通信架构(高级扩展)
视频流WebSocket传输架构(替代HTTP轮询):
┌─ 前端WebSocket客户端
│  └── CameraMonitorApp新增WebSocket模块:
│      class VideoWebSocket {
│        constructor(url) {
│          this.ws = new WebSocket(url);
│          this.setupEventHandlers();
│        }
│        
│        setupEventHandlers() {
│          this.ws.onopen = () => console.log('WebSocket连接已建立');
│          this.ws.onmessage = (event) => this.handleMessage(event.data);
│          this.ws.onclose = () => console.log('WebSocket连接已关闭');
│          this.ws.onerror = (error) => console.error('WebSocket错误:', error);
│        }
│        
│        // 发送控制命令
│        sendCommand(cmd, data) {
│          this.ws.send(JSON.stringify({ command: cmd, data }));
│        }
│        
│        // 接收视频帧数据
│        handleMessage(data) {
│          if (typeof data === 'string') {
│            const msg = JSON.parse(data);
│            switch (msg.type) {
│              case 'video_frame':
│                this.displayVideoFrame(msg.frame);
│                break;
│              case 'status_update':
│                this.updateStatus(msg.status);
│                break;
│            }
│          } else {
│            // 二进制数据 - 视频帧
│            this.processBinaryFrame(data);
│          }
│        }
│      }
│
├─ 后端WebSocket服务器(Node.js + ws库)
│  └── websocket-server.js:
│      const WebSocket = require('ws');
│      const wss = new WebSocket.Server({ port: 8081 });
│      
│      // V4L2摄像头捕获
│      const { spawn } = require('child_process');
│      const ffmpeg = spawn('ffmpeg', [
│        '-f', 'v4l2',
│        '-i', '/dev/video0',
│        '-f', 'mjpeg',
│        '-vf', 'scale=640:480',
│        '-qscale', '5',
│        'pipe:1'
│      ]);
│      
│      wss.on('connection', (ws) => {
│        console.log('客户端连接');
│        
│        // 发送视频帧
│        ffmpeg.stdout.on('data', (data) => {
│          if (ws.readyState === WebSocket.OPEN) {
│            ws.send(data, { binary: true });
│          }
│        });
│        
│        // 接收控制命令
│        ws.on('message', (message) => {
│          const cmd = JSON.parse(message);
│          handleCommand(cmd);
│        });
│        
│        ws.on('close', () => console.log('客户端断开'));
│      });
│
└─ 性能优化策略
   ├── 帧率控制: 服务端限制发送频率
   ├── 压缩传输: MJPEG → WebP (减少50%带宽)
   ├── 自动降级: 网络差时降低分辨率
   └── 重连机制: 断线自动重连 + 指数退避

3. 硬核性能优化与调试技术

3.1 使用Chrome DevTools进行调用链性能分析
// 在代码中添加性能标记
class PerformanceMonitor {
    constructor() {
        // 启用Chrome性能时间线标记
        if (window.performance && performance.mark) {
            this.useNativePerf = true;
        }
    }
    
    markStart(name) {
        if (this.useNativePerf) {
            performance.mark(`${name}_start`);
        }
        // ...原有逻辑
    }
    
    markEnd(name) {
        if (this.useNativePerf) {
            performance.mark(`${name}_end`);
            performance.measure(name, `${name}_start`, `${name}_end`);
            
            // 获取所有测量结果
            const measures = performance.getEntriesByName(name);
            if (measures.length > 0) {
                console.log(`[性能] ${name}: ${measures[0].duration.toFixed(2)}ms`);
                
                // 如果超过阈值,记录到Performance Observer
                if (measures[0].duration > 100) {
                    this.reportLongTask(name, measures[0].duration);
                }
            }
        }
        // ...原有逻辑
    }
}
​
// Chrome DevTools Performance面板使用流程:
// 1. 打开DevTools → Performance面板
// 2. 点击Record按钮
// 3. 在页面执行操作(如点击"启动摄像头")
// 4. 点击Stop,分析性能火焰图
//
// 关键指标:
// - Long Tasks: >50ms的任务(阻塞主线程)
// - Layout Shifts: 意外布局偏移
// - Forced Reflows: 强制同步布局
3.2 内存泄漏检测与预防
// 内存泄漏检测代码
class MemoryLeakDetector {
    constructor() {
        this.snapshotCount = 0;
        this.snapshots = [];
        
        if (window.performance && performance.memory) {
            this.monitorMemory();
        }
    }
    
    monitorMemory() {
        setInterval(() => {
            const memory = performance.memory;
            const usedJSHeapSize = memory.usedJSHeapSize / 1024 / 1024;
            const totalJSHeapSize = memory.totalJSHeapSize / 1024 / 1024;
            
            console.log(`[内存] 已使用: ${usedJSHeapSize.toFixed(2)}MB / 总堆: ${totalJSHeapSize.toFixed(2)}MB`);
            
            // 检测内存泄漏模式
            if (this.snapshots.length >= 2) {
                const prev = this.snapshots[this.snapshots.length - 2];
                const curr = this.snapshots[this.snapshots.length - 1];
                
                if (curr.used > prev.used * 1.5) {
                    console.warn('[内存泄漏警告] 内存使用量快速增加!');
                    this.takeHeapSnapshot();
                }
            }
            
            this.snapshots.push({
                timestamp: Date.now(),
                used: usedJSHeapSize,
                total: totalJSHeapSize
            });
            
            // 保留最近10个快照
            if (this.snapshots.length > 10) {
                this.snapshots.shift();
            }
        }, 5000);
    }
    
    takeHeapSnapshot() {
        // Chrome DevTools协议,需要--remote-debugging-port=9222
        fetch('http://localhost:9222/json/version')
            .then(response => response.json())
            .then(data => {
                console.log('准备堆快照...');
                // 实际实现需要更复杂的Chrome DevTools协议通信
            });
    }
}
​
// 在CameraMonitorApp中集成
class CameraMonitorApp {
    constructor() {
        // ...原有代码
        this.memoryDetector = new MemoryLeakDetector();
        
        // 常见内存泄漏点修复
        this.fixMemoryLeaks();
    }
    
    fixMemoryLeaks() {
        // 1. 定时器清理
        window.addEventListener('beforeunload', () => {
            if (this.uptimeInterval) clearInterval(this.uptimeInterval);
            if (this.performance.monitorInterval) clearInterval(this.performance.monitorInterval);
        });
        
        // 2. 事件监听器清理
        this.cleanupEventListeners = () => {
            // 记录所有添加的事件监听器,在destroy时移除
            this.eventListeners.forEach(({element, type, handler}) => {
                element.removeEventListener(type, handler);
            });
            this.eventListeners = [];
        };
        
        // 3. 闭包引用断开
        this.videoElement = null;
        this.mediaStream = null;
        this.screenshotManager = null;
    }
}

4. 安全与隐私硬核分析

4.1 摄像头权限安全模型
浏览器摄像头权限安全架构:
┌─ 权限请求流程(getUserMedia调用时)
│  ├── 1. 安全检查链:
│  │   ├── 来源安全: HTTPS或localhost
│  │   ├── 页面可见性: document.hidden === false
│  │   ├── 用户手势: 必须由点击等手势触发
│  │   └── 权限状态: 检查navigator.permissions API
│  │
│  ├── 2. 权限提示显示:
│  │   ├── 浏览器UI: 显示摄像头选择器
│  │   ├── 设备标签: 显示设备名称(如"Integrated Camera")
│  │   └── 持久化选项: "记住此选择"
│  │
│  └── 3. 系统级权限检查:
│      ├── Linux: 检查/dev/video0权限 (video用户组)
│      ├── udev规则: 可能限制特定设备
│      └── SELinux/AppArmor: 额外的强制访问控制
│
├─ 隐私指示器(现代浏览器)
│  ├── 地址栏图标: 摄像头/麦克风使用中
│  ├── 操作系统提示: 某些系统有全局指示器
│  └── 标签页标题: 可能显示录制状态
│
└─ 权限滥用防护
   ├── 隐形iframe检测: 阻止隐藏的摄像头访问
   ├── 自动暂停: 页面隐藏时自动暂停视频流
   ├── 权限超时: 长时间未使用自动撤销
   └── 用户控制: 可随时在浏览器设置中撤销权限

总结:从调用树到架构的全面掌控

  1. 精准定位性能瓶颈:知道每次点击按钮后发生了什么,哪一步最耗时

  2. 优化初始化速度:避免localStorage阻塞,异步化设备枚举

  3. 选择合适的本地服务器:理解不同方案的优劣和适用场景

  4. 预防内存泄漏:知道在哪里可能泄漏,如何检测和修复

  5. 实现高级特性:PWA离线、WebSocket实时传输等

最关键的一点:代码已经具备了良好的架构基础,但需要:

  • 添加适当的错误边界和降级处理

  • 实现更细粒度的性能监控

  • 考虑生产环境的部署架构

  • 添加自动化测试确保稳定性

从前端按钮点击到内核UVC驱动的完整调用链认知,可以精准优化任何环节的性能问题。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐