();
+ if (body != null)
+ {
+ var newQuality = body.Quality?.ToLower() == "high"
+ ? StreamQualityProfile.High
+ : StreamQualityProfile.Low;
+
+ if (SetQuality(newQuality))
+ {
+ await context.Response.WriteAsJsonAsync(new { success = true, quality = newQuality.Level.ToString() });
+ }
+ else
+ {
+ context.Response.StatusCode = 500;
+ await context.Response.WriteAsJsonAsync(new { success = false, error = "切换质量失败" });
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "处理质量切换请求失败");
+ context.Response.StatusCode = 500;
+ await context.Response.WriteAsJsonAsync(new { success = false, error = ex.Message });
+ }
+ }
+ else
+ {
+ await context.Response.WriteAsJsonAsync(new { quality = _currentQuality.Level.ToString() });
+ }
+ });
+
_isRunning = true;
+ _logger.LogInformation("屏幕流服务已启动,端口: {Port}, 模式: {Mode}",
+ _config.ScreenStreamPort, _useH264 ? "H.264" : "JPEG");
- _logger.LogInformation("屏幕流 WebSocket 服务已启动,端口: {Port}", _config.ScreenStreamPort);
-
- // 启动接受连接的任务
- _ = AcceptConnectionsAsync(_cts.Token);
-
- // 启动屏幕推送任务
_streamTask = StreamScreenAsync(_cts.Token);
- }
- catch (HttpListenerException ex) when (ex.ErrorCode == 5)
- {
- _logger.LogError("启动 WebSocket 服务失败: 需要管理员权限或运行 netsh 命令添加 URL 保留");
- _logger.LogError("请以管理员身份运行: netsh http add urlacl url=http://+:{Port}/ user=Everyone", _config.ScreenStreamPort);
+ await _app.RunAsync(_cts.Token);
}
catch (Exception ex)
{
@@ -79,216 +171,150 @@ public class ScreenStreamService : IDisposable
}
}
- ///
- /// 接受 WebSocket 连接
- ///
- private async Task AcceptConnectionsAsync(CancellationToken cancellationToken)
+ private async Task HandleWebSocketAsync(WebSocket webSocket, CancellationToken ct)
{
- while (!cancellationToken.IsCancellationRequested && _isRunning)
- {
- try
- {
- var context = await _httpListener!.GetContextAsync();
-
- if (context.Request.IsWebSocketRequest)
- {
- _ = HandleWebSocketAsync(context, cancellationToken);
- }
- else
- {
- // 返回简单的状态页面
- context.Response.StatusCode = 200;
- context.Response.ContentType = "text/html";
- var html = $"Screen Stream Service
Clients: {_clients.Count}
";
- var buffer = Encoding.UTF8.GetBytes(html);
- await context.Response.OutputStream.WriteAsync(buffer, cancellationToken);
- context.Response.Close();
- }
- }
- catch (ObjectDisposedException)
- {
- break;
- }
- catch (Exception ex)
- {
- if (!cancellationToken.IsCancellationRequested)
- {
- _logger.LogError(ex, "接受连接时发生错误");
- }
- }
- }
- }
-
- ///
- /// 处理 WebSocket 连接
- ///
- private async Task HandleWebSocketAsync(HttpListenerContext context, CancellationToken cancellationToken)
- {
- WebSocket? webSocket = null;
try
{
- var wsContext = await context.AcceptWebSocketAsync(null);
- webSocket = wsContext.WebSocket;
+ lock (_clientsLock) { _clients.Add(webSocket); }
+ _logger.LogInformation("客户端连接,当前: {Count}, 模式: {Mode}",
+ _clients.Count, _useH264 ? "H.264" : "JPEG");
- lock (_clientsLock)
- {
- _clients.Add(webSocket);
- }
+ // 发送初始化消息告知客户端编码模式
+ var initMsg = System.Text.Encoding.UTF8.GetBytes(
+ System.Text.Json.JsonSerializer.Serialize(new
+ {
+ type = "init",
+ mode = _useH264 ? "h264" : "jpeg",
+ width = _currentQuality.Width,
+ height = _currentQuality.Height,
+ fps = _currentQuality.Fps,
+ quality = _currentQuality.Level.ToString()
+ }));
+ await webSocket.SendAsync(new ArraySegment(initMsg),
+ WebSocketMessageType.Text, true, ct);
- _logger.LogInformation("新的屏幕流客户端连接,当前客户端数: {Count}", _clients.Count);
-
- // 保持连接,等待客户端断开
var buffer = new byte[1024];
- while (webSocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
+ while (webSocket.State == WebSocketState.Open && !ct.IsCancellationRequested)
{
try
{
- var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), cancellationToken);
- if (result.MessageType == WebSocketMessageType.Close)
- {
- break;
- }
- }
- catch
- {
- break;
+ var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), ct);
+ if (result.MessageType == WebSocketMessageType.Close) break;
}
+ catch { break; }
}
}
- catch (Exception ex)
- {
- _logger.LogError(ex, "处理 WebSocket 连接时发生错误");
- }
finally
{
- if (webSocket != null)
- {
- lock (_clientsLock)
- {
- _clients.Remove(webSocket);
- }
- _logger.LogInformation("屏幕流客户端断开,当前客户端数: {Count}", _clients.Count);
-
- try
- {
- if (webSocket.State == WebSocketState.Open)
- {
- await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closed", CancellationToken.None);
- }
- webSocket.Dispose();
- }
- catch { }
- }
+ lock (_clientsLock) { _clients.Remove(webSocket); }
+ _logger.LogInformation("客户端断开,当前: {Count}", _clients.Count);
+ try { webSocket.Dispose(); } catch { }
}
}
- ///
- /// 持续推送屏幕画面
- ///
- private async Task StreamScreenAsync(CancellationToken cancellationToken)
+ private async Task StreamScreenAsync(CancellationToken ct)
{
- var frameInterval = TimeSpan.FromMilliseconds(1000.0 / _config.ScreenStreamFps);
-
- while (!cancellationToken.IsCancellationRequested && _isRunning)
+ while (!ct.IsCancellationRequested && _isRunning)
{
try
{
- List clientsCopy;
- lock (_clientsLock)
- {
- clientsCopy = _clients.ToList();
- }
+ List clients;
+ lock (_clientsLock) { clients = _clients.ToList(); }
- if (clientsCopy.Count > 0)
+ // 按需推流:只在有客户端连接时才采集编码
+ if (clients.Count > 0)
{
- // 截取屏幕
- var screenshot = _screenCaptureService.CaptureScreen(
- _config.ScreenStreamQuality,
- _config.ScreenStreamMaxWidth);
+ byte[]? frameData;
- if (screenshot.Length > 0)
+ if (_useH264)
{
- // 发送给所有客户端
- var sendTasks = clientsCopy
- .Where(ws => ws.State == WebSocketState.Open)
- .Select(ws => SendFrameAsync(ws, screenshot, cancellationToken));
+ // 使用 H.264 编码
+ frameData = _h264CaptureService.CaptureFrame();
+ }
+ else
+ {
+ // 回退到 JPEG
+ frameData = _screenCaptureService.CaptureScreen(
+ _config.ScreenStreamQuality, _currentQuality.Width);
+ }
- await Task.WhenAll(sendTasks);
+ 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(frameInterval, cancellationToken);
- }
- catch (OperationCanceledException)
- {
- break;
+
+ // 根据当前质量档位动态调整帧间隔
+ var interval = TimeSpan.FromMilliseconds(1000.0 / _currentQuality.Fps);
+ await Task.Delay(interval, ct);
}
+ catch (OperationCanceledException) { break; }
catch (Exception ex)
{
- _logger.LogError(ex, "推送屏幕画面时发生错误");
- await Task.Delay(1000, cancellationToken);
+ _logger.LogError(ex, "推送屏幕失败");
+ await Task.Delay(1000, ct);
}
}
}
-
+
///
- /// 发送一帧画面
+ /// 设置流质量(公开方法,供 SignalingClientService 调用)
///
- private async Task SendFrameAsync(WebSocket webSocket, byte[] frame, CancellationToken cancellationToken)
+ public bool SetQuality(StreamQualityProfile profile)
{
try
{
- await webSocket.SendAsync(
- new ArraySegment(frame),
- WebSocketMessageType.Binary,
- true,
- cancellationToken);
+ if (_currentQuality.Level == profile.Level)
+ {
+ return true; // 已是目标质量,无需切换
+ }
+
+ _logger.LogInformation("切换流质量: {OldQuality} -> {NewQuality}", _currentQuality, profile);
+ _currentQuality = profile;
+
+ if (_useH264)
+ {
+ return _h264CaptureService.SetQuality(profile);
+ }
+
+ return true;
}
catch (Exception ex)
{
- _logger.LogDebug(ex, "发送帧失败");
+ _logger.LogError(ex, "切换流质量失败");
+ return false;
}
}
- ///
- /// 停止服务
- ///
+ private async Task SendFrameAsync(WebSocket ws, byte[] frame, CancellationToken ct)
+ {
+ try
+ {
+ await ws.SendAsync(new ArraySegment(frame), WebSocketMessageType.Binary, true, ct);
+ }
+ catch { }
+ }
+
public async Task StopAsync()
{
_isRunning = false;
_cts?.Cancel();
- // 关闭所有客户端连接
- List clientsCopy;
- lock (_clientsLock)
+ List clients;
+ lock (_clientsLock) { clients = _clients.ToList(); _clients.Clear(); }
+
+ foreach (var ws in clients)
{
- clientsCopy = _clients.ToList();
- _clients.Clear();
+ try { ws.Dispose(); } catch { }
}
- foreach (var ws in clientsCopy)
+ if (_app != null)
{
- try
- {
- if (ws.State == WebSocketState.Open)
- {
- await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Server shutting down", CancellationToken.None);
- }
- ws.Dispose();
- }
- catch { }
- }
-
- _httpListener?.Stop();
- _httpListener?.Close();
-
- if (_streamTask != null)
- {
- try
- {
- await _streamTask;
- }
- catch { }
+ await _app.StopAsync();
+ await _app.DisposeAsync();
}
_logger.LogInformation("屏幕流服务已停止");
@@ -297,6 +323,6 @@ public class ScreenStreamService : IDisposable
public void Dispose()
{
_cts?.Dispose();
- _httpListener?.Close();
+ _h264CaptureService?.Dispose();
}
}
diff --git a/device-agent/Services/SignalingClientService.cs b/device-agent/Services/SignalingClientService.cs
new file mode 100644
index 0000000..ddae84b
--- /dev/null
+++ b/device-agent/Services/SignalingClientService.cs
@@ -0,0 +1,228 @@
+using Microsoft.AspNetCore.SignalR.Client;
+using Microsoft.Extensions.Options;
+using DeviceAgent.Models;
+
+namespace DeviceAgent.Services;
+
+///
+/// SignalR 信令客户端 - 连接到服务器接收质量控制指令
+///
+public class SignalingClientService : IDisposable
+{
+ private readonly ILogger _logger;
+ private readonly AgentConfig _config;
+ private readonly DeviceInfoService _deviceInfoService;
+ private ScreenStreamService? _screenStreamService;
+ private HubConnection? _connection;
+ private bool _isConnected;
+ private CancellationTokenSource? _reconnectCts;
+
+ public SignalingClientService(
+ ILogger logger,
+ IOptions config,
+ DeviceInfoService deviceInfoService)
+ {
+ _logger = logger;
+ _config = config.Value;
+ _deviceInfoService = deviceInfoService;
+ }
+
+ ///
+ /// 设置 ScreenStreamService 引用(避免循环依赖)
+ ///
+ public void SetScreenStreamService(ScreenStreamService screenStreamService)
+ {
+ _screenStreamService = screenStreamService;
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ if (!_config.ScreenStreamEnabled)
+ {
+ _logger.LogInformation("屏幕流已禁用,跳过信令连接");
+ return;
+ }
+
+ try
+ {
+ var hubUrl = $"{_config.ServerUrl}/hubs/stream-signaling";
+ _logger.LogInformation("连接到信令服务器: {HubUrl}", hubUrl);
+
+ _connection = new HubConnectionBuilder()
+ .WithUrl(hubUrl)
+ .WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10) })
+ .Build();
+
+ // 注册事件处理器
+ RegisterHandlers();
+
+ // 连接事件
+ _connection.Reconnecting += error =>
+ {
+ _logger.LogWarning("信令连接断开,正在重连...");
+ _isConnected = false;
+ return Task.CompletedTask;
+ };
+
+ _connection.Reconnected += async connectionId =>
+ {
+ _logger.LogInformation("信令连接已恢复: {ConnectionId}", connectionId);
+ _isConnected = true;
+ await RegisterDeviceAsync();
+ };
+
+ _connection.Closed += async error =>
+ {
+ _logger.LogWarning("信令连接关闭: {Error}", error?.Message);
+ _isConnected = false;
+
+ // 自动重连
+ await Task.Delay(5000, cancellationToken);
+ if (!cancellationToken.IsCancellationRequested)
+ {
+ await StartAsync(cancellationToken);
+ }
+ };
+
+ // 启动连接
+ await _connection.StartAsync(cancellationToken);
+ _isConnected = true;
+ _logger.LogInformation("信令连接已建立");
+
+ // 注册设备
+ await RegisterDeviceAsync();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "启动信令客户端失败");
+ }
+ }
+
+ private void RegisterHandlers()
+ {
+ if (_connection == null) return;
+
+ // 服务器通知切换质量
+ _connection.On("SetQuality", async (quality) =>
+ {
+ _logger.LogInformation("收到质量切换指令: {Quality}", quality);
+
+ if (_screenStreamService != null)
+ {
+ var profile = quality.ToLower() == "high"
+ ? StreamQualityProfile.High
+ : StreamQualityProfile.Low;
+
+ _screenStreamService.SetQuality(profile);
+ }
+ });
+
+ // 服务器通知开始推流
+ _connection.On("StartStreaming", async (quality) =>
+ {
+ _logger.LogInformation("收到开始推流指令: {Quality}", quality);
+
+ if (_screenStreamService != null)
+ {
+ var profile = quality.ToLower() == "high"
+ ? StreamQualityProfile.High
+ : StreamQualityProfile.Low;
+
+ _screenStreamService.SetQuality(profile);
+ }
+ // 注意:ScreenStreamService 已经在运行,这里只是切换质量
+ });
+
+ // 服务器通知停止推流
+ _connection.On("StopStreaming", async () =>
+ {
+ _logger.LogInformation("收到停止推流指令");
+ // 切换到低质量,实际推流由客户端连接数控制
+ if (_screenStreamService != null)
+ {
+ _screenStreamService.SetQuality(StreamQualityProfile.Low);
+ }
+ });
+
+ // 批量设备质量控制
+ _connection.On, string>("DevicesNeedStream", async (deviceUuids, quality) =>
+ {
+ var myUuid = _deviceInfoService.GetDeviceInfo().Uuid;
+ if (deviceUuids.Contains(myUuid) && _screenStreamService != null)
+ {
+ _logger.LogInformation("设备在监控列表中,质量: {Quality}", quality);
+ var profile = quality.ToLower() == "high"
+ ? StreamQualityProfile.High
+ : StreamQualityProfile.Low;
+ _screenStreamService.SetQuality(profile);
+ }
+ });
+
+ _connection.On>("DevicesStopStream", async (deviceUuids) =>
+ {
+ var myUuid = _deviceInfoService.GetDeviceInfo().Uuid;
+ if (deviceUuids.Contains(myUuid) && _screenStreamService != null)
+ {
+ _logger.LogInformation("设备停止监控");
+ _screenStreamService.SetQuality(StreamQualityProfile.Low);
+ }
+ });
+
+ _connection.On("DeviceQualityChange", async (deviceUuid, quality) =>
+ {
+ var myUuid = _deviceInfoService.GetDeviceInfo().Uuid;
+ if (deviceUuid == myUuid && _screenStreamService != null)
+ {
+ _logger.LogInformation("设备质量切换: {Quality}", quality);
+ var profile = quality.ToLower() == "high"
+ ? StreamQualityProfile.High
+ : StreamQualityProfile.Low;
+ _screenStreamService.SetQuality(profile);
+ }
+ });
+ }
+
+ private async Task RegisterDeviceAsync()
+ {
+ if (_connection == null || !_isConnected) return;
+
+ try
+ {
+ var uuid = _deviceInfoService.GetDeviceInfo().Uuid;
+ await _connection.InvokeAsync("RegisterDevice", uuid);
+ _logger.LogInformation("设备已注册到信令服务器: {Uuid}", uuid);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "注册设备失败");
+ }
+ }
+
+ public async Task StopAsync()
+ {
+ if (_connection != null)
+ {
+ try
+ {
+ var uuid = _deviceInfoService.GetDeviceInfo().Uuid;
+ await _connection.InvokeAsync("UnregisterDevice", uuid);
+ _logger.LogInformation("设备已从信令服务器注销");
+ }
+ catch { }
+
+ await _connection.StopAsync();
+ await _connection.DisposeAsync();
+ _connection = null;
+ }
+
+ _isConnected = false;
+ _logger.LogInformation("信令客户端已停止");
+ }
+
+ public void Dispose()
+ {
+ _reconnectCts?.Cancel();
+ _reconnectCts?.Dispose();
+ _connection?.DisposeAsync().AsTask().Wait();
+ }
+}
diff --git a/device-agent/Worker.cs b/device-agent/Worker.cs
index de220d1..17b2b9a 100644
--- a/device-agent/Worker.cs
+++ b/device-agent/Worker.cs
@@ -11,6 +11,7 @@ public class Worker : BackgroundService
private readonly ScreenCaptureService _screenCaptureService;
private readonly ScreenStreamService _screenStreamService;
private readonly RemoteDesktopService _remoteDesktopService;
+ private readonly SignalingClientService _signalingClientService;
private readonly AgentConfig _config;
private string? _cachedUuid;
@@ -24,6 +25,7 @@ public class Worker : BackgroundService
ScreenCaptureService screenCaptureService,
ScreenStreamService screenStreamService,
RemoteDesktopService remoteDesktopService,
+ SignalingClientService signalingClientService,
IOptions config)
{
_logger = logger;
@@ -32,6 +34,7 @@ public class Worker : BackgroundService
_screenCaptureService = screenCaptureService;
_screenStreamService = screenStreamService;
_remoteDesktopService = remoteDesktopService;
+ _signalingClientService = signalingClientService;
_config = config.Value;
}
@@ -50,11 +53,16 @@ public class Worker : BackgroundService
EnableRemoteDesktopOnStartup();
}
+ // 设置 SignalingClientService 的 ScreenStreamService 引用(避免循环依赖)
+ _signalingClientService.SetScreenStreamService(_screenStreamService);
+
// 启动实时屏幕流服务(在后台任务中)
Task? screenStreamTask = null;
+ Task? signalingTask = null;
if (_config.ScreenStreamEnabled)
{
screenStreamTask = Task.Run(() => _screenStreamService.StartAsync(stoppingToken), stoppingToken);
+ signalingTask = Task.Run(() => _signalingClientService.StartAsync(stoppingToken), stoppingToken);
// 等待一小段时间让服务启动
await Task.Delay(500, stoppingToken);
}
@@ -108,6 +116,7 @@ public class Worker : BackgroundService
if (_config.ScreenStreamEnabled)
{
await _screenStreamService.StopAsync();
+ await _signalingClientService.StopAsync();
}
_logger.LogInformation("DeviceAgent 服务已停止");
diff --git a/device-agent/appsettings.json b/device-agent/appsettings.json
index 7d97379..97b6609 100644
--- a/device-agent/appsettings.json
+++ b/device-agent/appsettings.json
@@ -15,9 +15,11 @@
"ScreenCaptureMaxWidth": 800,
"ScreenStreamEnabled": true,
"ScreenStreamPort": 9100,
- "ScreenStreamFps": 10,
+ "ScreenStreamFps": 3,
"ScreenStreamQuality": 60,
- "ScreenStreamMaxWidth": 1280,
+ "ScreenStreamMaxWidth": 320,
+ "UseH264Encoding": true,
+ "H264Bitrate": 100000,
"EnableRemoteDesktopOnStart": true
}
}
diff --git a/test-h264-stream.html b/test-h264-stream.html
new file mode 100644
index 0000000..75573c7
--- /dev/null
+++ b/test-h264-stream.html
@@ -0,0 +1,290 @@
+
+
+
+
+ H.264 视频流测试
+
+
+
+
+
H.264 视频流测试
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+