303 lines
9.3 KiB
C#
303 lines
9.3 KiB
C#
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();
|
||
}
|
||
}
|