serverRoom/device-agent/Services/ScreenStreamService.cs

303 lines
9.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Net;
using System.Net.WebSockets;
using System.Text;
using Microsoft.Extensions.Options;
namespace DeviceAgent.Services;
/// <summary>
/// 屏幕流服务 - 通过 WebSocket 实时推送屏幕画面
/// </summary>
public class ScreenStreamService : IDisposable
{
private readonly ILogger<ScreenStreamService> _logger;
private readonly ScreenCaptureService _screenCaptureService;
private readonly AgentConfig _config;
private HttpListener? _httpListener;
private readonly List<WebSocket> _clients = new();
private readonly object _clientsLock = new();
private CancellationTokenSource? _cts;
private Task? _streamTask;
private bool _isRunning;
public ScreenStreamService(
ILogger<ScreenStreamService> logger,
ScreenCaptureService screenCaptureService,
IOptions<AgentConfig> config)
{
_logger = logger;
_screenCaptureService = screenCaptureService;
_config = config.Value;
}
/// <summary>
/// 启动 WebSocket 服务器
/// </summary>
public async Task StartAsync(CancellationToken cancellationToken)
{
if (!_config.ScreenStreamEnabled)
{
_logger.LogInformation("屏幕流服务已禁用");
return;
}
try
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_httpListener = new HttpListener();
// 尝试使用 localhost不需要管理员权限
_httpListener.Prefixes.Add($"http://localhost:{_config.ScreenStreamPort}/");
_httpListener.Prefixes.Add($"http://127.0.0.1:{_config.ScreenStreamPort}/");
// 尝试添加通配符(需要管理员权限)
try
{
_httpListener.Prefixes.Add($"http://*:{_config.ScreenStreamPort}/");
}
catch { }
_httpListener.Start();
_isRunning = true;
_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);
}
catch (Exception ex)
{
_logger.LogError(ex, "启动屏幕流服务失败");
}
}
/// <summary>
/// 接受 WebSocket 连接
/// </summary>
private async Task AcceptConnectionsAsync(CancellationToken cancellationToken)
{
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 = $"<html><body><h1>Screen Stream Service</h1><p>Clients: {_clients.Count}</p></body></html>";
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, "接受连接时发生错误");
}
}
}
}
/// <summary>
/// 处理 WebSocket 连接
/// </summary>
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}", _clients.Count);
// 保持连接,等待客户端断开
var buffer = new byte[1024];
while (webSocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
{
try
{
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken);
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 { }
}
}
}
/// <summary>
/// 持续推送屏幕画面
/// </summary>
private async Task StreamScreenAsync(CancellationToken cancellationToken)
{
var frameInterval = TimeSpan.FromMilliseconds(1000.0 / _config.ScreenStreamFps);
while (!cancellationToken.IsCancellationRequested && _isRunning)
{
try
{
List<WebSocket> clientsCopy;
lock (_clientsLock)
{
clientsCopy = _clients.ToList();
}
if (clientsCopy.Count > 0)
{
// 截取屏幕
var screenshot = _screenCaptureService.CaptureScreen(
_config.ScreenStreamQuality,
_config.ScreenStreamMaxWidth);
if (screenshot.Length > 0)
{
// 发送给所有客户端
var sendTasks = clientsCopy
.Where(ws => ws.State == WebSocketState.Open)
.Select(ws => SendFrameAsync(ws, screenshot, cancellationToken));
await Task.WhenAll(sendTasks);
}
}
await Task.Delay(frameInterval, cancellationToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "推送屏幕画面时发生错误");
await Task.Delay(1000, cancellationToken);
}
}
}
/// <summary>
/// 发送一帧画面
/// </summary>
private async Task SendFrameAsync(WebSocket webSocket, byte[] frame, CancellationToken cancellationToken)
{
try
{
await webSocket.SendAsync(
new ArraySegment<byte>(frame),
WebSocketMessageType.Binary,
true,
cancellationToken);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "发送帧失败");
}
}
/// <summary>
/// 停止服务
/// </summary>
public async Task StopAsync()
{
_isRunning = false;
_cts?.Cancel();
// 关闭所有客户端连接
List<WebSocket> clientsCopy;
lock (_clientsLock)
{
clientsCopy = _clients.ToList();
_clients.Clear();
}
foreach (var ws in clientsCopy)
{
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 { }
}
_logger.LogInformation("屏幕流服务已停止");
}
public void Dispose()
{
_cts?.Dispose();
_httpListener?.Close();
}
}