From 7c49107200fcd128cc478300328d2307da27e482 Mon Sep 17 00:00:00 2001 From: BigSoulja Date: Fri, 27 Sep 2024 09:58:05 +0100 Subject: [PATCH 1/4] Fixed the issue where certain things like UAC prompts, Fullsreen games, resolution changes etc. would stop the capture from working and require a manual restart. The system should now reliably re-initialize itself. Would recommend disabling sidebar notifications for this program in the windows sidebar as it will pop up a connection notification whenever this happens. --- .../Capture/Dx11ScreenCapture.cs | 227 +++++++++++++++--- HyperionScreenCap/Form/MainForm.cs | 55 ++++- HyperionScreenCap/Helper/HyperionTask.cs | 125 +++++++--- HyperionScreenCap/Helper/UpdateChecker.cs | 16 +- HyperionScreenCap/HyperionScreenCap.csproj | 38 +-- HyperionScreenCap/Networking/FbsClinet.cs | 19 ++ .../Networking/HyperionClient.cs | 26 +- HyperionScreenCap/Networking/ProtoClient.cs | 18 ++ HyperionScreenCap/Networking/fbs/RawImage.cs | 34 ++- HyperionScreenCap/app.config | 14 +- HyperionScreenCap/packages.config | 17 +- 11 files changed, 464 insertions(+), 125 deletions(-) diff --git a/HyperionScreenCap/Capture/Dx11ScreenCapture.cs b/HyperionScreenCap/Capture/Dx11ScreenCapture.cs index 16ac024..e5baeb2 100644 --- a/HyperionScreenCap/Capture/Dx11ScreenCapture.cs +++ b/HyperionScreenCap/Capture/Dx11ScreenCapture.cs @@ -12,6 +12,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading; +using log4net; namespace HyperionScreenCap { @@ -41,9 +42,15 @@ class DX11ScreenCapture : IScreenCapture private bool _desktopDuplicatorInvalid; private bool _disposed; + private int _reinitializationAttempts = 0; + private const int MAX_REINITIALIZATION_ATTEMPTS = 5; + private const int REINITIALIZATION_DELAY_MS = 1000; + public int CaptureWidth { get; private set; } public int CaptureHeight { get; private set; } + private static readonly ILog LOG = LogManager.GetLogger(typeof(DX11ScreenCapture)); + public static String GetAvailableMonitors() { StringBuilder response = new StringBuilder(); @@ -78,31 +85,73 @@ public DX11ScreenCapture(int adapterIndex, int monitorIndex, int scalingFactor, public void Initialize() { + Dispose(); + + int retryCount = 0; + const int maxRetries = 5; + const int retryDelay = 1000; // 1 second + + while (retryCount < maxRetries) + { + try + { + InitializeInternal(); + return; // If successful, exit the method + } + catch (SharpDXException ex) + { + LOG.Error($"SharpDX exception during initialization (attempt {retryCount + 1}/{maxRetries}): {ex.Message}"); + retryCount++; + if (retryCount >= maxRetries) + { + throw new Exception("Failed to initialize DX11 screen capture after multiple attempts", ex); + } + Thread.Sleep(retryDelay); + } + catch (Exception ex) + { + LOG.Error($"Unexpected exception during initialization: {ex.Message}"); + throw; + } + } + InitDesktopDuplicator(); + } + + private void InitializeInternal() + { + // Move all the existing initialization code here int mipLevels; - if ( _scalingFactor == 1 ) + if (_scalingFactor == 1) mipLevels = 1; - else if ( _scalingFactor > 0 && _scalingFactor % 2 == 0 ) + else if (_scalingFactor > 0 && _scalingFactor % 2 == 0) { - /// Mip level for a scaling factor other than one is computed as follows: - /// 2^n = 2 + n - 1 where LHS is the scaling factor and RHS is the MipLevels value. _scalingFactorLog2 = Convert.ToInt32(Math.Log(_scalingFactor, 2)); mipLevels = 2 + _scalingFactorLog2 - 1; } else - throw new Exception("Invalid scaling factor. Allowed valued are 1, 2, 4, etc."); + throw new Exception("Invalid scaling factor. Allowed values are 1, 2, 4, etc."); + + _factory?.Dispose(); + _adapter?.Dispose(); + _output?.Dispose(); + _output1?.Dispose(); + _device?.Dispose(); + _stagingTexture?.Dispose(); + _smallerTexture?.Dispose(); + _smallerTextureView?.Dispose(); - // Create DXGI Factory1 _factory = new Factory1(); _adapter = _factory.GetAdapter1(_adapterIndex); - // Create device from Adapter _device = new SharpDX.Direct3D11.Device(_adapter); - // Get DXGI.Output _output = _adapter.GetOutput(_monitorIndex); + if (_output == null) + { + throw new Exception($"No output found for adapter {_adapterIndex} and monitor {_monitorIndex}"); + } _output1 = _output.QueryInterface(); - // Width/Height of desktop to capture var desktopBounds = _output.Description.DesktopBounds; _width = desktopBounds.Right - desktopBounds.Left; _height = desktopBounds.Bottom - desktopBounds.Top; @@ -110,7 +159,6 @@ public void Initialize() CaptureWidth = _width / _scalingFactor; CaptureHeight = _height / _scalingFactor; - // Create Staging texture CPU-accessible var stagingTextureDesc = new Texture2DDescription { CpuAccessFlags = CpuAccessFlags.Read, @@ -126,7 +174,6 @@ public void Initialize() }; _stagingTexture = new Texture2D(_device, stagingTextureDesc); - // Create smaller texture to downscale the captured image var smallerTextureDesc = new Texture2DDescription { CpuAccessFlags = CpuAccessFlags.None, @@ -152,25 +199,123 @@ public void Initialize() private void InitDesktopDuplicator() { - // Duplicate the output - _duplicatedOutput = _output1.DuplicateOutput(_device); + try + { + _duplicatedOutput?.Dispose(); + _duplicatedOutput = _output1.DuplicateOutput(_device); + _desktopDuplicatorInvalid = false; + LOG.Debug("Desktop duplicator initialized successfully."); + } + catch (SharpDXException ex) + { + LOG.Error($"SharpDXException in InitDesktopDuplicator: {ex.Message}", ex); + _desktopDuplicatorInvalid = true; + throw; + } + } + + private void ReinitializeOutputAndDevice() + { + LOG.Debug("Reinitializing Output and Device..."); + try + { + _factory?.Dispose(); + _adapter?.Dispose(); + _output?.Dispose(); + _output1?.Dispose(); + _device?.Dispose(); + + _factory = new Factory1(); + if (_factory == null) throw new InvalidOperationException("Failed to create Factory1"); + + _adapter = _factory.GetAdapter1(_adapterIndex); + if (_adapter == null) throw new InvalidOperationException($"No adapter found for index {_adapterIndex}"); + + _device = new SharpDX.Direct3D11.Device(_adapter); + if (_device == null) throw new InvalidOperationException("Failed to create Device"); - _desktopDuplicatorInvalid = false; + _output = _adapter.GetOutput(_monitorIndex); + if (_output == null) throw new InvalidOperationException($"No output found for adapter {_adapterIndex} and monitor {_monitorIndex}"); + + _output1 = _output.QueryInterface(); + if (_output1 == null) throw new InvalidOperationException("Failed to query Output1 interface"); + + LOG.Info("Successfully reinitialized Output and Device."); + } + catch (Exception ex) + { + LOG.Error($"Failed to reinitialize Output and Device: {ex.Message}", ex); + throw; + } + } + + private void EnsureDeviceInitialized() + { + if (_device == null || _device.IsDisposed) + { + LOG.Warn("Device is null or disposed. Attempting to reinitialize..."); + try + { + ReinitializeOutputAndDevice(); + } + catch (Exception ex) + { + LOG.Error($"Failed to reinitialize device: {ex.Message}", ex); + throw new InvalidOperationException("Failed to reinitialize device", ex); + } + } } public byte[] Capture() { - if ( _desktopDuplicatorInvalid ) + if (_desktopDuplicatorInvalid) { - _duplicatedOutput?.Dispose(); - InitDesktopDuplicator(); + LOG.Warn("Desktop duplicator is invalid. Attempting to reinitialize..."); + if (!ReinitializeDesktopDuplicator()) + { + throw new InvalidOperationException("Failed to reinitialize desktop duplicator after multiple attempts"); + } } _captureTimer.Restart(); - byte[] response = ManagedCapture(); - _captureTimer.Stop(); + try + { + byte[] response = ManagedCapture(); + _captureTimer.Stop(); + _reinitializationAttempts = 0; // Reset the counter on successful capture + return response; + } + catch (InvalidOperationException ex) + { + LOG.Error($"Capture failed: {ex.Message}", ex); + _desktopDuplicatorInvalid = true; + throw; + } + } - return response; + private bool ReinitializeDesktopDuplicator() + { + while (_reinitializationAttempts < MAX_REINITIALIZATION_ATTEMPTS) + { + try + { + _reinitializationAttempts++; + LOG.Info($"Reinitialization attempt {_reinitializationAttempts} of {MAX_REINITIALIZATION_ATTEMPTS}"); + + // Only reinitialize the desktop duplicator + InitDesktopDuplicator(); + + _desktopDuplicatorInvalid = false; + LOG.Info("Desktop duplicator reinitialized successfully."); + return true; + } + catch (Exception ex) + { + LOG.Error($"Failed to reinitialize desktop duplicator: {ex.Message}", ex); + Thread.Sleep(REINITIALIZATION_DELAY_MS * _reinitializationAttempts); + } + } + return false; } private byte[] ManagedCapture() @@ -180,31 +325,42 @@ private byte[] ManagedCapture() try { + EnsureDeviceInitialized(); + try { // Try to get duplicated frame within given time _duplicatedOutput.AcquireNextFrame(_frameCaptureTimeout, out duplicateFrameInformation, out screenResource); - if ( duplicateFrameInformation.LastPresentTime == 0 && _lastCapturedFrame != null ) + if (duplicateFrameInformation.LastPresentTime == 0 && _lastCapturedFrame != null) return _lastCapturedFrame; } - catch ( SharpDXException ex ) + catch (SharpDXException ex) { - if ( ex.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Code && _lastCapturedFrame != null ) + if (ex.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Code && _lastCapturedFrame != null) return _lastCapturedFrame; - if ( ex.ResultCode.Code == SharpDX.DXGI.ResultCode.AccessLost.Code ) + if (ex.ResultCode.Code == SharpDX.DXGI.ResultCode.AccessLost.Code) + { _desktopDuplicatorInvalid = true; + throw new InvalidOperationException("Desktop duplicator access lost", ex); + } - throw ex; + throw; } // Check if scaling is used - if ( CaptureWidth != _width ) + if (CaptureWidth != _width) { // Copy resource into memory that can be accessed by the CPU - using ( var screenTexture2D = screenResource.QueryInterface() ) + using (var screenTexture2D = screenResource.QueryInterface()) + { + if (_device == null || _device.ImmediateContext == null) + { + throw new InvalidOperationException("Device or ImmediateContext is null"); + } _device.ImmediateContext.CopySubresourceRegion(screenTexture2D, 0, null, _smallerTexture, 0); + } // Generates the mipmap of the screen _device.ImmediateContext.GenerateMips(_smallerTextureView); @@ -215,8 +371,14 @@ private byte[] ManagedCapture() else { // Copy resource into memory that can be accessed by the CPU - using ( var screenTexture2D = screenResource.QueryInterface() ) + using (var screenTexture2D = screenResource.QueryInterface()) + { + if (_device == null || _device.ImmediateContext == null) + { + throw new InvalidOperationException("Device or ImmediateContext is null"); + } _device.ImmediateContext.CopyResource(screenTexture2D, _stagingTexture); + } } // Get the desktop capture texture @@ -224,13 +386,18 @@ private byte[] ManagedCapture() _lastCapturedFrame = ToRGBArray(mapSource); return _lastCapturedFrame; } + catch (Exception ex) + { + LOG.Error($"Error in ManagedCapture: {ex.Message}", ex); + throw; + } finally { screenResource?.Dispose(); // Fixed OUT_OF_MEMORY issue on AMD Radeon cards. Ignoring all exceptions during unmapping. - try { _device.ImmediateContext.UnmapSubresource(_stagingTexture, 0); } catch { }; + try { _device?.ImmediateContext?.UnmapSubresource(_stagingTexture, 0); } catch { }; // Ignore DXGI_ERROR_INVALID_CALL, DXGI_ERROR_ACCESS_LOST errors since capture is already complete - try { _duplicatedOutput.ReleaseFrame(); } catch { } + try { _duplicatedOutput?.ReleaseFrame(); } catch { } } } diff --git a/HyperionScreenCap/Form/MainForm.cs b/HyperionScreenCap/Form/MainForm.cs index 2cb9121..e8a3161 100644 --- a/HyperionScreenCap/Form/MainForm.cs +++ b/HyperionScreenCap/Form/MainForm.cs @@ -254,14 +254,16 @@ private void EnableCapture() } LOG.Info($"Enabling {SettingsManager.HyperionTaskConfigurations.Count} screen capture(s)"); - foreach ( HyperionTaskConfiguration configuration in SettingsManager.HyperionTaskConfigurations) + foreach (HyperionTaskConfiguration configuration in SettingsManager.HyperionTaskConfigurations) { if (configuration.Enabled) { HyperionTask hyperionTask = new HyperionTask(configuration, _notificationUtils); + hyperionTask.OnCaptureDisabled += HyperionTask_OnCaptureDisabled; hyperionTask.EnableCapture(); _hyperionTasks.Add(hyperionTask); - } else + } + else { LOG.Info($"Capture task with ID {configuration.Id} is disabled. Skipping."); } @@ -271,6 +273,20 @@ private void EnableCapture() LOG.Info($"Enabled {_hyperionTasks.Count} screen capture(s)"); } + private void HyperionTask_OnCaptureDisabled(object sender, EventArgs e) + { + if (InvokeRequired) + { + Invoke(new Action(() => HyperionTask_OnCaptureDisabled(sender, e))); + return; + } + + LOG.Warn("Capture disabled event received. Attempting to restart capture."); + ToggleCapture(CaptureCommand.OFF); + Thread.Sleep(2000); // Wait a bit before restarting + ToggleCapture(CaptureCommand.ON); + } + private void DisableCapture() { LOG.Info($"Disabling {_hyperionTasks.Count} screen capture(s)"); @@ -285,19 +301,40 @@ private void DisableCapture() private void DisableCaptureOnFailure() { - while ( CaptureEnabled ) + int consecutiveFailures = 0; + while (CaptureEnabled) { - foreach ( HyperionTask task in _hyperionTasks ) + bool allTasksFailed = true; + foreach (HyperionTask task in _hyperionTasks) { - if ( !task.CaptureEnabled ) + if (task.CaptureEnabled) { - // We have found a task for which capture has been disabled due to failure - // Turning off capture and exiting this thread - LOG.Error($"Found {task} with capture disabled due to failure. Issuing OFF command."); + allTasksFailed = false; + } + else + { + LOG.Error($"Found {task} with capture disabled due to failure. Attempting to restart."); + task.RestartCapture(); + } + } + + if (allTasksFailed) + { + consecutiveFailures++; + if (consecutiveFailures > 5) // Arbitrary number, adjust as needed + { + LOG.Error("All tasks failed consecutively. Performing full restart."); ToggleCapture(CaptureCommand.OFF, false, false); - return; + Thread.Sleep(2000); // Wait a bit before restarting + ToggleCapture(CaptureCommand.ON, false, false); + consecutiveFailures = 0; } } + else + { + consecutiveFailures = 0; + } + Thread.Sleep(AppConstants.CAPTURE_FAILURE_DETECTION_INTERVAL); } } diff --git a/HyperionScreenCap/Helper/HyperionTask.cs b/HyperionScreenCap/Helper/HyperionTask.cs index d7780fd..0cb7001 100644 --- a/HyperionScreenCap/Helper/HyperionTask.cs +++ b/HyperionScreenCap/Helper/HyperionTask.cs @@ -21,6 +21,8 @@ class HyperionTask // TODO: Remove notifications from here public bool CaptureEnabled { get; private set; } private Thread _captureThread; + public event EventHandler OnCaptureDisabled; + public HyperionTask(HyperionTaskConfiguration configuration, NotificationUtils notificationUtils) { this._configuration = configuration; @@ -103,35 +105,55 @@ private void DisposeHyperionClients() private void ConnectHyperionClients() { - foreach ( HyperionClient hyperionClient in _hyperionClients ) + foreach (HyperionClient hyperionClient in _hyperionClients) { - if ( hyperionClient.IsConnected() ) - { - // Hyperion client already initialized. Ignoring request. - return; - } try { LOG.Info($"{this}: Connecting {hyperionClient}"); - hyperionClient?.Dispose(); - // TODO: check for memory leak in each of the Hyperion Clients - hyperionClient.Connect(); - // Double checking since sometimes exceptions are not thrown even if connection fails - if ( hyperionClient.IsConnected() ) + hyperionClient.Dispose(); // Ensure any existing connection is closed + hyperionClient.Connect(); // This will now send registration + if (hyperionClient.IsConnected()) { LOG.Info($"{this}: {hyperionClient} connected"); _notificationUtils.Info($"Connected to Hyperion server using {hyperionClient}!"); + + // Only send initial frame if screen capture is initialized + if (_screenCapture != null) + { + hyperionClient.SendInitialFrame(_screenCapture.CaptureWidth, _screenCapture.CaptureHeight); + + // Send an actual captured frame immediately + byte[] initialFrame = CaptureInitialFrame(); + hyperionClient.SendImageData(initialFrame, _screenCapture.CaptureWidth, _screenCapture.CaptureHeight); + } } else + { throw new Exception(GetHyperionInitFailedMsg(hyperionClient)); + } } - catch ( Exception ex ) + catch (Exception ex) { - throw new Exception(GetHyperionInitFailedMsg(hyperionClient), ex); + LOG.Error($"{this}: Failed to connect to Hyperion server: {ex.Message}", ex); + throw; } } } + private byte[] CaptureInitialFrame() + { + try + { + return _screenCapture.Capture(); + } + catch (Exception ex) + { + LOG.Error($"{this}: Failed to capture initial frame: {ex.Message}", ex); + // Return a black frame as a fallback + return new byte[_screenCapture.CaptureWidth * _screenCapture.CaptureHeight * 3]; + } + } + private void TransmitNextFrame() { foreach ( HyperionClient hyperionClient in _hyperionClients ) @@ -151,45 +173,72 @@ private void TransmitNextFrame() } } + public void RestartCapture() + { + DisableCapture(); + Thread.Sleep(1000); // Wait a bit before restarting + EnableCapture(); + } + private void StartCapture() { InstantiateScreenCapture(); InstantiateHyperionClients(); int captureAttempt = 1; - while ( CaptureEnabled ) + while (CaptureEnabled) { - try // This block will help retry capture before giving up + try { InitScreenCapture(); ConnectHyperionClients(); - TransmitNextFrame(); - _screenCapture.DelayNextCapture(); - captureAttempt = 1; // Reset attempt count + while (CaptureEnabled) + { + TransmitNextFrame(); + _screenCapture.DelayNextCapture(); + } } - catch ( Exception ex ) + catch (Exception ex) { LOG.Error($"{this}: Exception in screen capture attempt: {captureAttempt}", ex); - if ( captureAttempt > AppConstants.REINIT_CAPTURE_AFTER_ATTEMPTS ) + if (captureAttempt > AppConstants.REINIT_CAPTURE_AFTER_ATTEMPTS) { - // After a few attempt, try disposing screen capture object as well - _screenCapture?.Dispose(); - LOG.Info($"{this}: Will re-initialize screen capture on retry"); + LOG.Info($"{this}: Attempting to recreate screen capture object and reconnect to Hyperion"); + RecreateScreenCaptureAndReconnect(); + return; // Exit this method, as a new capture thread has been started } - if ( ++captureAttempt == AppConstants.MAX_CAPTURE_ATTEMPTS ) + Thread.Sleep(AppConstants.CAPTURE_FAILED_COOLDOWN_MILLIS); + captureAttempt++; + + if (captureAttempt > AppConstants.MAX_CAPTURE_ATTEMPTS) { - LOG.Error($"{this}: Max screen capture attempts reached. Giving up."); - _notificationUtils.Error(ex.Message); + LOG.Error($"{this}: Max screen capture attempts reached. Disabling capture."); CaptureEnabled = false; - } - else - { - LOG.Info($"{this}: Waiting before next screen capture attempt"); - Thread.Sleep(AppConstants.CAPTURE_FAILED_COOLDOWN_MILLIS); + OnCaptureDisabled?.Invoke(this, EventArgs.Empty); } } } } + private void RecreateScreenCaptureAndReconnect() + { + LOG.Info($"{this}: Recreating screen capture and reconnecting to Hyperion"); + + // Dispose of existing resources + _screenCapture?.Dispose(); + _screenCapture = null; + + foreach (var client in _hyperionClients) + { + client.Dispose(); + } + _hyperionClients.Clear(); + + // Wait a bit before reconnecting + Thread.Sleep(2000); + + // Let StartCapture handle the reinitialization + } + private void TryStartCapture() { try // Properly dispose everything object when turning off capture @@ -207,11 +256,19 @@ private void TryStartCapture() public void EnableCapture() { LOG.Info($"{this}: Enabling screen capture"); - CaptureEnabled = true; - _captureThread = new Thread(TryStartCapture) { IsBackground = true }; - _captureThread.Start(); + if (_captureThread == null || !_captureThread.IsAlive) + { + CaptureEnabled = true; + _captureThread = new Thread(TryStartCapture) { IsBackground = true }; + _captureThread.Start(); + } + else + { + LOG.Warn($"{this}: Capture thread is already running"); + } } + public void DisableCapture() { LOG.Info($"{this}: Disabling screen capture"); diff --git a/HyperionScreenCap/Helper/UpdateChecker.cs b/HyperionScreenCap/Helper/UpdateChecker.cs index 32d9234..195653f 100644 --- a/HyperionScreenCap/Helper/UpdateChecker.cs +++ b/HyperionScreenCap/Helper/UpdateChecker.cs @@ -24,14 +24,14 @@ class UpdateChecker public UpdateChecker() { _restClient = new RestClient(GITHUB_API_BASE_URL); - RestRequest request = new RestRequest(GITHUB_LATEST_RELEASE_GET_URL, Method.GET); - IRestResponse response = _restClient.Execute(request); + RestRequest request = new RestRequest(GITHUB_LATEST_RELEASE_GET_URL, Method.Get); // Updated this line + RestResponse response = _restClient.Execute(request); // Updated this line LatestRelease = response.Data; } public bool IsUpdateAvailable() { - if ( LatestRelease != null ) + if (LatestRelease != null) { Version currVer = Assembly.GetExecutingAssembly().GetName().Version; Version newVer; @@ -39,12 +39,12 @@ public bool IsUpdateAvailable() { newVer = new Version(LatestRelease.tag_name.Replace(TAG_NAME_PREFIX, "")); } - catch ( Exception ex ) + catch (Exception ex) { LOG.Error($"Tag name ({LatestRelease.tag_name}) for the latest release has an unexpected format", ex); newVer = ZERO_VERSION; // Fall back on 0.0 } - if ( newVer > currVer ) + if (newVer > currVer) return true; } return false; @@ -54,7 +54,7 @@ public static void StartUpdateCheck(bool isStartupCheck) { LOG.Info("Starting update check"); UpdateChecker updateChecker = new UpdateChecker(); - if ( updateChecker.IsUpdateAvailable() ) + if (updateChecker.IsUpdateAvailable()) { Release latestRelease = updateChecker.LatestRelease; StringBuilder bodyBuilder = new StringBuilder(); @@ -66,7 +66,7 @@ public static void StartUpdateCheck(bool isStartupCheck) bodyBuilder.Append("Would you like to download the update?"); DialogResult dialogResult = MessageBox.Show(bodyBuilder.ToString(), "Hyperion Screen Capture Update Available", MessageBoxButtons.YesNo, MessageBoxIcon.Information); - if ( dialogResult == DialogResult.Yes ) + if (dialogResult == DialogResult.Yes) { LOG.Info("Starting latest release download"); Process.Start(latestRelease.assets[0].browser_download_url); @@ -74,7 +74,7 @@ public static void StartUpdateCheck(bool isStartupCheck) } else { - if ( !isStartupCheck ) + if (!isStartupCheck) { MessageBox.Show("No updates available. If you think this is an error, please check your internet connection.", "Hyperion Screen Capture Update Check", MessageBoxButtons.OK); diff --git a/HyperionScreenCap/HyperionScreenCap.csproj b/HyperionScreenCap/HyperionScreenCap.csproj index b45527b..b305111 100644 --- a/HyperionScreenCap/HyperionScreenCap.csproj +++ b/HyperionScreenCap/HyperionScreenCap.csproj @@ -104,8 +104,8 @@ ..\packages\Markdig.0.22.0\lib\net452\Markdig.dll - - ..\packages\Microsoft.Bcl.AsyncInterfaces.5.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + + ..\packages\Microsoft.Bcl.AsyncInterfaces.8.0.0\lib\net462\Microsoft.Bcl.AsyncInterfaces.dll ..\packages\Microsoft.CodeAnalysis.Common.3.8.0\lib\netstandard2.0\Microsoft.CodeAnalysis.dll @@ -119,11 +119,11 @@ ..\packages\Microsoft.CodeAnalysis.Workspaces.Common.3.8.0\lib\netstandard2.0\Microsoft.CodeAnalysis.Workspaces.dll - - ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll + + ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll - - ..\packages\RestSharp.106.11.7\lib\net452\RestSharp.dll + + ..\packages\RestSharp.112.0.0\lib\net48\RestSharp.dll ..\packages\SharpDX.4.2.0\lib\net45\SharpDX.dll @@ -141,8 +141,8 @@ ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - - ..\packages\System.Collections.Immutable.5.0.0\lib\net461\System.Collections.Immutable.dll + + ..\packages\System.Collections.Immutable.8.0.0\lib\net462\System.Collections.Immutable.dll @@ -163,26 +163,36 @@ - - ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll + + ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll - - ..\packages\System.Reflection.Metadata.5.0.0\lib\net461\System.Reflection.Metadata.dll + + ..\packages\System.Reflection.Metadata.8.0.0\lib\net462\System.Reflection.Metadata.dll - - ..\packages\System.Runtime.CompilerServices.Unsafe.5.0.0\lib\net45\System.Runtime.CompilerServices.Unsafe.dll + + ..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll ..\packages\System.Text.Encoding.CodePages.5.0.0\lib\net461\System.Text.Encoding.CodePages.dll + + ..\packages\System.Text.Encodings.Web.8.0.0\lib\net462\System.Text.Encodings.Web.dll + + + ..\packages\System.Text.Json.8.0.4\lib\net462\System.Text.Json.dll + ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + ..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll + diff --git a/HyperionScreenCap/Networking/FbsClinet.cs b/HyperionScreenCap/Networking/FbsClinet.cs index eebe44d..228a5d8 100644 --- a/HyperionScreenCap/Networking/FbsClinet.cs +++ b/HyperionScreenCap/Networking/FbsClinet.cs @@ -1,6 +1,7 @@ using System; using hyperionnet; using FlatBuffers; +using static Humanizer.In; namespace HyperionScreenCap.Networking { @@ -55,11 +56,29 @@ private void SendFinishedMessage(FlatBufferBuilder finaliedBuilder) var messageSize = messageToSend.Length; sendMessageSize(messageSize); _stream.Write(messageToSend, 0, messageSize); + _stream.Flush(); // Ensure data is sent immediately } public override String ToString() { return $"FbsClinet[{_host}:{_port} ({_priority})]"; } + + protected override void SendRegistrationMessage() + { + var builder = new FlatBufferBuilder(64); + var originOffset = builder.CreateString("HyperionScreenCap"); + var registerOffset = Register.CreateRegister(builder, originOffset, _priority); + var requestOffset = Request.CreateRequest(builder, Command.Register, registerOffset.Value); + builder.Finish(requestOffset.Value); + SendFinishedMessage(builder); + } + + public override void SendInitialFrame(int width, int height) + { + // Send a black frame to initialize the connection + byte[] blackFrame = new byte[width * height * 3]; + SendImageDataMessage(blackFrame, width, height); + } } } diff --git a/HyperionScreenCap/Networking/HyperionClient.cs b/HyperionScreenCap/Networking/HyperionClient.cs index 54008a0..47d7676 100644 --- a/HyperionScreenCap/Networking/HyperionClient.cs +++ b/HyperionScreenCap/Networking/HyperionClient.cs @@ -30,33 +30,33 @@ public HyperionClient(string host, int port, int priority, int messageDuration) _messageDuration = messageDuration; } - public void Connect() + protected abstract void SendRegistrationMessage(); + public abstract void SendInitialFrame(int width, int height); + + public virtual void Connect() { - if ( _initLock || IsConnected() ) - { - LOG.Info($"{this} already connected. Skipping request."); - return; - } _initLock = true; LOG.Info($"{this} Init lock set"); - _socket = new TcpClient - { - SendTimeout = AppConstants.PROTO_CLIENT_SOCKET_TIMEOUT, - ReceiveTimeout = AppConstants.PROTO_CLIENT_SOCKET_TIMEOUT - }; - try { + _socket = new TcpClient + { + SendTimeout = AppConstants.PROTO_CLIENT_SOCKET_TIMEOUT, + ReceiveTimeout = AppConstants.PROTO_CLIENT_SOCKET_TIMEOUT + }; + _socket.Connect(_host, _port); _stream = _socket.GetStream(); + Initialized = true; + + SendRegistrationMessage(); // Moved here to ensure it's called after connecting } finally { _initLock = false; LOG.Info($"{this} Init lock unset"); } - Initialized = true; } public bool IsConnected() diff --git a/HyperionScreenCap/Networking/ProtoClient.cs b/HyperionScreenCap/Networking/ProtoClient.cs index b8e022c..38917d0 100644 --- a/HyperionScreenCap/Networking/ProtoClient.cs +++ b/HyperionScreenCap/Networking/ProtoClient.cs @@ -1,6 +1,7 @@ using System; using Google.ProtocolBuffers; using proto; +using static Humanizer.In; namespace HyperionScreenCap.Networking { @@ -78,5 +79,22 @@ public override String ToString() { return $"ProtoClient[{_host}:{_port} ({_priority})]"; } + + protected override void SendRegistrationMessage() + { + // Assuming there's no specific RegisterRequest, we'll use the general HyperionRequest + var request = proto.HyperionRequest.CreateBuilder() + .SetCommand(proto.HyperionRequest.Types.Command.COLOR) // Use an existing command + .Build(); + + SendRequest(request); + } + + public override void SendInitialFrame(int width, int height) + { + // Send a black frame to initialize the connection + byte[] blackFrame = new byte[width * height * 3]; + SendImageDataMessage(blackFrame, width, height); + } } } diff --git a/HyperionScreenCap/Networking/fbs/RawImage.cs b/HyperionScreenCap/Networking/fbs/RawImage.cs index 3757751..8ff802a 100644 --- a/HyperionScreenCap/Networking/fbs/RawImage.cs +++ b/HyperionScreenCap/Networking/fbs/RawImage.cs @@ -33,20 +33,40 @@ public struct RawImage : IFlatbufferObject public int Height { get { int o = __p.__offset(8); return o != 0 ? __p.bb.GetInt(o + __p.bb_pos) : (int)-1; } } public bool MutateHeight(int height) { int o = __p.__offset(8); if (o != 0) { __p.bb.PutInt(o + __p.bb_pos, height); return true; } else { return false; } } - public static Offset CreateRawImage(FlatBufferBuilder builder, - VectorOffset dataOffset = default(VectorOffset), - int width = -1, - int height = -1) { +public static Offset CreateRawImage(FlatBufferBuilder builder, + VectorOffset dataOffset = default(VectorOffset), + int width = -1, + int height = -1) +{ builder.StartTable(3); RawImage.AddHeight(builder, height); RawImage.AddWidth(builder, width); - RawImage.AddData(builder, dataOffset); + if (dataOffset.Value != 0) // Check if data is not null + { + RawImage.AddData(builder, dataOffset); + } return RawImage.EndRawImage(builder); - } +} public static void StartRawImage(FlatBufferBuilder builder) { builder.StartTable(3); } public static void AddData(FlatBufferBuilder builder, VectorOffset dataOffset) { builder.AddOffset(0, dataOffset.Value, 0); } - public static VectorOffset CreateDataVector(FlatBufferBuilder builder, byte[] data) { builder.StartVector(1, data.Length, 1); for (int i = data.Length - 1; i >= 0; i--) builder.AddByte(data[i]); return builder.EndVector(); } + + public static VectorOffset CreateDataVector(FlatBufferBuilder builder, byte[] data) + { + if (data == null || data.Length == 0) // Handle null or empty data case + { + // Return an empty vector if data is null or empty to avoid crashing + return builder.EndVector(); + } + + builder.StartVector(1, data.Length, 1); + for (int i = data.Length - 1; i >= 0; i--) + { + builder.AddByte(data[i]); + } + return builder.EndVector(); + } + public static VectorOffset CreateDataVectorBlock(FlatBufferBuilder builder, byte[] data) { builder.StartVector(1, data.Length, 1); builder.Add(data); return builder.EndVector(); } public static void StartDataVector(FlatBufferBuilder builder, int numElems) { builder.StartVector(1, numElems, 1); } public static void AddWidth(FlatBufferBuilder builder, int width) { builder.AddInt(1, width, -1); } diff --git a/HyperionScreenCap/app.config b/HyperionScreenCap/app.config index 36c3a75..fe1e587 100644 --- a/HyperionScreenCap/app.config +++ b/HyperionScreenCap/app.config @@ -106,7 +106,7 @@ - + @@ -114,7 +114,7 @@ - + @@ -126,12 +126,20 @@ - + + + + + + + + + diff --git a/HyperionScreenCap/packages.config b/HyperionScreenCap/packages.config index e3389c4..f1548ab 100644 --- a/HyperionScreenCap/packages.config +++ b/HyperionScreenCap/packages.config @@ -8,30 +8,33 @@ - + - - + + - + - + - - + + + + + \ No newline at end of file From 76db4ec6d5b5b4b314131bbfb7cde918e79e89d8 Mon Sep 17 00:00:00 2001 From: BigSoulja Date: Fri, 27 Sep 2024 11:06:05 +0100 Subject: [PATCH 2/4] Added better handling for UAC prompts specifically --- .../Capture/Dx11ScreenCapture.cs | 290 +++++++----------- HyperionScreenCap/Config/AppConstants.cs | 5 + 2 files changed, 110 insertions(+), 185 deletions(-) diff --git a/HyperionScreenCap/Capture/Dx11ScreenCapture.cs b/HyperionScreenCap/Capture/Dx11ScreenCapture.cs index e5baeb2..f9a5a23 100644 --- a/HyperionScreenCap/Capture/Dx11ScreenCapture.cs +++ b/HyperionScreenCap/Capture/Dx11ScreenCapture.cs @@ -3,12 +3,7 @@ using SharpDX.Direct3D11; using SharpDX.DXGI; using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; -using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; @@ -16,6 +11,15 @@ namespace HyperionScreenCap { + // Custom exception to indicate that capture is paused + public class CapturePausedException : Exception + { + public CapturePausedException(string message) + : base(message) + { + } + } + class DX11ScreenCapture : IScreenCapture { private int _adapterIndex; @@ -39,13 +43,8 @@ class DX11ScreenCapture : IScreenCapture private byte[] _lastCapturedFrame; private int _minCaptureTime; private Stopwatch _captureTimer; - private bool _desktopDuplicatorInvalid; private bool _disposed; - private int _reinitializationAttempts = 0; - private const int MAX_REINITIALIZATION_ATTEMPTS = 5; - private const int REINITIALIZATION_DELAY_MS = 1000; - public int CaptureWidth { get; private set; } public int CaptureHeight { get; private set; } @@ -54,14 +53,14 @@ class DX11ScreenCapture : IScreenCapture public static String GetAvailableMonitors() { StringBuilder response = new StringBuilder(); - using ( Factory1 factory = new Factory1() ) + using (Factory1 factory = new Factory1()) { int adapterIndex = 0; - foreach(Adapter adapter in factory.Adapters) + foreach (Adapter adapter in factory.Adapters) { response.Append($"Adapter Index {adapterIndex++}: {adapter.Description.Description}\n"); int outputIndex = 0; - foreach(Output output in adapter.Outputs) + foreach (Output output in adapter.Outputs) { response.Append($"\tMonitor Index {outputIndex++}: {output.Description.DeviceName}"); var desktopBounds = output.Description.DesktopBounds; @@ -85,41 +84,8 @@ public DX11ScreenCapture(int adapterIndex, int monitorIndex, int scalingFactor, public void Initialize() { - Dispose(); - - int retryCount = 0; - const int maxRetries = 5; - const int retryDelay = 1000; // 1 second - - while (retryCount < maxRetries) - { - try - { - InitializeInternal(); - return; // If successful, exit the method - } - catch (SharpDXException ex) - { - LOG.Error($"SharpDX exception during initialization (attempt {retryCount + 1}/{maxRetries}): {ex.Message}"); - retryCount++; - if (retryCount >= maxRetries) - { - throw new Exception("Failed to initialize DX11 screen capture after multiple attempts", ex); - } - Thread.Sleep(retryDelay); - } - catch (Exception ex) - { - LOG.Error($"Unexpected exception during initialization: {ex.Message}"); - throw; - } - } - InitDesktopDuplicator(); - } + Dispose(); // Ensure previous resources are released - private void InitializeInternal() - { - // Move all the existing initialization code here int mipLevels; if (_scalingFactor == 1) mipLevels = 1; @@ -131,27 +97,27 @@ private void InitializeInternal() else throw new Exception("Invalid scaling factor. Allowed values are 1, 2, 4, etc."); - _factory?.Dispose(); - _adapter?.Dispose(); - _output?.Dispose(); - _output1?.Dispose(); - _device?.Dispose(); - _stagingTexture?.Dispose(); - _smallerTexture?.Dispose(); - _smallerTextureView?.Dispose(); - + // Create DXGI Factory1 _factory = new Factory1(); _adapter = _factory.GetAdapter1(_adapterIndex); + // Create device from Adapter _device = new SharpDX.Direct3D11.Device(_adapter); + // Get DXGI.Output _output = _adapter.GetOutput(_monitorIndex); if (_output == null) { - throw new Exception($"No output found for adapter {_adapterIndex} and monitor {_monitorIndex}"); + throw new Exception($"Failed to get output for monitor index {_monitorIndex}."); } + _output1 = _output.QueryInterface(); + if (_output1 == null) + { + throw new Exception("Failed to get Output1 interface."); + } + // Width/Height of desktop to capture var desktopBounds = _output.Description.DesktopBounds; _width = desktopBounds.Right - desktopBounds.Left; _height = desktopBounds.Bottom - desktopBounds.Top; @@ -159,6 +125,7 @@ private void InitializeInternal() CaptureWidth = _width / _scalingFactor; CaptureHeight = _height / _scalingFactor; + // Create Staging texture CPU-accessible var stagingTextureDesc = new Texture2DDescription { CpuAccessFlags = CpuAccessFlags.Read, @@ -174,6 +141,7 @@ private void InitializeInternal() }; _stagingTexture = new Texture2D(_device, stagingTextureDesc); + // Create smaller texture to downscale the captured image var smallerTextureDesc = new Texture2DDescription { CpuAccessFlags = CpuAccessFlags.None, @@ -202,152 +170,99 @@ private void InitDesktopDuplicator() try { _duplicatedOutput?.Dispose(); + _duplicatedOutput = null; + + if (_output1 == null || _device == null) + { + throw new Exception("Output1 or Device is null. Cannot initialize desktop duplicator."); + } + _duplicatedOutput = _output1.DuplicateOutput(_device); - _desktopDuplicatorInvalid = false; LOG.Debug("Desktop duplicator initialized successfully."); } catch (SharpDXException ex) { LOG.Error($"SharpDXException in InitDesktopDuplicator: {ex.Message}", ex); - _desktopDuplicatorInvalid = true; throw; } } - private void ReinitializeOutputAndDevice() + public byte[] Capture() { - LOG.Debug("Reinitializing Output and Device..."); + _captureTimer.Restart(); try { - _factory?.Dispose(); - _adapter?.Dispose(); - _output?.Dispose(); - _output1?.Dispose(); - _device?.Dispose(); - - _factory = new Factory1(); - if (_factory == null) throw new InvalidOperationException("Failed to create Factory1"); - - _adapter = _factory.GetAdapter1(_adapterIndex); - if (_adapter == null) throw new InvalidOperationException($"No adapter found for index {_adapterIndex}"); - - _device = new SharpDX.Direct3D11.Device(_adapter); - if (_device == null) throw new InvalidOperationException("Failed to create Device"); - - _output = _adapter.GetOutput(_monitorIndex); - if (_output == null) throw new InvalidOperationException($"No output found for adapter {_adapterIndex} and monitor {_monitorIndex}"); - - _output1 = _output.QueryInterface(); - if (_output1 == null) throw new InvalidOperationException("Failed to query Output1 interface"); - - LOG.Info("Successfully reinitialized Output and Device."); - } - catch (Exception ex) - { - LOG.Error($"Failed to reinitialize Output and Device: {ex.Message}", ex); - throw; + byte[] response = ManagedCapture(); + _captureTimer.Stop(); + return response; } - } - - private void EnsureDeviceInitialized() - { - if (_device == null || _device.IsDisposed) + catch (SharpDXException ex) { - LOG.Warn("Device is null or disposed. Attempting to reinitialize..."); - try - { - ReinitializeOutputAndDevice(); - } - catch (Exception ex) + if (ex.ResultCode == SharpDX.DXGI.ResultCode.AccessLost || + ex.ResultCode == SharpDX.DXGI.ResultCode.AccessDenied || + ex.ResultCode == SharpDX.DXGI.ResultCode.SessionDisconnected || + ex.ResultCode == SharpDX.DXGI.ResultCode.DeviceRemoved || + ex.ResultCode == SharpDX.DXGI.ResultCode.InvalidCall) { - LOG.Error($"Failed to reinitialize device: {ex.Message}", ex); - throw new InvalidOperationException("Failed to reinitialize device", ex); + LOG.Warn($"Capture failed due to device loss: {ex.Message}. Attempting to reinitialize."); + Reinitialize(); + throw new CapturePausedException("Capture is paused due to device loss."); } - } - } - - public byte[] Capture() - { - if (_desktopDuplicatorInvalid) - { - LOG.Warn("Desktop duplicator is invalid. Attempting to reinitialize..."); - if (!ReinitializeDesktopDuplicator()) + else { - throw new InvalidOperationException("Failed to reinitialize desktop duplicator after multiple attempts"); + LOG.Error($"Capture failed: {ex.Message}", ex); + throw; } } - - _captureTimer.Restart(); - try - { - byte[] response = ManagedCapture(); - _captureTimer.Stop(); - _reinitializationAttempts = 0; // Reset the counter on successful capture - return response; - } - catch (InvalidOperationException ex) + catch (Exception ex) { LOG.Error($"Capture failed: {ex.Message}", ex); - _desktopDuplicatorInvalid = true; throw; } } - private bool ReinitializeDesktopDuplicator() + private void Reinitialize() { - while (_reinitializationAttempts < MAX_REINITIALIZATION_ATTEMPTS) + Dispose(); + int attempt = 0; + const int maxAttempts = 10; + const int delayBetweenAttempts = 1000; // milliseconds + + while (attempt < maxAttempts) { try { - _reinitializationAttempts++; - LOG.Info($"Reinitialization attempt {_reinitializationAttempts} of {MAX_REINITIALIZATION_ATTEMPTS}"); - - // Only reinitialize the desktop duplicator - InitDesktopDuplicator(); - - _desktopDuplicatorInvalid = false; - LOG.Info("Desktop duplicator reinitialized successfully."); - return true; + attempt++; + Initialize(); + LOG.Info("Reinitialized capture successfully."); + return; } catch (Exception ex) { - LOG.Error($"Failed to reinitialize desktop duplicator: {ex.Message}", ex); - Thread.Sleep(REINITIALIZATION_DELAY_MS * _reinitializationAttempts); + LOG.Error($"Failed to reinitialize capture (attempt {attempt}/{maxAttempts}): {ex.Message}", ex); + Thread.Sleep(delayBetweenAttempts); } } - return false; + throw new CapturePausedException("Failed to reinitialize capture after multiple attempts."); } private byte[] ManagedCapture() { + if (_duplicatedOutput == null) + { + throw new Exception("DuplicatedOutput is null. Cannot capture."); + } + SharpDX.DXGI.Resource screenResource = null; OutputDuplicateFrameInformation duplicateFrameInformation; try { - EnsureDeviceInitialized(); + // Try to get duplicated frame within given time + _duplicatedOutput.AcquireNextFrame(_frameCaptureTimeout, out duplicateFrameInformation, out screenResource); - try - { - // Try to get duplicated frame within given time - _duplicatedOutput.AcquireNextFrame(_frameCaptureTimeout, out duplicateFrameInformation, out screenResource); - - if (duplicateFrameInformation.LastPresentTime == 0 && _lastCapturedFrame != null) - return _lastCapturedFrame; - } - catch (SharpDXException ex) - { - if (ex.ResultCode.Code == SharpDX.DXGI.ResultCode.WaitTimeout.Code && _lastCapturedFrame != null) - return _lastCapturedFrame; - - if (ex.ResultCode.Code == SharpDX.DXGI.ResultCode.AccessLost.Code) - { - _desktopDuplicatorInvalid = true; - throw new InvalidOperationException("Desktop duplicator access lost", ex); - } - - throw; - } + if (duplicateFrameInformation.LastPresentTime == 0 && _lastCapturedFrame != null) + return _lastCapturedFrame; // Check if scaling is used if (CaptureWidth != _width) @@ -355,17 +270,13 @@ private byte[] ManagedCapture() // Copy resource into memory that can be accessed by the CPU using (var screenTexture2D = screenResource.QueryInterface()) { - if (_device == null || _device.ImmediateContext == null) - { - throw new InvalidOperationException("Device or ImmediateContext is null"); - } _device.ImmediateContext.CopySubresourceRegion(screenTexture2D, 0, null, _smallerTexture, 0); } // Generates the mipmap of the screen _device.ImmediateContext.GenerateMips(_smallerTextureView); - // Copy the mipmap of smallerTexture (size/ scalingFactor) to the staging texture: 1 for /2, 2 for /4...etc + // Copy the mipmap of smallerTexture (size/ scalingFactor) to the staging texture _device.ImmediateContext.CopySubresourceRegion(_smallerTexture, _scalingFactorLog2, null, _stagingTexture, 0); } else @@ -373,10 +284,6 @@ private byte[] ManagedCapture() // Copy resource into memory that can be accessed by the CPU using (var screenTexture2D = screenResource.QueryInterface()) { - if (_device == null || _device.ImmediateContext == null) - { - throw new InvalidOperationException("Device or ImmediateContext is null"); - } _device.ImmediateContext.CopyResource(screenTexture2D, _stagingTexture); } } @@ -386,11 +293,6 @@ private byte[] ManagedCapture() _lastCapturedFrame = ToRGBArray(mapSource); return _lastCapturedFrame; } - catch (Exception ex) - { - LOG.Error($"Error in ManagedCapture: {ex.Message}", ex); - throw; - } finally { screenResource?.Dispose(); @@ -412,15 +314,15 @@ private byte[] ToRGBArray(DataBox mapSource) var sourcePtr = mapSource.DataPointer; byte[] bytes = new byte[CaptureWidth * 3 * CaptureHeight]; int byteIndex = 0; - for ( int y = 0; y < CaptureHeight; y++ ) + for (int y = 0; y < CaptureHeight; y++) { Int32[] rowData = new Int32[CaptureWidth]; Marshal.Copy(sourcePtr, rowData, 0, CaptureWidth); - foreach ( Int32 pixelData in rowData ) + foreach (Int32 pixelData in rowData) { byte[] values = BitConverter.GetBytes(pixelData); - if ( BitConverter.IsLittleEndian ) + if (BitConverter.IsLittleEndian) { // Byte order : bgra bytes[byteIndex++] = values[2]; @@ -444,7 +346,7 @@ private byte[] ToRGBArray(DataBox mapSource) public void DelayNextCapture() { int remainingFrameTime = _minCaptureTime - (int)_captureTimer.ElapsedMilliseconds; - if ( remainingFrameTime > 0 ) + if (remainingFrameTime > 0) { Thread.Sleep(remainingFrameTime); } @@ -452,15 +354,33 @@ public void DelayNextCapture() public void Dispose() { - _duplicatedOutput?.Dispose(); - _output1?.Dispose(); - _output?.Dispose(); - _stagingTexture?.Dispose(); - _smallerTexture?.Dispose(); - _smallerTextureView?.Dispose(); - _device?.Dispose(); - _adapter?.Dispose(); - _factory?.Dispose(); + try { _duplicatedOutput?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _duplicatedOutput", ex); } + _duplicatedOutput = null; + + try { _output1?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _output1", ex); } + _output1 = null; + + try { _output?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _output", ex); } + _output = null; + + try { _stagingTexture?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _stagingTexture", ex); } + _stagingTexture = null; + + try { _smallerTexture?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _smallerTexture", ex); } + _smallerTexture = null; + + try { _smallerTextureView?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _smallerTextureView", ex); } + _smallerTextureView = null; + + try { _device?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _device", ex); } + _device = null; + + try { _adapter?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _adapter", ex); } + _adapter = null; + + try { _factory?.Dispose(); } catch (Exception ex) { LOG.Error("Exception during Dispose of _factory", ex); } + _factory = null; + _lastCapturedFrame = null; _disposed = true; } diff --git a/HyperionScreenCap/Config/AppConstants.cs b/HyperionScreenCap/Config/AppConstants.cs index 1285451..c136f08 100644 --- a/HyperionScreenCap/Config/AppConstants.cs +++ b/HyperionScreenCap/Config/AppConstants.cs @@ -10,6 +10,11 @@ class AppConstants /// public const int MAX_CAPTURE_ATTEMPTS = 45; + /// + /// Delay in milliseconds before retrying after a pause + /// + public const int CAPTURE_PAUSED_RETRY_DELAY = 2000; + /// /// Number of screen capture failure attemps after which screen capture should be re-initialized. /// From d591b40c2599c7fb93b86629ab301c382418fb9a Mon Sep 17 00:00:00 2001 From: BigSoulja Date: Fri, 27 Sep 2024 12:09:03 +0100 Subject: [PATCH 3/4] Refactored and made it much more robust even if things like the UAC prompt etc have been present for long amounts of time. Should recover in all situations now without any performance issues. --- .../Capture/Dx11ScreenCapture.cs | 29 +------ HyperionScreenCap/Config/AppConstants.cs | 2 +- HyperionScreenCap/Helper/HyperionTask.cs | 84 ++++++++++++------- HyperionScreenCap/Properties/AssemblyInfo.cs | 2 +- 4 files changed, 56 insertions(+), 61 deletions(-) diff --git a/HyperionScreenCap/Capture/Dx11ScreenCapture.cs b/HyperionScreenCap/Capture/Dx11ScreenCapture.cs index f9a5a23..8515bc3 100644 --- a/HyperionScreenCap/Capture/Dx11ScreenCapture.cs +++ b/HyperionScreenCap/Capture/Dx11ScreenCapture.cs @@ -204,8 +204,8 @@ public byte[] Capture() ex.ResultCode == SharpDX.DXGI.ResultCode.DeviceRemoved || ex.ResultCode == SharpDX.DXGI.ResultCode.InvalidCall) { - LOG.Warn($"Capture failed due to device loss: {ex.Message}. Attempting to reinitialize."); - Reinitialize(); + LOG.Warn($"Capture failed due to device loss: {ex.Message}. Disposing and signaling for reinitialization."); + Dispose(); throw new CapturePausedException("Capture is paused due to device loss."); } else @@ -221,31 +221,6 @@ public byte[] Capture() } } - private void Reinitialize() - { - Dispose(); - int attempt = 0; - const int maxAttempts = 10; - const int delayBetweenAttempts = 1000; // milliseconds - - while (attempt < maxAttempts) - { - try - { - attempt++; - Initialize(); - LOG.Info("Reinitialized capture successfully."); - return; - } - catch (Exception ex) - { - LOG.Error($"Failed to reinitialize capture (attempt {attempt}/{maxAttempts}): {ex.Message}", ex); - Thread.Sleep(delayBetweenAttempts); - } - } - throw new CapturePausedException("Failed to reinitialize capture after multiple attempts."); - } - private byte[] ManagedCapture() { if (_duplicatedOutput == null) diff --git a/HyperionScreenCap/Config/AppConstants.cs b/HyperionScreenCap/Config/AppConstants.cs index c136f08..35f45c3 100644 --- a/HyperionScreenCap/Config/AppConstants.cs +++ b/HyperionScreenCap/Config/AppConstants.cs @@ -13,7 +13,7 @@ class AppConstants /// /// Delay in milliseconds before retrying after a pause /// - public const int CAPTURE_PAUSED_RETRY_DELAY = 2000; + public const int CAPTURE_PAUSED_RETRY_DELAY = 3000; /// /// Number of screen capture failure attemps after which screen capture should be re-initialized. diff --git a/HyperionScreenCap/Helper/HyperionTask.cs b/HyperionScreenCap/Helper/HyperionTask.cs index 0cb7001..101be03 100644 --- a/HyperionScreenCap/Helper/HyperionTask.cs +++ b/HyperionScreenCap/Helper/HyperionTask.cs @@ -32,7 +32,7 @@ public HyperionTask(HyperionTaskConfiguration configuration, NotificationUtils n private void InitScreenCapture() { - if ( _screenCapture != null && !_screenCapture.IsDisposed() ) + if (_screenCapture != null && !_screenCapture.IsDisposed()) { // Screen capture already initialized. Ignoring request. return; @@ -43,7 +43,7 @@ private void InitScreenCapture() _screenCapture.Initialize(); LOG.Info($"{this}: Screen capture initialized"); } - catch ( Exception ex ) + catch (Exception ex) { _screenCapture?.Dispose(); throw new Exception("Failed to initialize screen capture: " + ex.Message, ex); @@ -156,69 +156,89 @@ private byte[] CaptureInitialFrame() private void TransmitNextFrame() { - foreach ( HyperionClient hyperionClient in _hyperionClients ) + try { - try + byte[] imageData = _screenCapture.Capture(); + foreach (HyperionClient hyperionClient in _hyperionClients) { - byte[] imageData = _screenCapture.Capture(); hyperionClient.SendImageData(imageData, _screenCapture.CaptureWidth, _screenCapture.CaptureHeight); - - // Uncomment the following to enable debugging - // MiscUtils.SaveRGBArrayToImageFile(imageData, _screenCapture.CaptureWidth, _screenCapture.CaptureHeight, AppConstants.DEBUG_IMAGE_FILE_NAME); - } - catch ( Exception ex ) - { - throw new Exception("Error occured while sending image to server: " + ex.Message, ex); } } - } - - public void RestartCapture() - { - DisableCapture(); - Thread.Sleep(1000); // Wait a bit before restarting - EnableCapture(); + catch (CapturePausedException) + { + // Re-throw to be handled in StartCapture() + throw; + } + catch (Exception ex) + { + LOG.Error("Error occurred while capturing or sending image to server: " + ex.Message, ex); + throw; + } } private void StartCapture() { - InstantiateScreenCapture(); - InstantiateHyperionClients(); int captureAttempt = 1; while (CaptureEnabled) { try { + // Ensure screen capture and clients are instantiated + if (_screenCapture == null || _screenCapture.IsDisposed()) + { + InstantiateScreenCapture(); + } + if (_hyperionClients == null || _hyperionClients.Count == 0) + { + InstantiateHyperionClients(); + } + InitScreenCapture(); ConnectHyperionClients(); + captureAttempt = 1; // Reset capture attempt counter after successful initialization + while (CaptureEnabled) { TransmitNextFrame(); _screenCapture.DelayNextCapture(); } } + catch (CapturePausedException ex) + { + LOG.Warn($"{this}: Capture paused: {ex.Message}. Disposing and preparing to reinitialize."); + _screenCapture?.Dispose(); + _screenCapture = null; + + DisposeHyperionClients(); + Thread.Sleep(AppConstants.CAPTURE_PAUSED_RETRY_DELAY); // Prevent rapid looping + } catch (Exception ex) { LOG.Error($"{this}: Exception in screen capture attempt: {captureAttempt}", ex); - if (captureAttempt > AppConstants.REINIT_CAPTURE_AFTER_ATTEMPTS) + if (captureAttempt >= AppConstants.REINIT_CAPTURE_AFTER_ATTEMPTS) { - LOG.Info($"{this}: Attempting to recreate screen capture object and reconnect to Hyperion"); - RecreateScreenCaptureAndReconnect(); - return; // Exit this method, as a new capture thread has been started + LOG.Info($"{this}: Disposing resources and preparing to reinitialize."); + _screenCapture?.Dispose(); + _screenCapture = null; + DisposeHyperionClients(); + captureAttempt = 1; // Reset capture attempt counter } - Thread.Sleep(AppConstants.CAPTURE_FAILED_COOLDOWN_MILLIS); - captureAttempt++; - - if (captureAttempt > AppConstants.MAX_CAPTURE_ATTEMPTS) + else { - LOG.Error($"{this}: Max screen capture attempts reached. Disabling capture."); - CaptureEnabled = false; - OnCaptureDisabled?.Invoke(this, EventArgs.Empty); + Thread.Sleep(AppConstants.CAPTURE_FAILED_COOLDOWN_MILLIS); + captureAttempt++; } } } } + public void RestartCapture() + { + DisableCapture(); + Thread.Sleep(1000); // Wait a bit before restarting + EnableCapture(); + } + private void RecreateScreenCaptureAndReconnect() { LOG.Info($"{this}: Recreating screen capture and reconnecting to Hyperion"); diff --git a/HyperionScreenCap/Properties/AssemblyInfo.cs b/HyperionScreenCap/Properties/AssemblyInfo.cs index f80e7b0..1438a00 100644 --- a/HyperionScreenCap/Properties/AssemblyInfo.cs +++ b/HyperionScreenCap/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.9.0.0")] +[assembly: AssemblyVersion("2.9.0.2")] //[assembly: AssemblyFileVersion("2.0.0.0")] Commented out so that it will be generated automatically From 6c89c147c286d2434fac92722464318d8456edda Mon Sep 17 00:00:00 2001 From: BigSoulja Date: Fri, 27 Sep 2024 12:10:30 +0100 Subject: [PATCH 4/4] update version number --- HyperionScreenCap/Properties/AssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HyperionScreenCap/Properties/AssemblyInfo.cs b/HyperionScreenCap/Properties/AssemblyInfo.cs index 1438a00..882b663 100644 --- a/HyperionScreenCap/Properties/AssemblyInfo.cs +++ b/HyperionScreenCap/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.9.0.2")] +[assembly: AssemblyVersion("2.9.0.3")] //[assembly: AssemblyFileVersion("2.0.0.0")] Commented out so that it will be generated automatically