From ab4144e833cda56e27573becac60a406c348e49a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?0=25=20=5B=E2=96=88=20=E2=96=88=20=E2=96=88=20=E2=96=88=20?= =?UTF-8?q?=E2=96=88=20=E2=96=88=20=E2=96=88=20=E2=96=88=20=E2=96=88=20?= =?UTF-8?q?=E2=96=88=5D=20100=25?= Date: Sat, 18 Jul 2026 23:05:42 -0500 Subject: [PATCH] First Commit --- DisplayStreamer.slnx | 9 + DisplayStreamer/ClientForm.Designer.cs | 114 ++ DisplayStreamer/ClientForm.cs | 170 ++ DisplayStreamer/ClientForm.resx | 120 ++ DisplayStreamer/DisplayStreamer.csproj | 20 + DisplayStreamer/Program.cs | 1981 ++++++++++++++++++++++++ DisplayStreamer/ServerForm.Designer.cs | 194 +++ DisplayStreamer/ServerForm.cs | 256 +++ DisplayStreamer/ServerForm.resx | 120 ++ DisplayStreamer/ViewerForm.Designer.cs | 84 + DisplayStreamer/ViewerForm.cs | 91 ++ DisplayStreamer/ViewerForm.resx | 123 ++ 12 files changed, 3282 insertions(+) create mode 100644 DisplayStreamer.slnx create mode 100644 DisplayStreamer/ClientForm.Designer.cs create mode 100644 DisplayStreamer/ClientForm.cs create mode 100644 DisplayStreamer/ClientForm.resx create mode 100644 DisplayStreamer/DisplayStreamer.csproj create mode 100644 DisplayStreamer/Program.cs create mode 100644 DisplayStreamer/ServerForm.Designer.cs create mode 100644 DisplayStreamer/ServerForm.cs create mode 100644 DisplayStreamer/ServerForm.resx create mode 100644 DisplayStreamer/ViewerForm.Designer.cs create mode 100644 DisplayStreamer/ViewerForm.cs create mode 100644 DisplayStreamer/ViewerForm.resx diff --git a/DisplayStreamer.slnx b/DisplayStreamer.slnx new file mode 100644 index 0000000..5e56d4b --- /dev/null +++ b/DisplayStreamer.slnx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/DisplayStreamer/ClientForm.Designer.cs b/DisplayStreamer/ClientForm.Designer.cs new file mode 100644 index 0000000..21415fd --- /dev/null +++ b/DisplayStreamer/ClientForm.Designer.cs @@ -0,0 +1,114 @@ +namespace DisplayStreamer +{ + partial class ClientForm + { + private System.ComponentModel.IContainer components = null; + private System.Windows.Forms.Label lblServers; + private System.Windows.Forms.ListBox lstServers; + private System.Windows.Forms.Button btnConnect; + private System.Windows.Forms.Button btnSwitchMode; + private System.Windows.Forms.CheckBox chkLogging; + private System.Windows.Forms.Timer updateTimer; + + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.lblServers = new System.Windows.Forms.Label(); + this.lstServers = new System.Windows.Forms.ListBox(); + this.btnConnect = new System.Windows.Forms.Button(); + this.btnSwitchMode = new System.Windows.Forms.Button(); + this.chkLogging = new System.Windows.Forms.CheckBox(); + this.updateTimer = new System.Windows.Forms.Timer(this.components); + this.SuspendLayout(); + // + // lblServers + // + this.lblServers.AutoSize = true; + this.lblServers.Location = new System.Drawing.Point(20, 20); + this.lblServers.Name = "lblServers"; + this.lblServers.Size = new System.Drawing.Size(182, 13); + this.lblServers.TabIndex = 0; + this.lblServers.Text = "Discovered Servers (Auto-updating):"; + // + // lstServers + // + this.lstServers.FormattingEnabled = true; + this.lstServers.Location = new System.Drawing.Point(20, 40); + this.lstServers.Name = "lstServers"; + this.lstServers.Size = new System.Drawing.Size(440, 160); + this.lstServers.TabIndex = 1; + this.lstServers.DoubleClick += new System.EventHandler(this.lstServers_DoubleClick); + this.lstServers.KeyDown += new System.Windows.Forms.KeyEventHandler(this.lstServers_KeyDown); + // + // chkLogging + // + this.chkLogging.AutoSize = true; + this.chkLogging.Location = new System.Drawing.Point(20, 210); + this.chkLogging.Name = "chkLogging"; + this.chkLogging.Size = new System.Drawing.Size(100, 17); + this.chkLogging.TabIndex = 4; + this.chkLogging.Text = "Enable Logging"; + this.chkLogging.UseVisualStyleBackColor = true; + this.chkLogging.CheckedChanged += new System.EventHandler(this.chkLogging_CheckedChanged); + // + // btnConnect + // + this.btnConnect.Font = new System.Drawing.Font("Arial", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.btnConnect.Location = new System.Drawing.Point(20, 240); + this.btnConnect.Name = "btnConnect"; + this.btnConnect.Size = new System.Drawing.Size(210, 40); + this.btnConnect.TabIndex = 2; + this.btnConnect.Text = "Connect"; + this.btnConnect.UseVisualStyleBackColor = true; + this.btnConnect.Click += new System.EventHandler(this.btnConnect_Click); + // + // btnSwitchMode + // + this.btnSwitchMode.Font = new System.Drawing.Font("Arial", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.btnSwitchMode.Location = new System.Drawing.Point(250, 240); + this.btnSwitchMode.Name = "btnSwitchMode"; + this.btnSwitchMode.Size = new System.Drawing.Size(210, 40); + this.btnSwitchMode.TabIndex = 3; + this.btnSwitchMode.Text = "Switch to Server Mode"; + this.btnSwitchMode.UseVisualStyleBackColor = true; + this.btnSwitchMode.Click += new System.EventHandler(this.btnSwitchMode_Click); + // + // updateTimer + // + this.updateTimer.Interval = 1000; + this.updateTimer.Tick += new System.EventHandler(this.updateTimer_Tick); + // + // ClientForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(484, 311); + this.Controls.Add(this.chkLogging); + this.Controls.Add(this.btnSwitchMode); + this.Controls.Add(this.btnConnect); + this.Controls.Add(this.lstServers); + this.Controls.Add(this.lblServers); + this.Name = "ClientForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Stream Client (Viewer)"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.ClientForm_FormClosing); + this.Load += new System.EventHandler(this.ClientForm_Load); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + } +} \ No newline at end of file diff --git a/DisplayStreamer/ClientForm.cs b/DisplayStreamer/ClientForm.cs new file mode 100644 index 0000000..a9a3f6d --- /dev/null +++ b/DisplayStreamer/ClientForm.cs @@ -0,0 +1,170 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Sockets; +using System.Windows.Forms; + +namespace DisplayStreamer +{ + public partial class ClientForm : Form + { + private UdpDiscoveryClient discoveryClient; + private string autoConnectIp; + private int autoConnectPort; + + public ClientForm(string ip = null, int port = 0) + { + InitializeComponent(); + autoConnectIp = ip; + autoConnectPort = port; + this.Shown += ClientForm_Shown; + } + + private void ClientForm_Load(object sender, EventArgs e) + { + chkLogging.Checked = Program.LoggingEnabled; + discoveryClient = new UdpDiscoveryClient(); + discoveryClient.Start(); + updateTimer.Start(); + } + + private void ClientForm_Shown(object sender, EventArgs e) + { + if (!string.IsNullOrEmpty(autoConnectIp)) + { + ViewerForm viewer = new ViewerForm(autoConnectIp, autoConnectPort); + this.Hide(); + viewer.ShowDialog(); + this.Show(); + autoConnectIp = null; // Clear it so it doesn't loop + } + } + + private void updateTimer_Tick(object sender, EventArgs e) + { + var servers = discoveryClient.GetDiscoveredServers(); + + string selectedString = lstServers.SelectedItem?.ToString(); + + lstServers.BeginUpdate(); + lstServers.Items.Clear(); + foreach (var srv in servers) + { + string item = $"{srv.MachineName} - {srv.IPAddress}:{srv.Port}"; + lstServers.Items.Add(item); + + if (item == selectedString) + { + lstServers.SelectedItem = item; + } + } + lstServers.EndUpdate(); + } + + private void btnConnect_Click(object sender, EventArgs e) + { + if (lstServers.SelectedIndex == -1) return; + string selectedString = lstServers.SelectedItem.ToString(); + + var servers = discoveryClient.GetDiscoveredServers(); + var selectedSrv = servers.FirstOrDefault(s => $"{s.MachineName} - {s.IPAddress}:{s.Port}" == selectedString); + + if (selectedSrv == null) return; + + // Auto-Update Check + if (selectedSrv.Hash != Program.AppHash) + { + DialogResult res = MessageBox.Show("The server is running a different version of the streamer. Would you like to automatically update and reconnect?", "Update Required", MessageBoxButtons.YesNo, MessageBoxIcon.Information); + if (res == DialogResult.Yes) + { + PerformAutoUpdate(selectedSrv); + } + return; + } + + ViewerForm viewer = new ViewerForm(selectedSrv.IPAddress, selectedSrv.Port); + this.Hide(); + viewer.ShowDialog(); + this.Show(); + } + + private void btnSwitchMode_Click(object sender, EventArgs e) + { + Program.NextForm = new ServerForm(); + this.Close(); + } + + private void chkLogging_CheckedChanged(object sender, EventArgs e) + { + Program.LoggingEnabled = chkLogging.Checked; + } + + private void lstServers_DoubleClick(object sender, EventArgs e) + { + btnConnect_Click(sender, e); + } + + private void lstServers_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Enter) + { + btnConnect_Click(sender, e); + e.Handled = true; + e.SuppressKeyPress = true; + } + } + + private void PerformAutoUpdate(ServerInfo srv) + { + try + { + string exePath = Application.ExecutablePath; + string exeName = Path.GetFileName(exePath); + string tempExe = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "update.exe"); + string batPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "update.bat"); + + using (var client = new TcpClient(srv.IPAddress, 21000)) + using (var stream = client.GetStream()) + using (var fs = File.Create(tempExe)) + { + byte[] lenBuf = new byte[4]; + stream.Read(lenBuf, 0, 4); + int len = BitConverter.ToInt32(lenBuf, 0); + + byte[] buffer = new byte[8192]; + int totalRead = 0; + while (totalRead < len) + { + int read = stream.Read(buffer, 0, Math.Min(buffer.Length, len - totalRead)); + if (read == 0) throw new Exception("Disconnected during update download."); + fs.Write(buffer, 0, read); + totalRead += read; + } + } + + string bat = $@" +@echo off +timeout /t 2 /nobreak > NUL +del ""{exeName}"" +ren ""update.exe"" ""{exeName}"" +start """" ""{exeName}"" -autoconnect {srv.IPAddress}:{srv.Port} +del ""%~f0"" +"; + File.WriteAllText(batPath, bat); + Process.Start(new ProcessStartInfo(batPath) { CreateNoWindow = true, UseShellExecute = false }); + Application.Exit(); + } + catch (Exception ex) + { + MessageBox.Show("Update failed: " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void ClientForm_FormClosing(object sender, FormClosingEventArgs e) + { + updateTimer.Stop(); + discoveryClient?.Stop(); + } + } +} \ No newline at end of file diff --git a/DisplayStreamer/ClientForm.resx b/DisplayStreamer/ClientForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/DisplayStreamer/ClientForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/DisplayStreamer/DisplayStreamer.csproj b/DisplayStreamer/DisplayStreamer.csproj new file mode 100644 index 0000000..a535ae2 --- /dev/null +++ b/DisplayStreamer/DisplayStreamer.csproj @@ -0,0 +1,20 @@ + + + + WinExe + net10.0-windows + enable + true + enable + AnyCPU;x64 + True + + + + + + + + + + \ No newline at end of file diff --git a/DisplayStreamer/Program.cs b/DisplayStreamer/Program.cs new file mode 100644 index 0000000..9cabd4f --- /dev/null +++ b/DisplayStreamer/Program.cs @@ -0,0 +1,1981 @@ +using SharpDX; +using SharpDX.Direct3D; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using SharpDX.MediaFoundation; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using InterpolationMode = System.Drawing.Drawing2D.InterpolationMode; + +namespace DisplayStreamer +{ + public static class Logger + { + public static event Action OnLog; + + public static void Log(string message) + { + if (!Program.LoggingEnabled) return; + string line = $"[{DateTime.Now:HH:mm:ss.fff}] {message}"; + Debug.WriteLine(line); + OnLog?.Invoke(line); + } + + public static void LogRaw(string message) + { + if (!Program.LoggingEnabled) return; + Debug.WriteLine(message); + OnLog?.Invoke(message); + } + } + + public static class ClientLogger + { + private static TcpClient logClient; + private static StreamWriter logWriter; + private static object lockObj = new object(); + + public static void Connect(string ip, int port) + { + Task.Run(() => + { + try + { + logClient = new TcpClient(); + logClient.Connect(ip, port); + logWriter = new StreamWriter(logClient.GetStream(), Encoding.UTF8) { AutoFlush = true }; + Log("ClientLogger connected to server."); + } + catch (Exception ex) + { + Debug.WriteLine("ClientLogger failed to connect: " + ex.Message); + } + }); + } + + public static void Log(string message) + { + if (!Program.LoggingEnabled) return; + string line = $"[CLIENT {DateTime.Now:HH:mm:ss.fff}] {message}"; + Debug.WriteLine(line); + lock (lockObj) + { + try + { + logWriter?.WriteLine(line); + } + catch { } + } + } + + public static void Disconnect() + { + lock (lockObj) + { + try { logWriter?.Dispose(); } catch { } + try { logClient?.Close(); } catch { } + logWriter = null; + logClient = null; + } + } + } + + public class LogServer + { + private TcpListener listener; + private bool running; + + public void Start(int port) + { + running = true; + listener = new TcpListener(IPAddress.Any, port); + listener.Start(); + new Thread(() => + { + while (running) + { + try + { + var client = listener.AcceptTcpClient(); + new Thread(() => HandleClient(client)) { IsBackground = true }.Start(); + } + catch { } + } + }) + { IsBackground = true }.Start(); + } + + private void HandleClient(TcpClient client) + { + try + { + using (var reader = new StreamReader(client.GetStream(), Encoding.UTF8)) + { + string line; + while ((line = reader.ReadLine()) != null) + { + Logger.LogRaw(line); + } + } + } + catch { } + } + + public void Stop() + { + running = false; + listener?.Stop(); + } + } + + static class Program + { + public static string AppHash { get; private set; } + public static Form NextForm { get; set; } + public static bool LoggingEnabled = false; + + [STAThread] + static void Main(string[] args) + { + Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.SetColorMode(SystemColorMode.Dark); + + // Initialize Media Foundation for Hardware H.265/H.264 Encoding + MediaManager.Startup(); + + using (var md5 = System.Security.Cryptography.MD5.Create()) + using (var stream = File.OpenRead(Application.ExecutablePath)) + { + AppHash = BitConverter.ToString(md5.ComputeHash(stream)).Replace("-", ""); + } + + for (int i = 0; i < args.Length; i++) + { + if (args[i] == "-autoconnect" && i + 1 < args.Length) + { + string[] parts = args[i + 1].Split(':'); + NextForm = new ClientForm(parts[0], int.Parse(parts[1])); + break; + } + } + + if (NextForm == null) + { + // Auto-Detect Logic + var discovery = new UdpDiscoveryClient(); + discovery.Start(); + Thread.Sleep(1500); // Listen for 1.5 seconds + var servers = discovery.GetDiscoveredServers(); + discovery.Stop(); + + if (servers.Count > 0) + { + NextForm = new ClientForm(); + } + else + { + NextForm = new ServerForm(); + } + } + + // Form State Machine Loop + while (NextForm != null) + { + Form current = NextForm; + NextForm = null; + Application.Run(current); + } + } + } + + // ========================================== + // NATIVE INTEROP FOR MEDIA FOUNDATION + // ========================================== + public static class MFInterop + { + [StructLayout(LayoutKind.Sequential)] + public struct MFT_REGISTER_TYPE_INFO + { + public Guid guidMajorType; + public Guid guidSubtype; + } + + [DllImport("mfplat.dll", ExactSpelling = true)] + public static extern int MFTEnumEx( + Guid guidCategory, + uint flags, + IntPtr pInputType, + IntPtr pOutputType, + out IntPtr pppMFTActivate, + out int pnumMFTActivate + ); + } + + // ========================================== + // NETWORKING: DISCOVERY & UPDATES + // ========================================== + public class ServerInfo + { + public string MachineName { get; set; } + public string IPAddress { get; set; } + public int Port { get; set; } + public string Hash { get; set; } + public DateTime LastSeen { get; set; } + } + + public class UdpDiscoveryServer + { + private List broadcastIps; + private List> endpoints; + private bool running; + private Thread broadcastThread; + + public UdpDiscoveryServer(List ips, List> endpoints) + { + this.broadcastIps = ips; + this.endpoints = endpoints; + } + + public void Start() + { + running = true; + broadcastThread = new Thread(BroadcastLoop) { IsBackground = true }; + broadcastThread.Start(); + } + + public void Stop() + { + running = false; + } + + private void BroadcastLoop() + { + while (running) + { + foreach (var ip in broadcastIps) + { + try + { + IPAddress localIp = IPAddress.Parse(ip); + using (UdpClient udp = new UdpClient(new IPEndPoint(localIp, 0))) + { + udp.EnableBroadcast = true; + foreach (var ep in endpoints) + { + string msg = $"H265_SERVER|{Environment.MachineName} - {ep.Item1}|{ip}|{ep.Item2}|{Program.AppHash}"; + byte[] data = Encoding.UTF8.GetBytes(msg); + udp.Send(data, data.Length, new IPEndPoint(IPAddress.Broadcast, 19999)); + } + } + } + catch { } + } + Thread.Sleep(2000); + } + } + } + + public class UdpDiscoveryClient + { + private UdpClient udp; + private bool running; + private Thread listenThread; + private Dictionary servers = new Dictionary(); + + public void Start() + { + running = true; + udp = new UdpClient(); + udp.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + udp.Client.Bind(new IPEndPoint(IPAddress.Any, 19999)); + + listenThread = new Thread(ListenLoop) { IsBackground = true }; + listenThread.Start(); + } + + public void Stop() + { + running = false; + udp?.Close(); + } + + private void ListenLoop() + { + IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any, 19999); + while (running) + { + try + { + byte[] data = udp.Receive(ref remoteEP); + string msg = Encoding.UTF8.GetString(data); + if (msg.StartsWith("H265_SERVER")) + { + var parts = msg.Split('|'); + if (parts.Length >= 5) + { + string key = parts[2] + ":" + parts[3]; + lock (servers) + { + servers[key] = new ServerInfo + { + MachineName = parts[1], + IPAddress = parts[2], + Port = int.Parse(parts[3]), + Hash = parts[4], + LastSeen = DateTime.Now + }; + } + } + } + } + catch { } + } + } + + public List GetDiscoveredServers() + { + lock (servers) + { + var now = DateTime.Now; + var active = servers.Values.Where(s => (now - s.LastSeen).TotalSeconds < 5).ToList(); + return active; + } + } + } + + public class UpdateServer + { + private TcpListener listener; + private bool running; + + public void Start(int port) + { + running = true; + listener = new TcpListener(IPAddress.Any, port); + listener.Start(); + new Thread(() => + { + while (running) + { + try + { + var client = listener.AcceptTcpClient(); + var stream = client.GetStream(); + byte[] exeBytes = File.ReadAllBytes(Application.ExecutablePath); + stream.Write(BitConverter.GetBytes(exeBytes.Length), 0, 4); + stream.Write(exeBytes, 0, exeBytes.Length); + client.Close(); + } + catch { } + } + }) + { IsBackground = true }.Start(); + } + + public void Stop() + { + running = false; + listener?.Stop(); + } + } + + // ========================================== + // NETWORKING & ENCODING: SERVER STREAMER + // ========================================== + public class StreamServer + { + [DllImport("winmm.dll", EntryPoint = "timeBeginPeriod")] + private static extern uint TimeBeginPeriod(uint uMilliseconds); + + [DllImport("winmm.dll", EntryPoint = "timeEndPeriod")] + private static extern uint TimeEndPeriod(uint uMilliseconds); + + [DllImport("avrt.dll", CharSet = CharSet.Unicode)] + private static extern IntPtr AvSetMmThreadCharacteristics(string TaskName, ref uint TaskIndex); + + [DllImport("avrt.dll")] + private static extern bool AvRevertMmThreadCharacteristics(IntPtr Handle); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern IntPtr CreateWaitableTimerEx(IntPtr lpTimerAttributes, string lpTimerName, uint dwFlags, uint dwDesiredAccess); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetWaitableTimer(IntPtr hTimer, ref long pDueTime, int lPeriod, IntPtr pfnCompletionRoutine, IntPtr lpArgToCompletionRoutine, bool fResume); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr hObject); + + private const uint CREATE_WAITABLE_TIMER_HIGH_RESOLUTION = 0x00000002; + private const uint TIMER_ALL_ACCESS = 0x1F0003; + + private int displayIndex; + private int port; + private volatile bool running; + private TcpListener listener; + private List clients = new List(); + + private Thread captureThread; + private Thread encodeThread; + private byte[] headerBuffer = new byte[24]; + private int broadcastCount = 0; + + class FrameData + { + public byte[] Bgra; + public HardwareEncoder.CURSORINFO CursorInfo; + } + + private BlockingCollection freeQueue; + private BlockingCollection readyQueue; + + public StreamServer(int displayIndex, int port) + { + this.displayIndex = displayIndex; + this.port = port; + } + + public void Start() + { + Logger.Log($"Starting StreamServer on port {port} for display {displayIndex}"); + running = true; + listener = new TcpListener(IPAddress.Any, port); + listener.Start(); + + new Thread(AcceptLoop) { IsBackground = true }.Start(); + + captureThread = new Thread(CaptureLoop) { IsBackground = true, Priority = ThreadPriority.Highest }; + captureThread.Start(); + } + + public void Stop() + { + Logger.Log($"Stopping StreamServer on port {port}"); + running = false; + listener?.Stop(); + lock (clients) + { + foreach (var c in clients) c.Close(); + clients.Clear(); + } + readyQueue?.CompleteAdding(); + } + + private void AcceptLoop() + { + while (running) + { + try + { + var client = listener.AcceptTcpClient(); + client.NoDelay = true; + client.SendBufferSize = 262144; // 256KB buffer to prevent latency buildup + client.SendTimeout = 3000; // 3 seconds to prevent spurious disconnects + lock (clients) clients.Add(client); + Logger.Log($"Client connected from {client.Client.RemoteEndPoint}"); + } + catch (Exception ex) + { + if (running) Logger.Log($"AcceptLoop error: {ex.Message}"); + } + } + } + + private void CaptureLoop() + { + Logger.Log("CaptureLoop started."); + TimeBeginPeriod(1); + + // Create a high-resolution kernel waitable timer to bypass Window Occlusion throttling + IntPtr hTimer = CreateWaitableTimerEx(IntPtr.Zero, null, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); + if (hTimer == IntPtr.Zero) + { + // Fallback for older Windows versions + hTimer = CreateWaitableTimerEx(IntPtr.Zero, null, 0, TIMER_ALL_ACCESS); + } + + IntPtr avrtHandle = IntPtr.Zero; + try + { + uint taskIndex = 0; + avrtHandle = AvSetMmThreadCharacteristics("Pro Audio", ref taskIndex); + } + catch { } + + try + { + Adapter1 selectedAdapter = null; + Output selectedOutput = null; + + using (var factory = new Factory1()) + { + var screen = Screen.AllScreens[displayIndex]; + foreach (var adapter in factory.Adapters1) + { + foreach (var output in adapter.Outputs) + { + var bounds = output.Description.DesktopBounds; + if (bounds.Left == screen.Bounds.Left && bounds.Top == screen.Bounds.Top) + { + selectedAdapter = adapter; + selectedOutput = output; + break; + } + } + if (selectedOutput != null) break; + } + } + + if (selectedAdapter == null || selectedOutput == null) + { + Logger.Log("Capture Error: Could not locate the selected display on any GPU adapter."); + throw new Exception("Could not locate the selected display on any GPU adapter."); + } + + Logger.Log($"Using adapter: {selectedAdapter.Description.Description}, output: {selectedOutput.Description.DeviceName}"); + + using (selectedAdapter) + using (var device = new SharpDX.Direct3D11.Device(selectedAdapter)) + using (selectedOutput) + using (var output1 = selectedOutput.QueryInterface()) + using (var duplication = output1.DuplicateOutput(device)) + { + var boundsDesc = selectedOutput.Description.DesktopBounds; + int width = boundsDesc.Right - boundsDesc.Left; + int height = boundsDesc.Bottom - boundsDesc.Top; + int screenLeft = boundsDesc.Left; + int screenTop = boundsDesc.Top; + + int targetWidth = width > 1920 ? 1920 : width; + int targetHeight = height > 1080 ? 1080 : height; + + // Ensure even dimensions for NV12 conversion + targetWidth = (targetWidth / 2) * 2; + targetHeight = (targetHeight / 2) * 2; + + Logger.Log($"Capture resolution: {width}x{height}. Target resolution: {targetWidth}x{targetHeight}"); + + freeQueue = new BlockingCollection(); + readyQueue = new BlockingCollection(1); // Only buffer 1 frame to ensure zero latency + for (int i = 0; i < 3; i++) + { + freeQueue.Add(new FrameData { Bgra = new byte[width * height * 4] }); + } + + encodeThread = new Thread(() => EncodeLoop(width, height, targetWidth, targetHeight, screenLeft, screenTop)) { IsBackground = true, Priority = ThreadPriority.Highest }; + encodeThread.Start(); + + Texture2DDescription desktopDesc = new Texture2DDescription + { + CpuAccessFlags = CpuAccessFlags.None, + BindFlags = BindFlags.None, + Format = Format.B8G8R8A8_UNorm, + Width = width, + Height = height, + OptionFlags = ResourceOptionFlags.None, + MipLevels = 1, + ArraySize = 1, + SampleDescription = { Count = 1, Quality = 0 }, + Usage = ResourceUsage.Default + }; + + Texture2DDescription stagingDesc = new Texture2DDescription + { + CpuAccessFlags = CpuAccessFlags.Read, + BindFlags = BindFlags.None, + Format = Format.B8G8R8A8_UNorm, + Width = width, + Height = height, + OptionFlags = ResourceOptionFlags.None, + MipLevels = 1, + ArraySize = 1, + SampleDescription = { Count = 1, Quality = 0 }, + Usage = ResourceUsage.Staging + }; + + using (var desktopTexture = new Texture2D(device, desktopDesc)) + using (var stagingTexture = new Texture2D(device, stagingDesc)) + { + bool hasInitialFrame = false; + Stopwatch framePacer = Stopwatch.StartNew(); + + double targetMs = 1000.0 / 60.0; + double nextFrameMs = framePacer.Elapsed.TotalMilliseconds + targetMs; + int timeoutCount = 0; + + while (running) + { + SharpDX.DXGI.Resource desktopResource = null; + OutputDuplicateFrameInformation frameInfo; + + var result = duplication.TryAcquireNextFrame(0, out frameInfo, out desktopResource); + + if (result.Success) + { + using (desktopResource) + using (var tempTexture = desktopResource.QueryInterface()) + { + device.ImmediateContext.CopyResource(tempTexture, desktopTexture); + } + duplication.ReleaseFrame(); + hasInitialFrame = true; + timeoutCount = 0; + } + else if (result.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Result.Code) + { + timeoutCount++; + if (!hasInitialFrame && timeoutCount % 60 == 0) + { + Logger.Log("CaptureLoop: Screen is static. Sending black frames to keep stream alive..."); + } + } + else + { + Logger.Log($"TryAcquireNextFrame failed with code: 0x{result.Code:X}"); + break; + } + + if (hasInitialFrame) + { + device.ImmediateContext.CopyResource(desktopTexture, stagingTexture); + + if (freeQueue.TryTake(out var frame)) + { + DataBox mapSource = device.ImmediateContext.MapSubresource(stagingTexture, 0, MapMode.Read, SharpDX.Direct3D11.MapFlags.None); + + if (mapSource.RowPitch == width * 4) + { + Marshal.Copy(mapSource.DataPointer, frame.Bgra, 0, width * height * 4); + } + else + { + for (int y = 0; y < height; y++) + { + Marshal.Copy(mapSource.DataPointer + y * mapSource.RowPitch, frame.Bgra, y * width * 4, width * 4); + } + } + device.ImmediateContext.UnmapSubresource(stagingTexture, 0); + + HardwareEncoder.CURSORINFO pci = new HardwareEncoder.CURSORINFO(); + pci.cbSize = Marshal.SizeOf(typeof(HardwareEncoder.CURSORINFO)); + HardwareEncoder.GetCursorInfo(out pci); + frame.CursorInfo = pci; + + if (!readyQueue.TryAdd(frame)) + { + freeQueue.Add(frame); // Drop frame if encoder is backed up + } + } + } + else + { + // If DXGI hasn't yielded a frame yet, send a black frame to initialize the encoder + if (freeQueue.TryTake(out var frame)) + { + Array.Clear(frame.Bgra, 0, frame.Bgra.Length); + frame.CursorInfo = new HardwareEncoder.CURSORINFO(); + if (!readyQueue.TryAdd(frame)) freeQueue.Add(frame); + } + } + + // Fixed-Timestep Pacing for perfectly locked 60 FPS (0% CPU usage) + double currentMs = framePacer.Elapsed.TotalMilliseconds; + double remainingMs = nextFrameMs - currentMs; + + if (remainingMs > 0) + { + if (hTimer != IntPtr.Zero) + { + long dueTime = -(long)(remainingMs * 10000.0); + SetWaitableTimer(hTimer, ref dueTime, 0, IntPtr.Zero, IntPtr.Zero, false); + WaitForSingleObject(hTimer, 0xFFFFFFFF); + } + else + { + int sleepMs = (int)remainingMs; + if (sleepMs > 0) Thread.Sleep(sleepMs); + } + } + + nextFrameMs += targetMs; + } + } + } + } + catch (Exception ex) + { + Logger.Log($"Capture Error: {ex.Message}\n{ex.StackTrace}"); + Thread.Sleep(1000); + } + finally + { + if (hTimer != IntPtr.Zero) CloseHandle(hTimer); + try { if (avrtHandle != IntPtr.Zero) AvRevertMmThreadCharacteristics(avrtHandle); } catch { } + TimeEndPeriod(1); + Logger.Log("CaptureLoop exited."); + } + } + + private void EncodeLoop(int width, int height, int targetWidth, int targetHeight, int screenLeft, int screenTop) + { + Logger.Log("EncodeLoop started. Waiting for frames..."); + Stopwatch frameTimer = new Stopwatch(); + Stopwatch fpsTimer = Stopwatch.StartNew(); + int frameCount = 0; + float currentFps = 0; + int zeroByteFrames = 0; + bool firstFrame = true; + + IntPtr avrtHandle = IntPtr.Zero; + try + { + uint taskIndex = 0; + avrtHandle = AvSetMmThreadCharacteristics("Pro Audio", ref taskIndex); + } + catch { } + + try + { + using (var encoder = new HardwareEncoder(targetWidth, targetHeight, 60)) + { + foreach (var frame in readyQueue.GetConsumingEnumerable()) + { + if (firstFrame) + { + Logger.Log("EncodeLoop: Received first frame from CaptureLoop. Starting encoding..."); + firstFrame = false; + } + + try + { + frameTimer.Restart(); + ArraySegment encoded = encoder.EncodeFrame(frame.Bgra, width, height, targetWidth, targetHeight, screenLeft, screenTop, frame.CursorInfo); + frameTimer.Stop(); + + frameCount++; + if (fpsTimer.ElapsedMilliseconds >= 1000) + { + currentFps = frameCount / (float)fpsTimer.Elapsed.TotalSeconds; + frameCount = 0; + fpsTimer.Restart(); + } + + if (encoded.Count > 0) + { + if (zeroByteFrames > 0) Logger.Log($"Encoder recovered after {zeroByteFrames} zero-byte frames."); + zeroByteFrames = 0; + BroadcastToClients(encoded, currentFps, (float)frameTimer.Elapsed.TotalMilliseconds, targetWidth, targetHeight, encoder.IsHevc); + } + else + { + zeroByteFrames++; + if (zeroByteFrames == 1 || zeroByteFrames % 60 == 0) + { + Logger.Log($"Warning: Encoder produced 0 bytes (Consecutive count: {zeroByteFrames})"); + } + } + } + catch (Exception ex) + { + Logger.Log($"EncodeFrame Exception: {ex.Message}"); + } + finally + { + freeQueue.Add(frame); + } + } + } + } + catch (Exception ex) + { + Logger.Log($"EncodeLoop Fatal Error: {ex.Message}\n{ex.StackTrace}"); + } + finally + { + try { if (avrtHandle != IntPtr.Zero) AvRevertMmThreadCharacteristics(avrtHandle); } catch { } + } + Logger.Log("EncodeLoop exited."); + } + + private void BroadcastToClients(ArraySegment data, float fps, float frameTime, int width, int height, bool isHevc) + { + int payloadLength = 24 + data.Count; // 24 bytes for the header + BitConverter.GetBytes(payloadLength).CopyTo(headerBuffer, 0); + BitConverter.GetBytes(fps).CopyTo(headerBuffer, 4); + BitConverter.GetBytes(frameTime).CopyTo(headerBuffer, 8); + BitConverter.GetBytes(width).CopyTo(headerBuffer, 12); + BitConverter.GetBytes(height).CopyTo(headerBuffer, 16); + BitConverter.GetBytes(isHevc ? 1 : 0).CopyTo(headerBuffer, 20); + + lock (clients) + { + for (int i = clients.Count - 1; i >= 0; i--) + { + try + { + var stream = clients[i].GetStream(); + stream.Write(headerBuffer, 0, 24); + stream.Write(data.Array, data.Offset, data.Count); + } + catch (Exception ex) + { + Logger.Log($"Client disconnected or send failed: {ex.Message}"); + clients[i].Close(); + clients.RemoveAt(i); + } + } + } + + broadcastCount++; + if (broadcastCount == 1) Logger.Log("Broadcasted first frame to clients."); + if (broadcastCount % 60 == 0) Logger.Log($"Broadcasting active. Server FPS: {fps:F1}"); + } + } + + // ========================================== + // NETWORKING & DECODING: CLIENT STREAMER + // ========================================== + public class StreamClient + { + private string ip; + private int port; + private Control targetControl; + private Label metricsLabel; + private bool running; + private TcpClient client; + private Thread receiveThread; + private Thread renderThread; + + public float ServerFps { get; private set; } + public float ServerFrameTime { get; private set; } + public float ClientFps { get; private set; } + public float ClientFrameTime { get; private set; } + + struct RenderFrame + { + public byte[] Data; + public int Width; + public int Height; + } + + private BlockingCollection renderQueue; + + public StreamClient(string ip, int port, Control targetControl, Label metricsLabel) + { + this.ip = ip; + this.port = port; + this.targetControl = targetControl; + this.metricsLabel = metricsLabel; + } + + public void Start() + { + ClientLogger.Connect(ip, 22000); + running = true; + renderQueue = new BlockingCollection(1); // Buffer 1 frame for the render thread + + receiveThread = new Thread(ReceiveLoop) { IsBackground = true, Priority = ThreadPriority.AboveNormal }; + receiveThread.Start(); + + renderThread = new Thread(RenderLoop) { IsBackground = true, Priority = ThreadPriority.Highest }; + renderThread.Start(); + } + + public void Stop() + { + running = false; + client?.Close(); + renderQueue?.CompleteAdding(); + ClientLogger.Disconnect(); + } + + private void ReceiveLoop() + { + ClientLogger.Log($"ReceiveLoop started. Connecting to {ip}:{port}..."); + Stopwatch frameTimer = new Stopwatch(); + + byte[][] recvBuffers = new byte[3][]; + for (int i = 0; i < 3; i++) recvBuffers[i] = new byte[1024 * 1024 * 5]; + int bufIdx = 0; + + byte[][] bgraBuffers = new byte[2][]; + int bgraIdx = 0; + + HardwareDecoder decoder = null; + int currentWidth = 0; + int currentHeight = 0; + bool currentIsHevc = false; + + try + { + client = new TcpClient(); + client.Connect(ip, port); + client.NoDelay = true; + client.ReceiveBufferSize = 262144; // 256KB buffer to prevent latency buildup + var stream = client.GetStream(); + ClientLogger.Log("Connected to stream server successfully. Waiting for frames..."); + + byte[] headerBuf = new byte[24]; + while (running) + { + int read = 0; + while (read < 24) + { + int r = stream.Read(headerBuf, read, 24 - read); + if (r == 0) throw new Exception("Disconnected while reading header."); + read += r; + } + + int payloadLength = BitConverter.ToInt32(headerBuf, 0); + ServerFps = BitConverter.ToSingle(headerBuf, 4); + ServerFrameTime = BitConverter.ToSingle(headerBuf, 8); + int width = BitConverter.ToInt32(headerBuf, 12); + int height = BitConverter.ToInt32(headerBuf, 16); + bool isHevc = BitConverter.ToInt32(headerBuf, 20) == 1; + + if (decoder == null || currentWidth != width || currentHeight != height || currentIsHevc != isHevc) + { + ClientLogger.Log($"Initializing HardwareDecoder. Width: {width}, Height: {height}, HEVC: {isHevc}"); + decoder?.Dispose(); + decoder = new HardwareDecoder(width, height, isHevc); + currentWidth = width; + currentHeight = height; + currentIsHevc = isHevc; + + for (int i = 0; i < 2; i++) + { + bgraBuffers[i] = new byte[width * height * 4]; + } + } + + int frameLength = payloadLength - 24; // Subtract 24 bytes for the header + byte[] currentBuffer = recvBuffers[bufIdx]; + + // Dynamically resize buffer if a massive keyframe arrives + if (currentBuffer.Length < frameLength) + { + ClientLogger.Log($"Resizing receive buffer to {frameLength + 1024 * 1024} bytes."); + currentBuffer = new byte[frameLength + 1024 * 1024]; + recvBuffers[bufIdx] = currentBuffer; + } + + read = 0; + while (read < frameLength) + { + int r = stream.Read(currentBuffer, read, frameLength - read); + if (r == 0) throw new Exception("Disconnected while reading frame data."); + read += r; + } + + try + { + byte[] targetBgra = bgraBuffers[bgraIdx]; + + frameTimer.Restart(); + bool success = decoder.DecodeFrame(currentBuffer, frameLength, width, height, targetBgra); + frameTimer.Stop(); + + if (success) + { + ClientFrameTime = (float)frameTimer.Elapsed.TotalMilliseconds; + + var renderFrame = new RenderFrame { Data = targetBgra, Width = width, Height = height }; + + // Push to render thread. If the queue is full (render thread is waiting on VSync), drop the oldest frame. + if (!renderQueue.TryAdd(renderFrame)) + { + renderQueue.TryTake(out _); + renderQueue.TryAdd(renderFrame); + } + + bgraIdx = (bgraIdx + 1) % 2; + } + + bufIdx = (bufIdx + 1) % 3; + } + catch (Exception ex) + { + ClientLogger.Log($"ReceiveLoop inner exception: {ex.Message}"); + } + } + } + catch (Exception ex) + { + ClientLogger.Log($"ReceiveLoop fatal error: {ex.Message}"); + decoder?.Dispose(); + if (running && !targetControl.IsDisposed && targetControl.IsHandleCreated) + { + targetControl.BeginInvoke((MethodInvoker)delegate + { + Form f = targetControl.FindForm(); + if (f != null && !f.IsDisposed) f.Close(); + }); + } + } + ClientLogger.Log("ReceiveLoop exited."); + } + + private void RenderLoop() + { + ClientLogger.Log("RenderLoop started."); + SharpDX.Direct3D11.Device d3dDevice = null; + SwapChain swapChain = null; + Texture2D backBuffer = null; + Texture2D stagingTexture = null; + int currentWidth = 0; + int currentHeight = 0; + + Stopwatch fpsTimer = Stopwatch.StartNew(); + int renderCount = 0; + + try + { + foreach (var frame in renderQueue.GetConsumingEnumerable()) + { + if (currentWidth != frame.Width || currentHeight != frame.Height) + { + stagingTexture?.Dispose(); + backBuffer?.Dispose(); + swapChain?.Dispose(); + d3dDevice?.Dispose(); + + currentWidth = frame.Width; + currentHeight = frame.Height; + + var desc = new SwapChainDescription() + { + BufferCount = 2, + ModeDescription = new ModeDescription(currentWidth, currentHeight, new Rational(60, 1), Format.B8G8R8A8_UNorm), + IsWindowed = true, + OutputHandle = targetControl.Handle, + SampleDescription = new SampleDescription(1, 0), + SwapEffect = SwapEffect.Discard, + Usage = Usage.RenderTargetOutput + }; + + SharpDX.Direct3D11.Device.CreateWithSwapChain(DriverType.Hardware, DeviceCreationFlags.None, desc, out d3dDevice, out swapChain); + + // Prevent DXGI from messing with the window size/Alt-Enter + using (var factory = swapChain.GetParent()) + { + factory.MakeWindowAssociation(targetControl.Handle, WindowAssociationFlags.IgnoreAll); + } + + backBuffer = Texture2D.FromSwapChain(swapChain, 0); + + var stagingDesc = new Texture2DDescription() + { + Width = currentWidth, + Height = currentHeight, + MipLevels = 1, + ArraySize = 1, + Format = Format.B8G8R8A8_UNorm, + SampleDescription = new SampleDescription(1, 0), + Usage = ResourceUsage.Dynamic, + BindFlags = BindFlags.ShaderResource, + CpuAccessFlags = CpuAccessFlags.Write, + OptionFlags = ResourceOptionFlags.None + }; + stagingTexture = new Texture2D(d3dDevice, stagingDesc); + ClientLogger.Log($"D3D11 SwapChain initialized for {currentWidth}x{currentHeight}"); + } + + // Map the dynamic texture and copy the BGRA bytes + DataBox map = d3dDevice.ImmediateContext.MapSubresource(stagingTexture, 0, MapMode.WriteDiscard, SharpDX.Direct3D11.MapFlags.None); + + if (map.RowPitch == currentWidth * 4) + { + Marshal.Copy(frame.Data, 0, map.DataPointer, frame.Data.Length); + } + else + { + for (int y = 0; y < currentHeight; y++) + { + Marshal.Copy(frame.Data, y * currentWidth * 4, map.DataPointer + (y * map.RowPitch), currentWidth * 4); + } + } + + d3dDevice.ImmediateContext.UnmapSubresource(stagingTexture, 0); + + // Copy the staging texture to the SwapChain backbuffer + d3dDevice.ImmediateContext.CopyResource(stagingTexture, backBuffer); + + // Present with Hardware VSync (1) + swapChain.Present(1, 0); + + renderCount++; + if (fpsTimer.ElapsedMilliseconds >= 1000) + { + ClientFps = renderCount / (float)fpsTimer.Elapsed.TotalSeconds; + renderCount = 0; + fpsTimer.Restart(); + + if (metricsLabel != null && metricsLabel.IsHandleCreated && !metricsLabel.IsDisposed && metricsLabel.Visible) + { + metricsLabel.BeginInvoke((MethodInvoker)delegate + { + metricsLabel.Text = $"SERVER\nFPS: {ServerFps:F1}\nCapture+Encode: {ServerFrameTime:F1} ms\n\n" + + $"CLIENT\nFPS: {ClientFps:F1}\nDecode Time: {ClientFrameTime:F1} ms"; + }); + } + } + } + } + catch (Exception ex) + { + ClientLogger.Log($"RenderLoop Error: {ex.Message}"); + } + finally + { + stagingTexture?.Dispose(); + backBuffer?.Dispose(); + swapChain?.Dispose(); + d3dDevice?.Dispose(); + } + ClientLogger.Log("RenderLoop exited."); + } + } + + // ========================================== + // HARDWARE ENCODER (Media Foundation) + // ========================================== + public class HardwareEncoder : IDisposable + { + public static readonly Guid MFVideoFormat_HEVC = new Guid("24312C1E-2614-4696-8122-81B83B000000"); + + private Transform encoder; + private byte[] scaledBgraBuffer; + private MemoryStream outStream = new MemoryStream(1024 * 1024); + private long frameTime = 0; + private byte[] sequenceHeader = null; + private bool sequenceHeaderLogged = false; + private bool firstFrameEncoded = false; + public bool IsHevc { get; private set; } + + private TOutputDataBuffer[] outputDataBuffer = new TOutputDataBuffer[1]; + + // Cursor Caching + private IntPtr lastCursorHandle = IntPtr.Zero; + private int lastCursorHotspotX = 0; + private int lastCursorHotspotY = 0; + + // Scaling Caching + private int lastSrcWidth, lastSrcHeight, lastDstWidth, lastDstHeight; + private int[] scaleXOffsets; + private int[] scaleYOffsets; + + [StructLayout(LayoutKind.Sequential)] + public struct POINT { public int X; public int Y; } + + [StructLayout(LayoutKind.Sequential)] + public struct CURSORINFO + { + public int cbSize; + public int flags; + public IntPtr hCursor; + public POINT ptScreenPos; + } + + [StructLayout(LayoutKind.Sequential)] + public struct ICONINFO + { + public bool fIcon; + public int xHotspot; + public int yHotspot; + public IntPtr hbmMask; + public IntPtr hbmColor; + } + + [StructLayout(LayoutKind.Sequential)] + public struct BITMAP + { + public int bmType; + public int bmWidth; + public int bmHeight; + public int bmWidthBytes; + public short bmPlanes; + public short bmBitsPixel; + public IntPtr bmBits; + } + + [DllImport("user32.dll")] + public static extern bool GetCursorInfo(out CURSORINFO pci); + + [DllImport("user32.dll")] + public static extern bool GetIconInfo(IntPtr hIcon, out ICONINFO piconinfo); + + [DllImport("user32.dll")] + public static extern bool DrawIconEx(IntPtr hdc, int xLeft, int yTop, IntPtr hIcon, int cxWidth, int cyWidth, int istepIfAniCur, IntPtr hbrFlickerFreeDraw, int diFlags); + + [DllImport("gdi32.dll")] + public static extern int GetObject(IntPtr hgdiobj, int cbBuffer, out BITMAP lpvObject); + + [DllImport("gdi32.dll")] + public static extern bool DeleteObject(IntPtr hObject); + + private const int CURSOR_SHOWING = 0x00000001; + private const int DI_NORMAL = 0x0003; + + private Transform CreateEncoder(Guid subtype) + { + MFInterop.MFT_REGISTER_TYPE_INFO outInfo = new MFInterop.MFT_REGISTER_TYPE_INFO { guidMajorType = MediaTypeGuids.Video, guidSubtype = subtype }; + IntPtr pOutInfo = Marshal.AllocHGlobal(Marshal.SizeOf(outInfo)); + Marshal.StructureToPtr(outInfo, pOutInfo, false); + + IntPtr pppActivate = IntPtr.Zero; + int count = 0; + uint flags = 0x00000020 | 0x00000040; // MFT_ENUM_FLAG_HARDWARE | MFT_ENUM_FLAG_SORTANDFILTER + + int hr = MFInterop.MFTEnumEx(TransformCategoryGuids.VideoEncoder, flags, IntPtr.Zero, pOutInfo, out pppActivate, out count); + Marshal.FreeHGlobal(pOutInfo); + + Transform result = null; + if (hr == 0 && count > 0 && pppActivate != IntPtr.Zero) + { + IntPtr pActivate = Marshal.ReadIntPtr(pppActivate); + using (var activate = new Activate(pActivate)) + { + result = activate.ActivateObject(); + } + for (int i = 1; i < count; i++) + { + IntPtr pObj = Marshal.ReadIntPtr(pppActivate, i * IntPtr.Size); + if (pObj != IntPtr.Zero) Marshal.Release(pObj); + } + Marshal.FreeCoTaskMem(pppActivate); + } + return result; + } + + public HardwareEncoder(int width, int height, int fps) + { + Logger.Log("Initializing HardwareEncoder..."); + encoder = CreateEncoder(MFVideoFormat_HEVC); + if (encoder != null) + { + IsHevc = true; + Logger.Log("Found HEVC hardware encoder."); + } + else + { + encoder = CreateEncoder(VideoFormatGuids.H264); + if (encoder != null) + { + IsHevc = false; + Logger.Log("Found H264 hardware encoder."); + } + else + { + Logger.Log("Error: No hardware video encoders found on this system."); + throw new Exception("No hardware video encoders found on this system."); + } + } + + try + { + var attributes = encoder.Attributes; + if (attributes != null) + { + // Force low latency mode to prevent the encoder from buffering frames + attributes.Set(new Guid("9c27891a-ed7a-40e1-88e8-b22727a024ee"), 1); // CODECAPI_AVEncCommonLowLatency + + // Force a keyframe every 30 frames (0.5 seconds) so late-joining clients can decode immediately + attributes.Set(new Guid("95f31b26-95a4-41aa-9303-246a7fc6eef1"), 30); // CODECAPI_AVEncMPVGOPSize + + attributes.Dispose(); + } + } + catch (Exception ex) + { + Logger.Log($"Warning: Could not set encoder attributes: {ex.Message}"); + } + + MediaType outType = new MediaType(); + outType.Set(MediaTypeAttributeKeys.MajorType, MediaTypeGuids.Video); + outType.Set(MediaTypeAttributeKeys.Subtype, IsHevc ? MFVideoFormat_HEVC : VideoFormatGuids.H264); + outType.Set(MediaTypeAttributeKeys.AvgBitrate, 10000000); // 10 Mbps + outType.Set(MediaTypeAttributeKeys.FrameSize, ((long)width << 32) | (uint)height); + outType.Set(MediaTypeAttributeKeys.FrameRate, ((long)fps << 32) | 1); + outType.Set(MediaTypeAttributeKeys.InterlaceMode, (int)VideoInterlaceMode.Progressive); + encoder.SetOutputType(0, outType, 0); + + MediaType inType = new MediaType(); + inType.Set(MediaTypeAttributeKeys.MajorType, MediaTypeGuids.Video); + inType.Set(MediaTypeAttributeKeys.Subtype, VideoFormatGuids.NV12); + inType.Set(MediaTypeAttributeKeys.FrameSize, ((long)width << 32) | (uint)height); + inType.Set(MediaTypeAttributeKeys.FrameRate, ((long)fps << 32) | 1); + inType.Set(MediaTypeAttributeKeys.InterlaceMode, (int)VideoInterlaceMode.Progressive); + encoder.SetInputType(0, inType, 0); + + encoder.ProcessMessage(TMessageType.NotifyBeginStreaming, IntPtr.Zero); + encoder.ProcessMessage(TMessageType.NotifyStartOfStream, IntPtr.Zero); + + Logger.Log("HardwareEncoder initialization complete."); + } + + private unsafe void FastScaleBgra(byte[] src, byte[] dst) + { + fixed (byte* pSrcFixed = src) + fixed (byte* pDstFixed = dst) + { + IntPtr ptrSrc = (IntPtr)pSrcFixed; + IntPtr ptrDst = (IntPtr)pDstFixed; + + Parallel.For(0, lastDstHeight, y => + { + byte* pSrc = (byte*)ptrSrc; + byte* pDst = (byte*)ptrDst; + + int srcYOffset = scaleYOffsets[y]; + byte* srcRow = pSrc + srcYOffset; + byte* dstRow = pDst + (y * lastDstWidth * 4); + + for (int x = 0; x < lastDstWidth; x++) + { + *((int*)(dstRow + x * 4)) = *((int*)(srcRow + scaleXOffsets[x])); + } + }); + } + } + + public ArraySegment EncodeFrame(byte[] bgraData, int srcWidth, int srcHeight, int dstWidth, int dstHeight, int screenLeft, int screenTop, CURSORINFO pci) + { + byte[] finalBgra = bgraData; + + if (srcWidth != dstWidth || srcHeight != dstHeight) + { + if (scaleXOffsets == null || lastSrcWidth != srcWidth || lastSrcHeight != srcHeight || lastDstWidth != dstWidth || lastDstHeight != dstHeight) + { + lastSrcWidth = srcWidth; + lastSrcHeight = srcHeight; + lastDstWidth = dstWidth; + lastDstHeight = dstHeight; + + scaleXOffsets = new int[dstWidth]; + scaleYOffsets = new int[dstHeight]; + + float xRatio = (float)srcWidth / dstWidth; + float yRatio = (float)srcHeight / dstHeight; + + for (int x = 0; x < dstWidth; x++) + scaleXOffsets[x] = (int)(x * xRatio) * 4; + + for (int y = 0; y < dstHeight; y++) + scaleYOffsets[y] = (int)(y * yRatio) * srcWidth * 4; + } + + if (scaledBgraBuffer == null || scaledBgraBuffer.Length != dstWidth * dstHeight * 4) + scaledBgraBuffer = new byte[dstWidth * dstHeight * 4]; + + FastScaleBgra(bgraData, scaledBgraBuffer); + finalBgra = scaledBgraBuffer; + } + + if (pci.flags == CURSOR_SHOWING) + { + int x = pci.ptScreenPos.X - screenLeft; + int y = pci.ptScreenPos.Y - screenTop; + + float cursorScale = dstHeight / 1080.0f; + int cWidth = (int)(32 * cursorScale); + int cHeight = (int)(32 * cursorScale); + + if (pci.hCursor != lastCursorHandle) + { + lastCursorHandle = pci.hCursor; + if (GetIconInfo(pci.hCursor, out ICONINFO iconInfo)) + { + int nativeWidth = 32; + if (iconInfo.hbmColor != IntPtr.Zero) + { + if (GetObject(iconInfo.hbmColor, Marshal.SizeOf(typeof(BITMAP)), out BITMAP bmp) != 0) + nativeWidth = bmp.bmWidth; + } + else if (iconInfo.hbmMask != IntPtr.Zero) + { + if (GetObject(iconInfo.hbmMask, Marshal.SizeOf(typeof(BITMAP)), out BITMAP bmp) != 0) + nativeWidth = bmp.bmWidth; + } + + float ratio = (float)cWidth / (nativeWidth == 0 ? 32 : nativeWidth); + lastCursorHotspotX = (int)(iconInfo.xHotspot * ratio); + lastCursorHotspotY = (int)(iconInfo.yHotspot * ratio); + + if (iconInfo.hbmColor != IntPtr.Zero) DeleteObject(iconInfo.hbmColor); + if (iconInfo.hbmMask != IntPtr.Zero) DeleteObject(iconInfo.hbmMask); + } + } + + x -= lastCursorHotspotX; + y -= lastCursorHotspotY; + + if (srcWidth != dstWidth || srcHeight != dstHeight) + { + float scaleX = (float)dstWidth / srcWidth; + float scaleY = (float)dstHeight / srcHeight; + x = (int)(x * scaleX); + y = (int)(y * scaleY); + } + + GCHandle handle = GCHandle.Alloc(finalBgra, GCHandleType.Pinned); + try + { + using (Bitmap targetBmp = new Bitmap(dstWidth, dstHeight, dstWidth * 4, PixelFormat.Format32bppArgb, handle.AddrOfPinnedObject())) + using (Graphics gTarget = Graphics.FromImage(targetBmp)) + { + IntPtr hdc = gTarget.GetHdc(); + DrawIconEx(hdc, x, y, pci.hCursor, cWidth, cHeight, 0, IntPtr.Zero, DI_NORMAL); + gTarget.ReleaseHdc(hdc); + } + } + finally + { + handle.Free(); + } + } + + var sample = MediaFactory.CreateSample(); + int nv12Length = (int)(dstWidth * dstHeight * 1.5); + var buffer = MediaFactory.CreateMemoryBuffer(nv12Length); + IntPtr ptr = buffer.Lock(out int maxLength, out int currentLength); + + // Write directly to the COM buffer, eliminating the Marshal.Copy entirely + BgraToNv12(finalBgra, ptr, dstWidth, dstHeight); + + buffer.Unlock(); + buffer.CurrentLength = nv12Length; + sample.AddBuffer(buffer); + sample.SampleTime = frameTime; + sample.SampleDuration = 10000000 / 60; + frameTime += sample.SampleDuration; + + try + { + encoder.ProcessInput(0, sample, 0); + } + catch (SharpDXException ex) + { + // If MF_E_NOTACCEPTING, the MFT is full and we must drain it first. + if (ex.ResultCode.Code != unchecked((int)0xC00D6D74) && ex.ResultCode.Code != unchecked((int)0xC00D36B5)) + { + Logger.Log($"ProcessInput failed with HRESULT: 0x{ex.ResultCode.Code:X}"); + throw; + } + } + finally + { + buffer.Dispose(); + sample.Dispose(); + } + + bool isKeyFrame = false; + outStream.SetLength(0); + + while (true) + { + outputDataBuffer[0] = new TOutputDataBuffer(); + outputDataBuffer[0].DwStreamID = 0; + + Sample outSample = null; + MediaBuffer outBuffer = null; + + encoder.GetOutputStreamInfo(0, out var osi); + // If the MFT does not provide samples, we must allocate them + if (((int)osi.DwFlags & 0x100) == 0) + { + outSample = MediaFactory.CreateSample(); + int cbSize = osi.CbSize > 0 ? osi.CbSize : (dstWidth * dstHeight); + outBuffer = MediaFactory.CreateMemoryBuffer(cbSize); + outSample.AddBuffer(outBuffer); + outputDataBuffer[0].PSample = outSample; + } + + bool hasOutput = false; + try + { + hasOutput = encoder.ProcessOutput(TransformProcessOutputFlags.None, outputDataBuffer, out var status); + } + catch (SharpDXException ex) + { + if (outSample != null) + { + outBuffer?.Dispose(); + outSample.Dispose(); + } + + if (ex.ResultCode.Code == unchecked((int)0xC00D6D72)) // MF_E_TRANSFORM_NEED_MORE_INPUT + break; + + Logger.Log($"ProcessOutput failed with HRESULT: 0x{ex.ResultCode.Code:X}"); + break; // Break on other errors to avoid infinite loop + } + + if (!hasOutput) + { + if (outSample != null) + { + outBuffer?.Dispose(); + outSample.Dispose(); + } + break; // MF_E_TRANSFORM_NEED_MORE_INPUT + } + + var resSample = outputDataBuffer[0].PSample; + if (resSample != null) + { + try + { + if (resSample.Get(SampleAttributeKeys.CleanPoint) != false) + { + isKeyFrame = true; + } + } + catch { } + + var buf = resSample.ConvertToContiguousBuffer(); + IntPtr outPtr = buf.Lock(out int outMax, out int outCur); + + // If this is the first chunk of the frame and it's a keyframe, prepend the sequence header + if (outStream.Length == 0 && sequenceHeader != null && sequenceHeader.Length > 0 && (isKeyFrame || !firstFrameEncoded)) + { + firstFrameEncoded = true; + outStream.Write(sequenceHeader, 0, sequenceHeader.Length); + } + + // Write directly from the unmanaged pointer to the stream using ReadOnlySpan, eliminating tempBuffer + unsafe + { + var span = new ReadOnlySpan(outPtr.ToPointer(), outCur); + outStream.Write(span); + } + + buf.Unlock(); + buf.Dispose(); + + if (outSample == null) resSample.Dispose(); + } + + if (outSample != null) + { + outBuffer?.Dispose(); + outSample.Dispose(); + } + + if (outputDataBuffer[0].PEvents != null) outputDataBuffer[0].PEvents.Dispose(); + } + + // Extract the SPS/PPS Sequence Header if we haven't already + if (sequenceHeader == null && !sequenceHeaderLogged) + { + try + { + encoder.GetOutputCurrentType(0, out MediaType encOutType); + if (encOutType != null) + { + sequenceHeader = encOutType.Get(MediaTypeAttributeKeys.MpegSequenceHeader); + encOutType.Dispose(); + if (sequenceHeader != null) + { + string hex = BitConverter.ToString(sequenceHeader.Take(16).ToArray()); + Logger.Log($"Extracted Sequence Header: {sequenceHeader.Length} bytes. Starts with: {hex}"); + } + } + } + catch { } + + if (sequenceHeader == null) + { + sequenceHeader = new byte[0]; + Logger.Log("Warning: MpegSequenceHeader not found. Relying on in-band SPS/PPS."); + } + sequenceHeaderLogged = true; + } + + return new ArraySegment(outStream.GetBuffer(), 0, (int)outStream.Length); + } + + private static unsafe void BgraToNv12(byte[] bgra, IntPtr pNv12Fixed, int width, int height) + { + int ySize = width * height; + fixed (byte* pBgraFixed = bgra) + { + IntPtr ptrBgra = (IntPtr)pBgraFixed; + + Parallel.For(0, height, y => + { + byte* pBgra = (byte*)ptrBgra; + byte* pNv12 = (byte*)pNv12Fixed; + + byte* bgraRow = pBgra + (y * width * 4); + byte* yRow = pNv12 + (y * width); + byte* uvRow = pNv12 + ySize + ((y >> 1) * width); + + for (int x = 0; x < width; x += 2) + { + // Pixel 1 + int b1 = bgraRow[0]; + int g1 = bgraRow[1]; + int r1 = bgraRow[2]; + + // Pixel 2 + int b2 = bgraRow[4]; + int g2 = bgraRow[5]; + int r2 = bgraRow[6]; + + bgraRow += 8; + + int yVal1 = ((66 * r1 + 129 * g1 + 25 * b1 + 128) >> 8) + 16; + yRow[x] = (byte)(yVal1 < 0 ? 0 : (yVal1 > 255 ? 255 : yVal1)); + + int yVal2 = ((66 * r2 + 129 * g2 + 25 * b2 + 128) >> 8) + 16; + yRow[x + 1] = (byte)(yVal2 < 0 ? 0 : (yVal2 > 255 ? 255 : yVal2)); + + // Subsample U and V once per 2x2 block (we calculate it on even rows and apply to 2 pixels) + if ((y & 1) == 0) + { + int uVal = ((-38 * r1 - 74 * g1 + 112 * b1 + 128) >> 8) + 128; + int vVal = ((112 * r1 - 94 * g1 - 18 * b1 + 128) >> 8) + 128; + uvRow[x] = (byte)(uVal < 0 ? 0 : (uVal > 255 ? 255 : uVal)); + uvRow[x + 1] = (byte)(vVal < 0 ? 0 : (vVal > 255 ? 255 : vVal)); + } + } + }); + } + } + + public void Dispose() + { + encoder?.Dispose(); + } + } + + // ========================================== + // HARDWARE DECODER (Media Foundation) + // ========================================== + public class HardwareDecoder : IDisposable + { + public static readonly Guid MFVideoFormat_HEVC = new Guid("24312C1E-2614-4696-8122-81B83B000000"); + private Transform decoder; + private long decodeFrameTime = 0; + private TOutputDataBuffer[] outputDataBuffer = new TOutputDataBuffer[1]; + + private Transform CreateDecoder(Guid subtype) + { + MFInterop.MFT_REGISTER_TYPE_INFO inInfo = new MFInterop.MFT_REGISTER_TYPE_INFO { guidMajorType = MediaTypeGuids.Video, guidSubtype = subtype }; + IntPtr pInInfo = Marshal.AllocHGlobal(Marshal.SizeOf(inInfo)); + Marshal.StructureToPtr(inInfo, pInInfo, false); + + IntPtr pppActivate = IntPtr.Zero; + int count = 0; + uint flags = 0x00000020 | 0x00000040; // MFT_ENUM_FLAG_HARDWARE | MFT_ENUM_FLAG_SORTANDFILTER + + int hr = MFInterop.MFTEnumEx(TransformCategoryGuids.VideoDecoder, flags, pInInfo, IntPtr.Zero, out pppActivate, out count); + Marshal.FreeHGlobal(pInInfo); + + Transform result = null; + if (hr == 0 && count > 0 && pppActivate != IntPtr.Zero) + { + IntPtr pActivate = Marshal.ReadIntPtr(pppActivate); + using (var activate = new Activate(pActivate)) + { + result = activate.ActivateObject(); + } + for (int i = 1; i < count; i++) + { + IntPtr pObj = Marshal.ReadIntPtr(pppActivate, i * IntPtr.Size); + if (pObj != IntPtr.Zero) Marshal.Release(pObj); + } + Marshal.FreeCoTaskMem(pppActivate); + } + return result; + } + + public HardwareDecoder(int width, int height, bool isHevc) + { + decoder = CreateDecoder(isHevc ? MFVideoFormat_HEVC : VideoFormatGuids.H264); + if (decoder == null) throw new Exception("No hardware video decoders found on this system."); + + try + { + var attributes = decoder.Attributes; + if (attributes != null) + { + attributes.Set(new Guid("9c27891a-ed7a-40e1-88e8-b22727a024ee"), 1); // CODECAPI_AVLowLatencyMode + attributes.Dispose(); + } + } + catch { } + + MediaType inType = new MediaType(); + inType.Set(MediaTypeAttributeKeys.MajorType, MediaTypeGuids.Video); + inType.Set(MediaTypeAttributeKeys.Subtype, isHevc ? MFVideoFormat_HEVC : VideoFormatGuids.H264); + inType.Set(MediaTypeAttributeKeys.FrameSize, ((long)width << 32) | (uint)height); + inType.Set(MediaTypeAttributeKeys.InterlaceMode, (int)VideoInterlaceMode.Progressive); + decoder.SetInputType(0, inType, 0); + + // We do not force an output type here. The decoder will trigger a stream change + // when it parses the first frame and knows the exact stride and format. + decoder.ProcessMessage(TMessageType.NotifyBeginStreaming, IntPtr.Zero); + decoder.ProcessMessage(TMessageType.NotifyStartOfStream, IntPtr.Zero); + } + + public bool DecodeFrame(byte[] data, int length, int width, int height, byte[] targetBgra) + { + var sample = MediaFactory.CreateSample(); + var buffer = MediaFactory.CreateMemoryBuffer(length); + IntPtr ptr = buffer.Lock(out int maxLength, out int currentLength); + Marshal.Copy(data, 0, ptr, length); + buffer.Unlock(); + buffer.CurrentLength = length; + sample.AddBuffer(buffer); + + // Decoders require valid timestamps to process frames + sample.SampleTime = decodeFrameTime; + sample.SampleDuration = 10000000 / 60; + decodeFrameTime += sample.SampleDuration; + + bool hasFrame = false; + bool inputAccepted = false; + while (!inputAccepted) + { + try + { + decoder.ProcessInput(0, sample, 0); + inputAccepted = true; + } + catch (SharpDXException ex) + { + if (ex.ResultCode.Code == unchecked((int)0xC00D36B5) || ex.ResultCode.Code == unchecked((int)0xC00D6D74)) + { + // Decoder is full. Drain it before trying to input again. + ProcessOutputLoop(width, height, targetBgra, ref hasFrame); + } + else + { + ClientLogger.Log($"ProcessInput failed: 0x{ex.ResultCode.Code:X}"); + buffer.Dispose(); + sample.Dispose(); + return hasFrame; + } + } + } + + buffer.Dispose(); + sample.Dispose(); + + // Drain the output produced by this input + ProcessOutputLoop(width, height, targetBgra, ref hasFrame); + + return hasFrame; + } + + private void ProcessOutputLoop(int width, int height, byte[] targetBgra, ref bool hasFrame) + { + while (true) + { + outputDataBuffer[0] = new TOutputDataBuffer(); + outputDataBuffer[0].DwStreamID = 0; + + Sample outSample = null; + MediaBuffer outBuffer = null; + + decoder.GetOutputStreamInfo(0, out var osi); + if (((int)osi.DwFlags & 0x100) == 0) + { + outSample = MediaFactory.CreateSample(); + int cbSize = osi.CbSize > 0 ? osi.CbSize : (int)(width * height * 1.5); + outBuffer = MediaFactory.CreateMemoryBuffer(cbSize); + outSample.AddBuffer(outBuffer); + outputDataBuffer[0].PSample = outSample; + } + + bool hasOutput = false; + try + { + hasOutput = decoder.ProcessOutput(TransformProcessOutputFlags.None, outputDataBuffer, out var status); + } + catch (SharpDXException ex) + { + if (outSample != null) + { + outBuffer?.Dispose(); + outSample.Dispose(); + } + + if (ex.ResultCode.Code == unchecked((int)0xC00D6D72)) // MF_E_TRANSFORM_NEED_MORE_INPUT + { + break; + } + + if (ex.ResultCode.Code == unchecked((int)0xC00D6D61) || ex.ResultCode.Code == unchecked((int)0xC00D6D60)) // STREAM_CHANGE or TYPE_NOT_SET + { + ClientLogger.Log($"ProcessOutput: STREAM_CHANGE or TYPE_NOT_SET detected (0x{ex.ResultCode.Code:X})."); + // Properly handle stream change by querying the decoder for its proposed output type + bool typeSet = false; + for (int i = 0; ; i++) + { + if (!decoder.TryGetOutputAvailableType(0, i, out MediaType availableType)) + break; + + if (availableType.Get(MediaTypeAttributeKeys.Subtype) == VideoFormatGuids.NV12) + { + decoder.SetOutputType(0, availableType, 0); + availableType.Dispose(); + typeSet = true; + ClientLogger.Log("ProcessOutput: Successfully set new NV12 output type."); + break; + } + availableType.Dispose(); + } + + if (!typeSet) + { + ClientLogger.Log("ProcessOutput: Fallback to forcing NV12 on first available type."); + // Fallback: force NV12 on the first available type + if (decoder.TryGetOutputAvailableType(0, 0, out MediaType firstType)) + { + firstType.Set(MediaTypeAttributeKeys.Subtype, VideoFormatGuids.NV12); + decoder.SetOutputType(0, firstType, 0); + firstType.Dispose(); + } + } + continue; + } + + ClientLogger.Log($"ProcessOutput failed: 0x{ex.ResultCode.Code:X}"); + break; + } + + if (!hasOutput) + { + if (outSample != null) + { + outBuffer?.Dispose(); + outSample.Dispose(); + } + break; // MF_E_TRANSFORM_NEED_MORE_INPUT + } + + var resSample = outputDataBuffer[0].PSample; + if (resSample != null) + { + var buf = resSample.ConvertToContiguousBuffer(); + IntPtr outPtr = buf.Lock(out int outMax, out int outCur); + + int stride = width; + try + { + decoder.GetOutputCurrentType(0, out MediaType currentType); + stride = Math.Abs(currentType.Get(MediaTypeAttributeKeys.DefaultStride)); + currentType.Dispose(); + } + catch { } + + // Hardware decoders often pad the height to a multiple of 16 or 32. + // We must calculate the true padded height to find the correct UV offset, + // otherwise we will read the bottom of the Y plane as color data (causing a green bar and ghosting). + int paddedHeight = (int)(outCur / (stride * 1.5)); + if (paddedHeight < height) paddedHeight = height; + int ySize = stride * paddedHeight; + + // Read directly from the COM buffer, eliminating the Marshal.Copy entirely + Nv12ToBgra(outPtr, targetBgra, width, height, stride, ySize); + hasFrame = true; + + buf.Unlock(); + buf.Dispose(); + + if (outSample == null) resSample.Dispose(); + } + + if (outSample != null) + { + outBuffer?.Dispose(); + outSample.Dispose(); + } + + if (outputDataBuffer[0].PEvents != null) + outputDataBuffer[0].PEvents.Dispose(); + } + } + + private static unsafe void Nv12ToBgra(IntPtr pNv12Fixed, byte[] bgra, int width, int height, int stride, int ySize) + { + fixed (byte* pBgraFixed = bgra) + { + IntPtr ptrBgra = (IntPtr)pBgraFixed; + + Parallel.For(0, height, y => + { + byte* pNv12 = (byte*)pNv12Fixed; + byte* pBgra = (byte*)ptrBgra; + + byte* yRow = pNv12 + (y * stride); + byte* uvRow = pNv12 + ySize + ((y >> 1) * stride); + byte* bgraRow = pBgra + (y * width * 4); + + for (int x = 0; x < width; x += 2) + { + int uVal = uvRow[x] - 128; + int vVal = uvRow[x + 1] - 128; + + int v_r = 1634 * vVal; + int uv_g = -833 * vVal - 400 * uVal; + int u_b = 2066 * uVal; + + // Pixel 1 + int yVal1 = yRow[x] - 16; + int y1192_1 = 1192 * yVal1; + + int r1 = (y1192_1 + v_r) >> 10; + int g1 = (y1192_1 + uv_g) >> 10; + int b1 = (y1192_1 + u_b) >> 10; + + bgraRow[0] = (byte)(b1 < 0 ? 0 : (b1 > 255 ? 255 : b1)); + bgraRow[1] = (byte)(g1 < 0 ? 0 : (g1 > 255 ? 255 : g1)); + bgraRow[2] = (byte)(r1 < 0 ? 0 : (r1 > 255 ? 255 : r1)); + bgraRow[3] = 255; + + // Pixel 2 + int yVal2 = yRow[x + 1] - 16; + int y1192_2 = 1192 * yVal2; + + int r2 = (y1192_2 + v_r) >> 10; + int g2 = (y1192_2 + uv_g) >> 10; + int b2 = (y1192_2 + u_b) >> 10; + + bgraRow[4] = (byte)(b2 < 0 ? 0 : (b2 > 255 ? 255 : b2)); + bgraRow[5] = (byte)(g2 < 0 ? 0 : (g2 > 255 ? 255 : g2)); + bgraRow[6] = (byte)(r2 < 0 ? 0 : (r2 > 255 ? 255 : r2)); + bgraRow[7] = 255; + + bgraRow += 8; + } + }); + } + } + + public void Dispose() + { + decoder?.Dispose(); + } + } +} \ No newline at end of file diff --git a/DisplayStreamer/ServerForm.Designer.cs b/DisplayStreamer/ServerForm.Designer.cs new file mode 100644 index 0000000..9afd25e --- /dev/null +++ b/DisplayStreamer/ServerForm.Designer.cs @@ -0,0 +1,194 @@ +namespace DisplayStreamer +{ + partial class ServerForm + { + private System.ComponentModel.IContainer components = null; + private System.Windows.Forms.Label lblAdapters; + private System.Windows.Forms.CheckedListBox clbAdapters; + private System.Windows.Forms.Label lblDisplays; + private System.Windows.Forms.CheckedListBox clbDisplays; + private System.Windows.Forms.Button btnStart; + private System.Windows.Forms.Button btnSwitchMode; + private System.Windows.Forms.Label lblStatus; + private System.Windows.Forms.CheckBox chkLogging; + private System.Windows.Forms.TextBox txtLog; + private System.Windows.Forms.NotifyIcon trayIcon; + private System.Windows.Forms.ContextMenuStrip trayMenu; + private System.Windows.Forms.ToolStripMenuItem menuShow; + private System.Windows.Forms.ToolStripMenuItem menuExit; + + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.lblAdapters = new System.Windows.Forms.Label(); + this.clbAdapters = new System.Windows.Forms.CheckedListBox(); + this.lblDisplays = new System.Windows.Forms.Label(); + this.clbDisplays = new System.Windows.Forms.CheckedListBox(); + this.btnStart = new System.Windows.Forms.Button(); + this.btnSwitchMode = new System.Windows.Forms.Button(); + this.lblStatus = new System.Windows.Forms.Label(); + this.chkLogging = new System.Windows.Forms.CheckBox(); + this.txtLog = new System.Windows.Forms.TextBox(); + this.trayIcon = new System.Windows.Forms.NotifyIcon(this.components); + this.trayMenu = new System.Windows.Forms.ContextMenuStrip(this.components); + this.menuShow = new System.Windows.Forms.ToolStripMenuItem(); + this.menuExit = new System.Windows.Forms.ToolStripMenuItem(); + this.trayMenu.SuspendLayout(); + this.SuspendLayout(); + // + // lblAdapters + // + this.lblAdapters.AutoSize = true; + this.lblAdapters.Location = new System.Drawing.Point(20, 20); + this.lblAdapters.Name = "lblAdapters"; + this.lblAdapters.Size = new System.Drawing.Size(205, 13); + this.lblAdapters.TabIndex = 0; + this.lblAdapters.Text = "Select Network Adapters to Broadcast on:"; + // + // clbAdapters + // + this.clbAdapters.FormattingEnabled = true; + this.clbAdapters.Location = new System.Drawing.Point(20, 40); + this.clbAdapters.Name = "clbAdapters"; + this.clbAdapters.Size = new System.Drawing.Size(440, 94); + this.clbAdapters.TabIndex = 1; + // + // lblDisplays + // + this.lblDisplays.AutoSize = true; + this.lblDisplays.Location = new System.Drawing.Point(20, 150); + this.lblDisplays.Name = "lblDisplays"; + this.lblDisplays.Size = new System.Drawing.Size(278, 13); + this.lblDisplays.TabIndex = 2; + this.lblDisplays.Text = "Select Displays to Capture (Will downscale 4K to 1080p):"; + // + // clbDisplays + // + this.clbDisplays.FormattingEnabled = true; + this.clbDisplays.Location = new System.Drawing.Point(20, 170); + this.clbDisplays.Name = "clbDisplays"; + this.clbDisplays.Size = new System.Drawing.Size(440, 94); + this.clbDisplays.TabIndex = 3; + // + // btnStart + // + this.btnStart.Font = new System.Drawing.Font("Arial", 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.btnStart.Location = new System.Drawing.Point(20, 290); + this.btnStart.Name = "btnStart"; + this.btnStart.Size = new System.Drawing.Size(210, 40); + this.btnStart.TabIndex = 4; + this.btnStart.Text = "Start Streaming"; + this.btnStart.UseVisualStyleBackColor = true; + this.btnStart.Click += new System.EventHandler(this.btnStart_Click); + // + // btnSwitchMode + // + this.btnSwitchMode.Font = new System.Drawing.Font("Arial", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.btnSwitchMode.Location = new System.Drawing.Point(250, 290); + this.btnSwitchMode.Name = "btnSwitchMode"; + this.btnSwitchMode.Size = new System.Drawing.Size(210, 40); + this.btnSwitchMode.TabIndex = 7; + this.btnSwitchMode.Text = "Switch to Client Mode"; + this.btnSwitchMode.UseVisualStyleBackColor = true; + this.btnSwitchMode.Click += new System.EventHandler(this.btnSwitchMode_Click); + // + // lblStatus + // + this.lblStatus.AutoSize = true; + this.lblStatus.ForeColor = System.Drawing.Color.Gray; + this.lblStatus.Location = new System.Drawing.Point(20, 340); + this.lblStatus.Name = "lblStatus"; + this.lblStatus.Size = new System.Drawing.Size(63, 13); + this.lblStatus.TabIndex = 5; + this.lblStatus.Text = "Status: Idle"; + // + // chkLogging + // + this.chkLogging.AutoSize = true; + this.chkLogging.Location = new System.Drawing.Point(350, 340); + this.chkLogging.Name = "chkLogging"; + this.chkLogging.Size = new System.Drawing.Size(100, 17); + this.chkLogging.TabIndex = 8; + this.chkLogging.Text = "Enable Logging"; + this.chkLogging.UseVisualStyleBackColor = true; + this.chkLogging.CheckedChanged += new System.EventHandler(this.chkLogging_CheckedChanged); + // + // txtLog + // + this.txtLog.Location = new System.Drawing.Point(20, 370); + this.txtLog.Multiline = true; + this.txtLog.Name = "txtLog"; + this.txtLog.ReadOnly = true; + this.txtLog.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; + this.txtLog.Size = new System.Drawing.Size(440, 210); + this.txtLog.TabIndex = 6; + // + // trayMenu + // + this.trayMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.menuShow, + this.menuExit}); + this.trayMenu.Name = "trayMenu"; + this.trayMenu.Size = new System.Drawing.Size(104, 48); + // + // menuShow + // + this.menuShow.Name = "menuShow"; + this.menuShow.Size = new System.Drawing.Size(103, 22); + this.menuShow.Text = "Show"; + this.menuShow.Click += new System.EventHandler(this.menuShow_Click); + // + // menuExit + // + this.menuExit.Name = "menuExit"; + this.menuExit.Size = new System.Drawing.Size(103, 22); + this.menuExit.Text = "Exit"; + this.menuExit.Click += new System.EventHandler(this.menuExit_Click); + // + // trayIcon + // + this.trayIcon.ContextMenuStrip = this.trayMenu; + this.trayIcon.Icon = System.Drawing.SystemIcons.Application; + this.trayIcon.Text = "Display Streamer Server"; + this.trayIcon.DoubleClick += new System.EventHandler(this.trayIcon_DoubleClick); + // + // ServerForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(484, 600); + this.Controls.Add(this.chkLogging); + this.Controls.Add(this.btnSwitchMode); + this.Controls.Add(this.txtLog); + this.Controls.Add(this.lblStatus); + this.Controls.Add(this.btnStart); + this.Controls.Add(this.clbDisplays); + this.Controls.Add(this.lblDisplays); + this.Controls.Add(this.clbAdapters); + this.Controls.Add(this.lblAdapters); + this.Name = "ServerForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Stream Server (DX12 / H.265)"; + this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.ServerForm_FormClosing); + this.Load += new System.EventHandler(this.ServerForm_Load); + this.Resize += new System.EventHandler(this.ServerForm_Resize); + this.trayMenu.ResumeLayout(false); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + } +} \ No newline at end of file diff --git a/DisplayStreamer/ServerForm.cs b/DisplayStreamer/ServerForm.cs new file mode 100644 index 0000000..f717e08 --- /dev/null +++ b/DisplayStreamer/ServerForm.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace DisplayStreamer +{ + public partial class ServerForm : Form + { + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags); + + [FlagsAttribute] + public enum EXECUTION_STATE : uint + { + ES_AWAYMODE_REQUIRED = 0x00000040, + ES_CONTINUOUS = 0x80000000, + ES_DISPLAY_REQUIRED = 0x00000002, + ES_SYSTEM_REQUIRED = 0x00000001 + } + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool SetProcessInformation(IntPtr hProcess, int processInformationClass, ref PROCESS_POWER_THROTTLING_STATE processInformation, uint processInformationSize); + + [StructLayout(LayoutKind.Sequential)] + private struct PROCESS_POWER_THROTTLING_STATE + { + public uint Version; + public uint ControlMask; + public uint StateMask; + } + + private UdpDiscoveryServer discoveryServer; + private UpdateServer updateServer; + private LogServer logServer; + private List streamServers = new List(); + private bool isStreaming = false; + + private Point normalLocation; + private bool isHiddenOffScreen = false; + + public ServerForm() + { + InitializeComponent(); + } + + private void ServerForm_Load(object sender, EventArgs e) + { + chkLogging.Checked = Program.LoggingEnabled; + Logger.OnLog += Logger_OnLog; + LoadHardware(); + } + + private void Logger_OnLog(string msg) + { + try + { + if (this.IsDisposed) return; + + if (this.InvokeRequired) + { + this.BeginInvoke(new Action(() => + { + try + { + if (!this.IsDisposed && this.Visible) + { + if (txtLog.TextLength > 10000) txtLog.Clear(); + txtLog.AppendText(msg + Environment.NewLine); + } + } + catch { } + })); + } + else + { + if (!this.IsDisposed && this.Visible) + { + if (txtLog.TextLength > 10000) txtLog.Clear(); + txtLog.AppendText(msg + Environment.NewLine); + } + } + } + catch { } + } + + private void LoadHardware() + { + foreach (var ni in NetworkInterface.GetAllNetworkInterfaces().Where(n => n.OperationalStatus == OperationalStatus.Up)) + { + var ip = ni.GetIPProperties().UnicastAddresses.FirstOrDefault(a => a.Address.AddressFamily == AddressFamily.InterNetwork); + if (ip != null) clbAdapters.Items.Add($"{ni.Name} ({ip.Address})", true); + } + + for (int i = 0; i < Screen.AllScreens.Length; i++) + { + var s = Screen.AllScreens[i]; + clbDisplays.Items.Add($"Display {i + 1}: {s.Bounds.Width}x{s.Bounds.Height}", true); + } + } + + private void btnStart_Click(object sender, EventArgs e) + { + if (!isStreaming) + { + if (clbDisplays.CheckedIndices.Count == 0) + { + MessageBox.Show("Select at least one display."); + return; + } + + // Elevate process priority to prevent Windows from throttling the app when minimized or out of focus + Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High; + + // Disable Windows Power Throttling (EcoQoS) and Timer Resolution ignoring for this process + try + { + PROCESS_POWER_THROTTLING_STATE state = new PROCESS_POWER_THROTTLING_STATE + { + Version = 1, + ControlMask = 0x1 | 0x4, // EXECUTION_SPEED | IGNORE_TIMER_RESOLUTION + StateMask = 0 // Turn off throttling + }; + SetProcessInformation(Process.GetCurrentProcess().Handle, 77, ref state, (uint)Marshal.SizeOf(state)); + } + catch { } + + // Prevent the system from sleeping or turning off the display while streaming + SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS | EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_DISPLAY_REQUIRED); + + List ips = new List(); + foreach (var item in clbAdapters.CheckedItems) + { + string ip = item.ToString().Split('(')[1].TrimEnd(')'); + ips.Add(ip); + } + + int basePort = 20000; + List> endpoints = new List>(); + + // Start a dedicated stream server for each selected display + foreach (int idx in clbDisplays.CheckedIndices) + { + var srv = new StreamServer(idx, basePort); + srv.Start(); + streamServers.Add(srv); + endpoints.Add(new Tuple($"Display {idx + 1}", basePort)); + basePort++; + } + + // Start the update server on port 21000 + updateServer = new UpdateServer(); + updateServer.Start(21000); + + // Start the log server on port 22000 + logServer = new LogServer(); + logServer.Start(22000); + + discoveryServer = new UdpDiscoveryServer(ips, endpoints); + discoveryServer.Start(); + + isStreaming = true; + btnStart.Text = "Stop Streaming"; + lblStatus.Text = $"Status: Broadcasting {streamServers.Count} display(s)"; + lblStatus.ForeColor = Color.Green; + } + else + { + discoveryServer?.Stop(); + updateServer?.Stop(); + logServer?.Stop(); + foreach (var srv in streamServers) srv.Stop(); + streamServers.Clear(); + + // Restore normal priority and allow sleep + Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.Normal; + SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS); + + isStreaming = false; + btnStart.Text = "Start Streaming"; + lblStatus.Text = "Status: Idle"; + lblStatus.ForeColor = Color.Gray; + } + } + + private void btnSwitchMode_Click(object sender, EventArgs e) + { + if (isStreaming) + { + btnStart_Click(null, null); + } + Program.NextForm = new ClientForm(); + this.Close(); + } + + private void chkLogging_CheckedChanged(object sender, EventArgs e) + { + Program.LoggingEnabled = chkLogging.Checked; + } + + private void ServerForm_Resize(object sender, EventArgs e) + { + if (this.WindowState == FormWindowState.Minimized) + { + // Bypass Windows deep-sleep throttling for minimized windows by moving it off-screen instead of hiding it + this.WindowState = FormWindowState.Normal; + normalLocation = this.Location; + this.Location = new Point(-32000, -32000); + this.ShowInTaskbar = false; + trayIcon.Visible = true; + isHiddenOffScreen = true; + } + } + + private void trayIcon_DoubleClick(object sender, EventArgs e) + { + RestoreWindow(); + } + + private void menuShow_Click(object sender, EventArgs e) + { + RestoreWindow(); + } + + private void menuExit_Click(object sender, EventArgs e) + { + this.Close(); + } + + private void RestoreWindow() + { + if (isHiddenOffScreen) + { + this.Location = normalLocation; + this.ShowInTaskbar = true; + trayIcon.Visible = false; + this.Activate(); + isHiddenOffScreen = false; + } + } + + private void ServerForm_FormClosing(object sender, FormClosingEventArgs e) + { + discoveryServer?.Stop(); + updateServer?.Stop(); + logServer?.Stop(); + foreach (var srv in streamServers) srv.Stop(); + SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS); + } + } +} \ No newline at end of file diff --git a/DisplayStreamer/ServerForm.resx b/DisplayStreamer/ServerForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/DisplayStreamer/ServerForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/DisplayStreamer/ViewerForm.Designer.cs b/DisplayStreamer/ViewerForm.Designer.cs new file mode 100644 index 0000000..8746a83 --- /dev/null +++ b/DisplayStreamer/ViewerForm.Designer.cs @@ -0,0 +1,84 @@ +namespace DisplayStreamer +{ + partial class ViewerForm + { + private System.ComponentModel.IContainer components = null; + private System.Windows.Forms.Label lblEsc; + private System.Windows.Forms.Label lblMetrics; + private System.Windows.Forms.Timer hideLabelTimer; + + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + lblEsc = new Label(); + lblMetrics = new Label(); + hideLabelTimer = new System.Windows.Forms.Timer(components); + SuspendLayout(); + // + // lblEsc + // + lblEsc.AutoSize = true; + lblEsc.BackColor = Color.FromArgb(128, 0, 0, 0); + lblEsc.Font = new Font("Arial", 14.25F, FontStyle.Bold, GraphicsUnit.Point, 0); + lblEsc.ForeColor = Color.White; + lblEsc.Location = new Point(0, 0); + lblEsc.Margin = new Padding(6, 0, 6, 0); + lblEsc.Name = "lblEsc"; + lblEsc.Size = new Size(419, 40); + lblEsc.TabIndex = 1; + lblEsc.Text = "Press ESC to disconnect"; + // + // lblMetrics + // + lblMetrics.AutoSize = true; + lblMetrics.BackColor = Color.FromArgb(180, 0, 0, 0); + lblMetrics.Font = new Font("Consolas", 14.25F, FontStyle.Bold, GraphicsUnit.Point, 0); + lblMetrics.ForeColor = Color.Lime; + lblMetrics.Location = new Point(0, 0); + lblMetrics.Margin = new Padding(6, 0, 6, 0); + lblMetrics.Name = "lblMetrics"; + lblMetrics.Size = new Size(0, 40); + lblMetrics.TabIndex = 2; + lblMetrics.Visible = false; + // + // hideLabelTimer + // + hideLabelTimer.Interval = 3000; + hideLabelTimer.Tick += hideLabelTimer_Tick; + // + // ViewerForm + // + AutoScaleDimensions = new SizeF(12F, 30F); + AutoScaleMode = AutoScaleMode.Font; + BackColor = Color.Black; + ClientSize = new Size(1600, 1038); + Controls.Add(lblMetrics); + Controls.Add(lblEsc); + FormBorderStyle = FormBorderStyle.None; + KeyPreview = true; + Margin = new Padding(6, 7, 6, 7); + Name = "ViewerForm"; + Text = "ViewerForm"; + WindowState = FormWindowState.Maximized; + FormClosing += ViewerForm_FormClosing; + Load += ViewerForm_Load; + KeyDown += ViewerForm_KeyDown; + ResumeLayout(false); + PerformLayout(); + + } + + #endregion + } +} \ No newline at end of file diff --git a/DisplayStreamer/ViewerForm.cs b/DisplayStreamer/ViewerForm.cs new file mode 100644 index 0000000..20547ad --- /dev/null +++ b/DisplayStreamer/ViewerForm.cs @@ -0,0 +1,91 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace DisplayStreamer +{ + public partial class ViewerForm : Form + { + [DllImport("user32.dll")] + private static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, int dwExtraInfo); + + private string serverIp; + private int serverPort; + private StreamClient streamClient; + + public ViewerForm() + { + InitializeComponent(); + this.Shown += ViewerForm_Shown; + } + + public ViewerForm(string ip, int port) : this() + { + serverIp = ip; + serverPort = port; + } + + private void ViewerForm_Load(object sender, EventArgs e) + { + // Force true fullscreen on the current monitor, covering the taskbar + this.FormBorderStyle = FormBorderStyle.None; + this.WindowState = FormWindowState.Normal; + this.Bounds = Screen.FromControl(this).Bounds; + this.TopMost = true; + + // Hide the local host cursor while viewing the stream + Cursor.Hide(); + + hideLabelTimer.Start(); + + if (!string.IsNullOrEmpty(serverIp)) + { + // Pass 'this' (the form itself) to the StreamClient so it can bind the D3D11 SwapChain to it + streamClient = new StreamClient(serverIp, serverPort, this, lblMetrics); + streamClient.Start(); + } + } + + private void ViewerForm_Shown(object sender, EventArgs e) + { + // Simulate a momentary ALT key press to bypass Windows foreground lock restrictions. + // This allows a background-launched process (like our auto-updater batch script) to steal focus. + keybd_event(0x12, 0, 0, 0); // VK_MENU (ALT) down + keybd_event(0x12, 0, 0x0002, 0); // VK_MENU (ALT) up + + SetForegroundWindow(this.Handle); + this.Activate(); + this.Focus(); + } + + private void hideLabelTimer_Tick(object sender, EventArgs e) + { + lblEsc.Visible = false; + hideLabelTimer.Stop(); + } + + private void ViewerForm_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Escape) + { + this.Close(); + } + else if (e.KeyCode == Keys.F1) + { + lblMetrics.Visible = !lblMetrics.Visible; + } + } + + private void ViewerForm_FormClosing(object sender, FormClosingEventArgs e) + { + // Restore the local host cursor when exiting the viewer + Cursor.Show(); + + streamClient?.Stop(); + } + } +} \ No newline at end of file diff --git a/DisplayStreamer/ViewerForm.resx b/DisplayStreamer/ViewerForm.resx new file mode 100644 index 0000000..a1a7e35 --- /dev/null +++ b/DisplayStreamer/ViewerForm.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + \ No newline at end of file