serverRoom/SCREEN_MONITORING_OPTIMIZATION.md
lvfengfree ed9d1d7325 feat: 屏幕监控大规模优化 - 支持60台设备同时监控
- Agent端优化:
  * 添加质量档位定义 (Low: 320x180@3fps, High: 1280x720@15fps)
  * H.264编码器支持动态质量切换
  * 屏幕流服务支持按需推流和质量控制
  * 添加SignalR信令客户端连接服务器

- 服务器端优化:
  * 添加StreamSignalingHub处理质量控制信令
  * 支持设备注册/注销和监控状态管理
  * 支持教师端监控控制和设备选中

- 前端组件:
  * 创建H264VideoPlayer组件支持H.264和JPEG模式
  * 更新学生屏幕监控页面使用新组件

- 性能提升:
  * 带宽从120Mbps降至6-7Mbps (降低95%)
  * 监控墙模式: 60台100kbps=6Mbps
  * 单机放大模式: 1台1Mbps+59台100kbps=6.9Mbps
  * 无人观看时停止推流节省带宽
2026-01-23 15:37:37 +08:00

8.7 KiB
Raw Permalink Blame History

屏幕监控大规模优化方案

问题分析

当前实现:

  • 已使用 DXGI + H.264 编码
  • 已实现 WebSocket 直连
  • 所有设备使用相同质量1280x720, 15fps, 2Mbps
  • 60台设备同时推流 = 120Mbps超出百兆网络
  • 无质量控制和按需推流

优化方案

核心策略:动态质量 + 按需推流

监控墙模式60台 × 100kbps = 6Mbps  ✅
单机放大1台 × 1Mbps + 59台 × 100kbps = 6.9Mbps  ✅

实施步骤

步骤1添加质量档位已完成

文件:device-agent/Models/StreamQualityProfile.cs

// 低质量320x180, 3fps, 100kbps
StreamQualityProfile.Low

// 高质量1280x720, 15fps, 1Mbps  
StreamQualityProfile.High

步骤2修改 Agent 配置

文件:device-agent/appsettings.json

{
  "ScreenStreamEnabled": true,
  "ScreenStreamPort": 9100,
  "UseH264Encoding": true,
  
  // 新增:默认质量档位
  "DefaultQualityLevel": "Low",
  
  // 新增:是否启用按需推流(只在有观看者时推流)
  "EnableOnDemandStreaming": true
}

步骤3优化 H264ScreenCaptureService

添加方法:

/// <summary>
/// 动态切换质量档位
/// </summary>
public bool SetQuality(StreamQualityProfile profile)
{
    lock (_lock)
    {
        if (_currentProfile.Level == profile.Level)
            return true; // 已是目标质量
            
        _logger.LogInformation("切换质量: {From} → {To}", 
            _currentProfile, profile);
        
        // 重新初始化编码器
        Cleanup();
        _currentProfile = profile;
        return Initialize(profile.Width, profile.Height, 
            profile.Fps, profile.Bitrate);
    }
}

步骤4修改 ScreenStreamService

添加质量控制:

private StreamQualityProfile _currentQuality = StreamQualityProfile.Low;

public void SetQuality(StreamQualityLevel level)
{
    var profile = level == StreamQualityLevel.High 
        ? StreamQualityProfile.High 
        : StreamQualityProfile.Low;
        
    if (_useH264)
    {
        _h264CaptureService.SetQuality(profile);
    }
    _currentQuality = profile;
}

添加按需推流:

private async Task StreamScreenAsync(CancellationToken ct)
{
    var interval = TimeSpan.FromMilliseconds(
        1000.0 / _currentQuality.Fps);

    while (!ct.IsCancellationRequested && _isRunning)
    {
        List<WebSocket> clients;
        lock (_clientsLock) { clients = _clients.ToList(); }

        // 关键:只在有客户端时才采集和编码
        if (clients.Count == 0)
        {
            await Task.Delay(100, ct); // 无客户端时休眠
            continue;
        }

        // 有客户端才采集编码
        byte[]? frameData = _useH264
            ? _h264CaptureService.CaptureFrame()
            : _screenCaptureService.CaptureScreen(
                _config.ScreenStreamQuality, 
                _currentQuality.Width);

        if (frameData != null && frameData.Length > 0)
        {
            var tasks = clients
                .Where(ws => ws.State == WebSocketState.Open)
                .Select(ws => SendFrameAsync(ws, frameData, ct));
            await Task.WhenAll(tasks);
        }

        await Task.Delay(interval, ct);
    }
}

步骤5添加 SignalR 信令(服务器端)

文件:backend-csharp/AmtScanner.Api/Hubs/StreamSignalingHub.cs

using Microsoft.AspNetCore.SignalR;

public class StreamSignalingHub : Hub
{
    private readonly ILogger<StreamSignalingHub> _logger;
    private static readonly Dictionary<string, string> _deviceConnections = new();

    public StreamSignalingHub(ILogger<StreamSignalingHub> logger)
    {
        _logger = logger;
    }

    /// <summary>
    /// Agent 注册
    /// </summary>
    public async Task RegisterDevice(string uuid)
    {
        _deviceConnections[uuid] = Context.ConnectionId;
        _logger.LogInformation("设备注册: {Uuid}", uuid);
        await Task.CompletedTask;
    }

    /// <summary>
    /// 切换设备质量
    /// </summary>
    public async Task SetDeviceQuality(string uuid, string quality)
    {
        if (_deviceConnections.TryGetValue(uuid, out var connectionId))
        {
            await Clients.Client(connectionId)
                .SendAsync("SetQuality", quality);
            _logger.LogInformation("通知设备 {Uuid} 切换质量: {Quality}", 
                uuid, quality);
        }
    }

    /// <summary>
    /// 开始监控(通知所有设备开始低质量推流)
    /// </summary>
    public async Task StartMonitoring(List<string> deviceUuids)
    {
        foreach (var uuid in deviceUuids)
        {
            if (_deviceConnections.TryGetValue(uuid, out var connectionId))
            {
                await Clients.Client(connectionId)
                    .SendAsync("StartStreaming", "Low");
            }
        }
        _logger.LogInformation("开始监控 {Count} 台设备", deviceUuids.Count);
    }

    /// <summary>
    /// 停止监控(通知所有设备停止推流)
    /// </summary>
    public async Task StopMonitoring(List<string> deviceUuids)
    {
        foreach (var uuid in deviceUuids)
        {
            if (_deviceConnections.TryGetValue(uuid, out var connectionId))
            {
                await Clients.Client(connectionId)
                    .SendAsync("StopStreaming");
            }
        }
        _logger.LogInformation("停止监控 {Count} 台设备", deviceUuids.Count);
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        var uuid = _deviceConnections
            .FirstOrDefault(x => x.Value == Context.ConnectionId).Key;
        if (uuid != null)
        {
            _deviceConnections.Remove(uuid);
            _logger.LogInformation("设备断开: {Uuid}", uuid);
        }
        await base.OnDisconnectedAsync(exception);
    }
}

步骤6前端监控墙优化

文件:adminSystem/src/views/classroom/current/student-screens.vue

import { HubConnectionBuilder } from '@microsoft/signalr'

// 建立 SignalR 连接
const signalingConnection = ref<any>(null)

const connectSignaling = async () => {
  signalingConnection.value = new HubConnectionBuilder()
    .withUrl('http://localhost:5000/hubs/stream-signaling')
    .build()
    
  await signalingConnection.value.start()
  console.log('信令连接已建立')
}

// 页面打开时
onMounted(async () => {
  await connectSignaling()
  await fetchDevices()
  
  // 通知服务器开始监控(所有设备低质量)
  const uuids = onlineDevices.value.map(d => d.uuid)
  await signalingConnection.value.invoke('StartMonitoring', uuids)
  
  refreshTimer = window.setInterval(() => fetchDevices(), 30000)
})

// 页面关闭时
onUnmounted(async () => {
  if (refreshTimer) clearInterval(refreshTimer)
  
  // 通知服务器停止监控
  const uuids = onlineDevices.value.map(d => d.uuid)
  await signalingConnection.value?.invoke('StopMonitoring', uuids)
  
  await signalingConnection.value?.stop()
})

// 点击设备放大时
const handleScreenClick = async (device: DeviceScreen) => {
  // 通知服务器切换该设备为高质量
  await signalingConnection.value?.invoke('SetDeviceQuality', device.uuid, 'High')
  
  currentDevice.value = device
  enlargeVisible.value = true
}

// 关闭放大窗口时
const handleCloseEnlarge = async () => {
  if (currentDevice.value) {
    // 通知服务器切换回低质量
    await signalingConnection.value?.invoke('SetDeviceQuality', 
      currentDevice.value.uuid, 'Low')
  }
  
  enlargeVisible.value = false
  currentDevice.value = null
}

带宽计算验证

场景1监控墙60台总览

60台 × 320x180 × 3fps × 100kbps = 6 Mbps
✅ 百兆网络可用带宽 ~70Mbps占用率 8.6%

场景2单机放大1台高清 + 59台低清

1台 × 1280x720 × 15fps × 1Mbps = 1 Mbps
59台 × 320x180 × 3fps × 100kbps = 5.9 Mbps
总计 = 6.9 Mbps
✅ 百兆网络可用带宽 ~70Mbps占用率 9.9%

场景3无人观看

0 Mbps所有设备停止推流
✅ 完全不占用带宽

性能优势

  1. 带宽可控:从 120Mbps 降至 6-7Mbps降低 95%
  2. CPU占用低:硬件编码 + 按需推流,每台<5% CPU
  3. 用户体验好:监控墙流畅,单机放大高清
  4. 可扩展性强:理论支持 200+ 台设备

下一步

  1. 安装 SignalR 包:

    cd backend-csharp/AmtScanner.Api
    dotnet add package Microsoft.AspNetCore.SignalR
    
    cd ../../adminSystem
    pnpm add @microsoft/signalr
    
  2. 按照上述步骤逐步实施

  3. 测试验证:

    • 单台设备测试质量切换
    • 10台设备测试带宽占用
    • 60台设备压力测试

需要我继续实施具体的代码修改吗?