diff --git a/Application/FileConverter/ConversionJobs/ConversionJob.cs b/Application/FileConverter/ConversionJobs/ConversionJob.cs index ecf68545..5730a94a 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob.cs @@ -468,6 +468,11 @@ protected void ConversionFailed(string exitingMessage) this.ErrorMessage = exitingMessage; } + protected void SetStatusMessage(string message) + { + this.ErrorMessage = message; + } + protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "") { if (this.PropertyChanged != null) diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs index 79f81586..0d12332a 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs @@ -23,8 +23,11 @@ public partial class ConversionJob_FFMPEG : ConversionJob private ProcessStartInfo ffmpegProcessStartInfo; private readonly List ffmpegArgumentStringByPass = new List(); + private ISettingsService settingsService; - ISettingsService settingsService = Ioc.Default.GetRequiredService(); + private Helpers.HardwareAccelerationMode currentHardwareAccelerationMode = Helpers.HardwareAccelerationMode.Off; + private bool softwareFallbackSucceeded; + private string softwareFallbackReason = string.Empty; public ConversionJob_FFMPEG() : base() { @@ -83,12 +86,16 @@ protected override void Initialize() RedirectStandardError = true }; - this.FillFFMpegArgumentsList(); + this.currentHardwareAccelerationMode = this.GetSettingsService().Settings.HardwareAccelerationMode; + this.softwareFallbackSucceeded = false; + this.softwareFallbackReason = string.Empty; + this.FillFFMpegArgumentsList(this.currentHardwareAccelerationMode); } - protected virtual void FillFFMpegArgumentsList() + protected virtual void FillFFMpegArgumentsList(Helpers.HardwareAccelerationMode hardwareAccelerationMode) { const string baseArgs = "-n -progress pipe:1"; + this.ffmpegArgumentStringByPass.Clear(); bool customCommandEnabled = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.EnableFFMPEGCustomCommand); if (customCommandEnabled) @@ -260,7 +267,7 @@ protected virtual void FillFFMpegArgumentsList() VideoEncodingSpeed videoEncodingSpeed = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.VideoEncodingSpeed); int audioEncodingBitrate = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.AudioBitrate); - Helpers.HardwareAccelerationMode hwAccel = settingsService.Settings.HardwareAccelerationMode; + Helpers.HardwareAccelerationMode hwAccel = hardwareAccelerationMode; string transformArgs = ConversionJob_FFMPEG.ComputeTransformArgs(this.ConversionPreset, hwAccel); string videoFilteringArgs = ConversionJob_FFMPEG.Encapsulate("-vf", transformArgs); @@ -432,9 +439,89 @@ protected override void Convert() throw new Exception("The conversion preset must be valid."); } + FFMpegExecutionResult executionResult = this.ExecuteFFMpegPasses(); + if (executionResult.IsSuccess) + { + this.CleanIntermediateFiles(); + return; + } + + if (this.CanRetryWithSoftwareEncoding(executionResult)) + { + string hardwareFailureReason = executionResult.ErrorMessage; + + this.softwareFallbackReason = hardwareFailureReason; + this.UserState = Properties.Resources.GpuEncodingFailedRetryingSoftwareEncode; + Diagnostics.Debug.Log("GPU encoding failed. Retry with software libx264 encoder."); + Diagnostics.Debug.Log($"GPU encoding failure reason: {hardwareFailureReason}"); + + this.DeleteCurrentOutputFileForRetry(); + this.ResetProgressForRetry(); + this.currentHardwareAccelerationMode = Helpers.HardwareAccelerationMode.Off; + this.FillFFMpegArgumentsList(this.currentHardwareAccelerationMode); + + executionResult = this.ExecuteFFMpegPasses(); + if (executionResult.IsSuccess) + { + this.softwareFallbackSucceeded = true; + this.CleanIntermediateFiles(); + return; + } + + if (string.IsNullOrEmpty(hardwareFailureReason)) + { + hardwareFailureReason = "Unknown GPU encoding error."; + } + + string softwareFailureReason = executionResult.ErrorMessage; + if (string.IsNullOrEmpty(softwareFailureReason)) + { + softwareFailureReason = "Unknown software encoding error."; + } + + Diagnostics.Debug.Log($"Software fallback failure reason: {softwareFailureReason}"); + this.ConversionFailed(this.BuildEncodeFailureMessage(hardwareFailureReason)); + this.CleanIntermediateFiles(); + return; + } + + if (!string.IsNullOrEmpty(executionResult.ErrorMessage)) + { + this.ConversionFailed(this.BuildEncodeFailureMessage(executionResult.ErrorMessage)); + } + else + { + this.ConversionFailed(this.BuildEncodeFailureMessage(string.Empty)); + } + + this.CleanIntermediateFiles(); + } + + protected override void OnConversionSucceed() + { + base.OnConversionSucceed(); + + if (!this.softwareFallbackSucceeded) + { + return; + } + + this.UserState = Properties.Resources.ConversionStateDoneSoftwareFallback; + + if (string.IsNullOrEmpty(this.softwareFallbackReason)) + { + this.softwareFallbackReason = "GPU encoder reported an error."; + } + + this.SetStatusMessage(Properties.Resources.GpuEncodingFailedAndRetriedWithSoftwareEncoding); + } + + private FFMpegExecutionResult ExecuteFFMpegPasses() + { for (int index = 0; index < this.ffmpegArgumentStringByPass.Count; index++) { FFMpegPass currentPass = this.ffmpegArgumentStringByPass[index]; + string lastErrorMessage = string.Empty; this.UserState = currentPass.Name; this.ffmpegProcessStartInfo.Arguments = currentPass.Arguments; @@ -457,25 +544,139 @@ protected override void Convert() string result = reader.ReadLine(); - this.ParseFFMPEGOutput(result); + string parsedErrorMessage = this.ParseFFMPEGOutput(result); + if (!string.IsNullOrEmpty(parsedErrorMessage)) + { + lastErrorMessage = parsedErrorMessage; + } Diagnostics.Debug.Log($"ffmpeg output: {result}"); } } exeProcess.WaitForExit(); + + if (this.State == ConversionState.Failed) + { + return new FFMpegExecutionResult(false, this.ErrorMessage); + } + + if (!string.IsNullOrEmpty(lastErrorMessage)) + { + return new FFMpegExecutionResult(false, lastErrorMessage); + } + + if (exeProcess.ExitCode != 0) + { + if (string.IsNullOrEmpty(lastErrorMessage)) + { + lastErrorMessage = $"ffmpeg exited with code {exeProcess.ExitCode}."; + } + + return new FFMpegExecutionResult(false, lastErrorMessage); + } } } - catch + catch (Exception exception) { - this.ConversionFailed(Properties.Resources.ErrorFailedToLaunchFFMPEG); - throw; + Diagnostics.Debug.Log(exception.ToString()); + return new FFMpegExecutionResult(false, Properties.Resources.ErrorFailedToLaunchFFMPEG); } } + return new FFMpegExecutionResult(true, string.Empty); + } + + private bool CanRetryWithSoftwareEncoding(FFMpegExecutionResult executionResult) + { + if (executionResult.IsSuccess || this.CancelIsRequested || this.State == ConversionState.Failed) + { + return false; + } + + if (this.ConversionPreset == null) + { + return false; + } + + if (this.currentHardwareAccelerationMode == Helpers.HardwareAccelerationMode.Off) + { + return false; + } + + if (!this.GetSettingsService().Settings.AutoRetrySoftwareEncodingOnGpuFailure) + { + return false; + } + + bool customCommandEnabled = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.EnableFFMPEGCustomCommand); + if (customCommandEnabled) + { + return false; + } + + return this.ConversionPreset.OutputType == OutputType.Mp4 || this.ConversionPreset.OutputType == OutputType.Mkv; + } + + private string BuildEncodeFailureMessage(string technicalReason) + { + bool isGpuEncodingPath = + this.currentHardwareAccelerationMode != Helpers.HardwareAccelerationMode.Off && + this.ConversionPreset != null && + (this.ConversionPreset.OutputType == OutputType.Mp4 || this.ConversionPreset.OutputType == OutputType.Mkv); + + if (!isGpuEncodingPath) + { + return string.IsNullOrEmpty(technicalReason) ? Properties.Resources.ErrorConversionFailed : technicalReason; + } + + if (this.GetSettingsService().Settings.AutoRetrySoftwareEncodingOnGpuFailure) + { + return Properties.Resources.ErrorGpuEncodingFailedAfterFallback; + } + + return Properties.Resources.ErrorGpuEncodingFailed; + } + + private ISettingsService GetSettingsService() + { + if (this.settingsService == null) + { + this.settingsService = Ioc.Default.GetRequiredService(); + } + + return this.settingsService; + } + + private void DeleteCurrentOutputFileForRetry() + { + if (!File.Exists(this.OutputFilePath)) + { + return; + } + + try + { + File.Delete(this.OutputFilePath); + } + catch (Exception exception) + { + Diagnostics.Debug.Log($"Failed to delete incomplete output file before retry: {this.OutputFilePath}."); + Diagnostics.Debug.Log(exception.ToString()); + } + } + + private void ResetProgressForRetry() + { + this.fileDuration = TimeSpan.Zero; + this.actualConvertedDuration = TimeSpan.Zero; + this.Progress = 0f; + } + + private void CleanIntermediateFiles() + { Diagnostics.Debug.Log(string.Empty); - // Clean intermediate files. for (int index = 0; index < this.ffmpegArgumentStringByPass.Count; index++) { FFMpegPass currentPass = this.ffmpegArgumentStringByPass[index]; @@ -491,8 +692,13 @@ protected override void Convert() } } - private void ParseFFMPEGOutput(string input) + private string ParseFFMPEGOutput(string input) { + if (string.IsNullOrEmpty(input)) + { + return string.Empty; + } + Match match = this.durationRegex.Match(input); if (match.Success && match.Groups.Count >= 6) { @@ -502,7 +708,7 @@ private void ParseFFMPEGOutput(string input) int milliseconds = int.Parse(match.Groups[4].Value) * 10; float bitrate = float.Parse(match.Groups[5].Value); this.fileDuration = new TimeSpan(0, hours, minutes, seconds, milliseconds); - return; + return string.Empty; } if (this.fileDuration.Ticks > 0) @@ -521,7 +727,7 @@ private void ParseFFMPEGOutput(string input) this.actualConvertedDuration = new TimeSpan(0, hours, minutes, seconds, milliseconds); this.Progress = this.actualConvertedDuration.Ticks / (float)this.fileDuration.Ticks; - return; + return string.Empty; } } @@ -537,9 +743,23 @@ private void ParseFFMPEGOutput(string input) } else { - this.ConversionFailed(input); + return input; } } + + return string.Empty; + } + + private struct FFMpegExecutionResult + { + public bool IsSuccess; + public string ErrorMessage; + + public FFMpegExecutionResult(bool isSuccess, string errorMessage) + { + this.IsSuccess = isSuccess; + this.ErrorMessage = errorMessage; + } } private struct FFMpegPass diff --git a/Application/FileConverter/Properties/Resources.Designer.cs b/Application/FileConverter/Properties/Resources.Designer.cs index acc310cf..ca887a1a 100644 --- a/Application/FileConverter/Properties/Resources.Designer.cs +++ b/Application/FileConverter/Properties/Resources.Designer.cs @@ -1501,11 +1501,74 @@ public static string Wav8bitsDescription { /// /// Looks up a localized string similar to Open File Converter website. /// - public static string WebsiteButtonDescription { - get { - return ResourceManager.GetString("WebsiteButtonDescription", resourceCulture); - } - } + public static string WebsiteButtonDescription { + get { + return ResourceManager.GetString("WebsiteButtonDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Auto-retry software encode if GPU encode fails. + /// + public static string AutoRetrySoftwareEncodingOnGpuFailureLabel { + get { + return ResourceManager.GetString("AutoRetrySoftwareEncodingOnGpuFailureLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GPU encode failed, retrying software encode. + /// + public static string GpuEncodingFailedRetryingSoftwareEncode { + get { + return ResourceManager.GetString("GpuEncodingFailedRetryingSoftwareEncode", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Done (software fallback). + /// + public static string ConversionStateDoneSoftwareFallback { + get { + return ResourceManager.GetString("ConversionStateDoneSoftwareFallback", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GPU encoding failed and conversion automatically retried with software encoding.. + /// + public static string GpuEncodingFailedAndRetriedWithSoftwareEncoding { + get { + return ResourceManager.GetString("GpuEncodingFailedAndRetriedWithSoftwareEncoding", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GPU encoding failed. Software fallback also failed. Check hardware acceleration settings or GPU drivers and try again.. + /// + public static string ErrorGpuEncodingFailedAfterFallback { + get { + return ResourceManager.GetString("ErrorGpuEncodingFailedAfterFallback", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GPU encoding failed. Check hardware acceleration settings or GPU drivers and try again.. + /// + public static string ErrorGpuEncodingFailed { + get { + return ResourceManager.GetString("ErrorGpuEncodingFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Conversion failed.. + /// + public static string ErrorConversionFailed { + get { + return ResourceManager.GetString("ErrorConversionFailed", resourceCulture); + } + } /// /// Looks up a localized string similar to None. diff --git a/Application/FileConverter/Properties/Resources.en.resx b/Application/FileConverter/Properties/Resources.en.resx index 7338f453..4c48ed95 100644 --- a/Application/FileConverter/Properties/Resources.en.resx +++ b/Application/FileConverter/Properties/Resources.en.resx @@ -574,4 +574,25 @@ use maj for uppercase version Could not load file converter user settings. Would you like to use default settings ? + + Auto-retry software encode if GPU encode fails + + + GPU encode failed, retrying software encode + + + Done (software fallback) + + + GPU encoding failed and conversion automatically retried with software encoding. + + + GPU encoding failed. Software fallback also failed. Check hardware acceleration settings or GPU drivers and try again. + + + GPU encoding failed. Check hardware acceleration settings or GPU drivers and try again. + + + Conversion failed. + diff --git a/Application/FileConverter/Properties/Resources.resx b/Application/FileConverter/Properties/Resources.resx index 11c79680..0e88a6e4 100644 --- a/Application/FileConverter/Properties/Resources.resx +++ b/Application/FileConverter/Properties/Resources.resx @@ -613,7 +613,28 @@ use maj for uppercase version OpenCL - - Vulkan - - \ No newline at end of file + + Vulkan + + + Auto-retry software encode if GPU encode fails + + + GPU encode failed, retrying software encode + + + Done (software fallback) + + + GPU encoding failed and conversion automatically retried with software encoding. + + + GPU encoding failed. Software fallback also failed. Check hardware acceleration settings or GPU drivers and try again. + + + GPU encoding failed. Check hardware acceleration settings or GPU drivers and try again. + + + Conversion failed. + + diff --git a/Application/FileConverter/Settings.cs b/Application/FileConverter/Settings.cs index 84e8f1b2..620b3342 100644 --- a/Application/FileConverter/Settings.cs +++ b/Application/FileConverter/Settings.cs @@ -23,6 +23,7 @@ public class Settings : ObservableObject, IXmlSerializable private int maximumNumberOfSimultaneousConversions; private bool copyFilesInClipboardAfterConversion = false; private Helpers.HardwareAccelerationMode hardwareAccelerationMode = Helpers.HardwareAccelerationMode.Off; + private bool autoRetrySoftwareEncodingOnGpuFailure = true; public ConversionPreset GetPresetFromName(string presetName) { @@ -237,6 +238,22 @@ public Helpers.HardwareAccelerationMode HardwareAccelerationMode this.OnPropertyChanged(); } } + + [XmlElement] + public bool AutoRetrySoftwareEncodingOnGpuFailure + { + get + { + return this.autoRetrySoftwareEncodingOnGpuFailure; + } + + set + { + this.autoRetrySoftwareEncodingOnGpuFailure = value; + this.OnPropertyChanged(); + } + } + public void OnDeserializationComplete() { this.DurationBetweenEndOfConversionsAndApplicationExit = System.Math.Max(0, System.Math.Min(10, this.DurationBetweenEndOfConversionsAndApplicationExit)); diff --git a/Application/FileConverter/Settings.default.xml b/Application/FileConverter/Settings.default.xml index 136b2518..d492da47 100644 --- a/Application/FileConverter/Settings.default.xml +++ b/Application/FileConverter/Settings.default.xml @@ -2114,4 +2114,5 @@ true false - \ No newline at end of file + true + diff --git a/Application/FileConverter/Views/SettingsWindow.xaml b/Application/FileConverter/Views/SettingsWindow.xaml index a7cd4b87..69a705e5 100644 --- a/Application/FileConverter/Views/SettingsWindow.xaml +++ b/Application/FileConverter/Views/SettingsWindow.xaml @@ -370,6 +370,7 @@ +