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