diff --git a/Application/FileConverter/Application.xaml.cs b/Application/FileConverter/Application.xaml.cs index 2fe59f6e..e89a2d6f 100644 --- a/Application/FileConverter/Application.xaml.cs +++ b/Application/FileConverter/Application.xaml.cs @@ -31,7 +31,7 @@ public partial class Application : System.Windows.Application private static readonly Version Version = new Version() { Major = 0, - Minor = 6, + Minor = 7, Patch = 0, }; @@ -41,6 +41,7 @@ public partial class Application : System.Windows.Application private bool needToRunConversionThread; private bool cancelAutoExit; + private bool isSessionEnding; private UpgradeVersionDescription upgradeVersionDescription = null; public Application() @@ -48,6 +49,8 @@ public Application() this.ConvertionJobs = this.conversionJobs.AsReadOnly(); } + public event EventHandler OnApplicationTerminate; + public static Version ApplicationVersion { get @@ -89,6 +92,11 @@ public bool Verbose public void CancelAutoExit() { this.cancelAutoExit = true; + + if (this.OnApplicationTerminate != null) + { + this.OnApplicationTerminate.Invoke(this, new ApplicationTerminateArgs(float.NaN)); + } } protected override void OnStartup(StartupEventArgs e) @@ -109,8 +117,8 @@ protected override void OnExit(ExitEventArgs e) base.OnExit(e); Debug.Log("Exit application."); - - if (this.upgradeVersionDescription != null && this.upgradeVersionDescription.NeedToUpgrade) + + if (!this.isSessionEnding && this.upgradeVersionDescription != null && this.upgradeVersionDescription.NeedToUpgrade) { Debug.Log("A new version of file converter has been found: {0}.", this.upgradeVersionDescription.LatestVersion); @@ -154,11 +162,19 @@ protected override void OnExit(ExitEventArgs e) Debug.Release(); } + protected override void OnSessionEnding(SessionEndingCancelEventArgs e) + { + base.OnSessionEnding(e); + + this.isSessionEnding = true; + this.Shutdown(); + } + private void Initialize() { Diagnostics.Debug.Log("File Converter v" + ApplicationVersion.ToString()); - Diagnostics.Debug.Log("The number of processors on this computer is {0}. Set the default number of conversion threads to {0}", Environment.ProcessorCount); - this.numberOfConversionThread = Environment.ProcessorCount; + this.numberOfConversionThread = System.Math.Max(1, Environment.ProcessorCount / 2); + Diagnostics.Debug.Log("The number of processors on this computer is {0}. Set the default number of conversion threads to {0}", this.numberOfConversionThread); // Retrieve arguments. Debug.Log("Retrieve arguments..."); @@ -198,6 +214,11 @@ private void Initialize() for (int index = 1; index < args.Length; index++) { string argument = args[index]; + if (string.IsNullOrEmpty(argument)) + { + continue; + } + if (argument.StartsWith("--")) { // This is an optional parameter. @@ -381,13 +402,33 @@ private void ConvertFiles() allConversionsSucceed &= this.conversionJobs[index].State == ConversionJob.ConversionState.Done; } + if (this.cancelAutoExit) + { + return; + } + if (allConversionsSucceed) { - System.Threading.Thread.Sleep((int)this.Settings.DurationBetweenEndOfConversionsAndApplicationExit * 1000); + float remainingTime = this.Settings.DurationBetweenEndOfConversionsAndApplicationExit; + while (remainingTime > 0f) + { + if (this.OnApplicationTerminate != null) + { + this.OnApplicationTerminate.Invoke(this, new ApplicationTerminateArgs(remainingTime)); + } + + System.Threading.Thread.Sleep(1000); + remainingTime--; - if (this.cancelAutoExit) + if (this.cancelAutoExit) + { + return; + } + } + + if (this.OnApplicationTerminate != null) { - return; + this.OnApplicationTerminate.Invoke(this, new ApplicationTerminateArgs(remainingTime)); } Dispatcher.BeginInvoke((Action)(() => Application.Current.Shutdown())); @@ -413,9 +454,9 @@ private void ExecuteConversionJob(object parameter) { conversionJob.StartConvertion(); } - catch (Exception) + catch (Exception exception) { - throw; + Debug.LogError("Failure during conversion: {0}", exception.ToString()); } if (conversionJob.State == ConversionJob.ConversionState.Done && !System.IO.File.Exists(conversionJob.OutputFilePath)) diff --git a/Application/FileConverter/ApplicationTerminateArgs.cs b/Application/FileConverter/ApplicationTerminateArgs.cs new file mode 100644 index 00000000..67fd2775 --- /dev/null +++ b/Application/FileConverter/ApplicationTerminateArgs.cs @@ -0,0 +1,18 @@ +// License: http://www.gnu.org/licenses/gpl.html GPL version 3. + +namespace FileConverter +{ + public class ApplicationTerminateArgs : System.EventArgs + { + public ApplicationTerminateArgs(float remainingTimeBeforeTermination) + { + this.RemainingTimeBeforeTermination = remainingTimeBeforeTermination; + } + + public float RemainingTimeBeforeTermination + { + get; + private set; + } + } +} \ No newline at end of file diff --git a/Application/FileConverter/ConversionJobs/CancelConversionJobCommand.cs b/Application/FileConverter/ConversionJobs/CancelConversionJobCommand.cs new file mode 100644 index 00000000..7fb2ef50 --- /dev/null +++ b/Application/FileConverter/ConversionJobs/CancelConversionJobCommand.cs @@ -0,0 +1,34 @@ +// License: http://www.gnu.org/licenses/gpl.html GPL version 3. + +namespace FileConverter.ConversionJobs +{ + using System; + using System.Windows.Input; + + public class CancelConversionJobCommand : ICommand + { + private readonly ConversionJob conversionJob; + + public CancelConversionJobCommand(ConversionJob conversionJob) + { + this.conversionJob = conversionJob; + } + + public event EventHandler CanExecuteChanged; + + public bool CanExecute(object parameter) + { + if (this.conversionJob == null) + { + return false; + } + + return this.conversionJob.IsCancelable && this.conversionJob.State == ConversionJob.ConversionState.InProgress; + } + + public void Execute(object parameter) + { + this.conversionJob?.Cancel(); + } + } +} \ No newline at end of file diff --git a/Application/FileConverter/ConversionJobs/ConversionFlags.cs b/Application/FileConverter/ConversionJobs/ConversionFlags.cs index a78abcd3..7e81df3a 100644 --- a/Application/FileConverter/ConversionJobs/ConversionFlags.cs +++ b/Application/FileConverter/ConversionJobs/ConversionFlags.cs @@ -9,6 +9,6 @@ public enum ConversionFlags { None = 0x00, - CdaExtraction = 0x01, + CdDriveExtraction = 0x01, } } \ No newline at end of file diff --git a/Application/FileConverter/ConversionJobs/ConversionJob.cs b/Application/FileConverter/ConversionJobs/ConversionJob.cs index 6f24b550..63cc122c 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob.cs @@ -16,6 +16,7 @@ public class ConversionJob : INotifyPropertyChanged private string errorMessage = string.Empty; private string initialInputPath = string.Empty; private string userState = string.Empty; + private CancelConversionJobCommand cancelCommand; public ConversionJob() { @@ -126,6 +127,31 @@ public ConversionFlags StateFlags protected set; } + public CancelConversionJobCommand CancelCommand + { + get + { + if (this.cancelCommand == null) + { + this.cancelCommand = new CancelConversionJobCommand(this); + } + + return this.cancelCommand; + } + } + + public bool IsCancelable + { + get; + protected set; + } + + protected bool CancelIsRequested + { + get; + private set; + } + protected virtual InputPostConversionAction InputPostConversionAction { get @@ -141,7 +167,7 @@ protected virtual InputPostConversionAction InputPostConversionAction public virtual bool CanStartConversion(ConversionFlags conversionFlags) { - return true; + return (conversionFlags & ConversionFlags.CdDriveExtraction) == 0; } public void PrepareConversion(string inputFilePath, string outputFilePath = null) @@ -225,6 +251,12 @@ public void PrepareConversion(string inputFilePath, string outputFilePath = null return; } + // Check if the input file is located on a cd drive. + if (PathHelpers.IsOnCDDrive(this.InputFilePath)) + { + this.StateFlags = ConversionFlags.CdDriveExtraction; + } + this.Initialize(); if (this.State == ConversionState.Unknown) @@ -252,7 +284,7 @@ public void StartConvertion() Debug.Log("Convert file {0} to {1}.", this.InputFilePath, this.OutputFilePath); this.State = ConversionState.InProgress; - + try { this.Convert(); @@ -262,6 +294,8 @@ public void StartConvertion() this.ConversionFailed(exception.Message); } + this.StateFlags = ConversionFlags.None; + if (this.State == ConversionState.Failed) { this.OnConversionFailed(); @@ -272,6 +306,17 @@ public void StartConvertion() } } + public virtual void Cancel() + { + if (!this.IsCancelable || this.State != ConversionState.InProgress) + { + return; + } + + this.CancelIsRequested = true; + this.ConversionFailed("Canceled."); + } + protected virtual void Convert() { } @@ -284,9 +329,17 @@ protected virtual void OnConversionFailed() { Debug.Log("Conversion Failed."); - if (System.IO.File.Exists(this.OutputFilePath)) + try + { + if (System.IO.File.Exists(this.OutputFilePath)) + { + System.IO.File.Delete(this.OutputFilePath); + } + } + catch (Exception exception) { - System.IO.File.Delete(this.OutputFilePath); + Debug.Log("Can't delete file '{0}' after conversion job failure.", this.OutputFilePath); + Debug.Log("An exception as been thrown: {0}.", exception.ToString()); } } diff --git a/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs b/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs index 64871674..da1f4eaa 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJobFactory.cs @@ -18,6 +18,11 @@ public static ConversionJob Create(ConversionPreset conversionPreset, string inp return new ConversionJob_Ico(conversionPreset); } + if (conversionPreset.OutputType == OutputType.Gif) + { + return new ConversionJob_Gif(conversionPreset); + } + if (PathHelpers.GetExtensionCategory(extension) == PathHelpers.InputCategoryNames.Image) { return new ConversionJob_ImageMagick(conversionPreset); diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs b/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs index be223b98..344ae353 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_ExtractCDA.cs @@ -35,12 +35,7 @@ protected override InputPostConversionAction InputPostConversionAction return InputPostConversionAction.None; } } - - public override bool CanStartConversion(ConversionFlags conversionFlags) - { - return (conversionFlags & ConversionFlags.CdaExtraction) == 0; - } - + protected override void Initialize() { base.Initialize(); @@ -108,8 +103,6 @@ protected override void Initialize() this.compressionConversionJob = ConversionJobFactory.Create(this.ConversionPreset, this.intermediateFilePath); this.compressionConversionJob.PrepareConversion(this.intermediateFilePath, this.OutputFilePath); this.compressionThread = new Thread(this.CompressAsync); - - this.StateFlags = ConversionFlags.CdaExtraction; } protected override void Convert() diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.Converters.cs b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.Converters.cs index 7adca67c..b5172837 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.Converters.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.Converters.cs @@ -3,11 +3,84 @@ namespace FileConverter.ConversionJobs { using System; + using System.Globalization; using FileConverter.Controls; public partial class ConversionJob_FFMPEG { + private static string Encapsulate(string optionName, string args) + { + if (string.IsNullOrEmpty(args)) + { + return string.Empty; + } + + return string.Format("{0} \"{1}\"", optionName, args); + } + + private static string ComputeTransformArgs(ConversionPreset conversionPreset) + { + float scaleFactor = conversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.VideoScale); + string scaleArgs = string.Empty; + + if (conversionPreset.OutputType == OutputType.Mkv || conversionPreset.OutputType == OutputType.Mp4) + { + // This presets use h264 codec, the size of the video need to be divisible by 2. + scaleArgs = string.Format("scale=trunc(iw*{0}/2)*2:trunc(ih*{0}/2)*2", scaleFactor.ToString("#.##", CultureInfo.InvariantCulture)); + } + else if (Math.Abs(scaleFactor - 1f) >= 0.005f) + { + scaleArgs = string.Format("scale=iw*{0}:ih*{0}", scaleFactor.ToString("#.##", CultureInfo.InvariantCulture)); + } + + float rotationAngleInDegrees = conversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.VideoRotation); + string rotationArgs = string.Empty; + if (Math.Abs(rotationAngleInDegrees - 0f) >= 0.05f) + { + // Transpose: + // 0: 90 CounterClockwise and vertical flip + // 1: 90 Clockwise + // 2: 90 CounterClockwise + // 3: 90 Clockwise and vertical flip + if (Math.Abs(rotationAngleInDegrees - 90f) <= 0.05f) + { + rotationArgs = "transpose=2"; + } + else if (Math.Abs(rotationAngleInDegrees - 180f) <= 0.05f) + { + rotationArgs = "vflip,hflip"; + } + else if (Math.Abs(rotationAngleInDegrees - 270f) <= 0.05f) + { + rotationArgs = "transpose=1"; + } + else + { + Diagnostics.Debug.LogError("Unsupported rotation: {0}°", rotationAngleInDegrees); + } + } + + // Build vf args content (scale=..., transpose=...) + string transformArgs = string.Empty; + if (!string.IsNullOrEmpty(scaleArgs)) + { + transformArgs += scaleArgs; + } + + if (!string.IsNullOrEmpty(rotationArgs)) + { + if (!string.IsNullOrEmpty(transformArgs)) + { + transformArgs += ","; + } + + transformArgs += rotationArgs; + } + + return transformArgs; + } + /// /// Convert bitrate in mp3 encoder quality index. /// @@ -263,5 +336,17 @@ private string AACBitrateToQualityIndex(int bitrate) throw new Exception("Unknown VBR bitrate."); } + + /// + /// Convert video quality index to H264 constant rate factor. + /// + /// The quality index. + /// Returns the H264 constant rate factor. + /// The range of the quantizer scale is 0-63: lower values mean better quality. + /// https://trac.ffmpeg.org/wiki/Encode/VP9 + private int WebmQualityToCRF(int quality) + { + return 63 - quality; + } } } diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs index be5b200d..7e3405d1 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_FFMPEG.cs @@ -3,6 +3,7 @@ namespace FileConverter.ConversionJobs { using System; + using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; @@ -20,14 +21,18 @@ public partial class ConversionJob_FFMPEG : ConversionJob private ProcessStartInfo ffmpegProcessStartInfo; + private List ffmpegArgumentStringByPass = new List(); + public ConversionJob_FFMPEG() : base() { + this.IsCancelable = true; } public ConversionJob_FFMPEG(ConversionPreset conversionPreset) : base(conversionPreset) { + this.IsCancelable = true; } - + protected virtual string FfmpegPath { get @@ -63,7 +68,11 @@ protected override void Initialize() this.ffmpegProcessStartInfo.RedirectStandardOutput = true; this.ffmpegProcessStartInfo.RedirectStandardError = true; - string arguments = string.Empty; + this.FillFFMpegArgumentsList(); + } + + protected virtual void FillFFMpegArgumentsList() + { switch (this.ConversionPreset.OutputType) { case OutputType.Aac: @@ -71,7 +80,9 @@ protected override void Initialize() // https://trac.ffmpeg.org/wiki/Encode/AAC int audioEncodingBitrate = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.AudioBitrate); string encoderArgs = string.Format("-c:a aac -q:a {0}", this.AACBitrateToQualityIndex(audioEncodingBitrate)); - arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + string arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + + this.ffmpegArgumentStringByPass.Add(new FFMpegPass(arguments)); } break; @@ -82,66 +93,20 @@ protected override void Initialize() int videoEncodingQuality = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.VideoQuality); int audioEncodingBitrate = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.AudioBitrate); - float scaleFactor = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.VideoScale); - string scaleArgs = string.Empty; - if (Math.Abs(scaleFactor - 1f) >= 0.005f) - { - scaleArgs = string.Format("scale=iw*{0}:ih*{0}", scaleFactor.ToString("#.##", CultureInfo.InvariantCulture)); - } - - float rotationAngleInDegrees = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.VideoRotation); - string rotationArgs = string.Empty; - if (Math.Abs(rotationAngleInDegrees - 0f) >= 0.05f) - { - // Transpose: - // 0: 90 CounterClockwise and vertical flip - // 1: 90 Clockwise - // 2: 90 CounterClockwise - // 3: 90 Clockwise and vertical flip - if (Math.Abs(rotationAngleInDegrees - 90f) <= 0.05f) - { - rotationArgs = "transpose=2"; - } - else if (Math.Abs(rotationAngleInDegrees - 180f) <= 0.05f) - { - rotationArgs = "vflip,hflip"; - } - else if (Math.Abs(rotationAngleInDegrees - 270f) <= 0.05f) - { - rotationArgs = "transpose=1"; - } - else - { - Diagnostics.Debug.LogError("Unsupported rotation: {0}°", rotationAngleInDegrees); - } - } + string transformArgs = ConversionJob_FFMPEG.ComputeTransformArgs(this.ConversionPreset); - // Build vf args (-vf "scale=..., transpose=...") - string transformArgs = string.Empty; - if (!string.IsNullOrEmpty(scaleArgs) || !string.IsNullOrEmpty(rotationArgs)) + string audioArgs = "-an"; + if (this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.EnableAudio)) { - transformArgs = "-vf \""; - if (!string.IsNullOrEmpty(scaleArgs)) - { - transformArgs += scaleArgs; - } - - if (!string.IsNullOrEmpty(rotationArgs)) - { - if (!string.IsNullOrEmpty(scaleArgs)) - { - transformArgs += ","; - } - - transformArgs += rotationArgs; - } - - transformArgs += "\""; + audioArgs = string.Format("-c:a libmp3lame -qscale:a {0}", this.MP3VBRBitrateToQualityIndex(audioEncodingBitrate)); } // Compute final arguments. - string encoderArgs = string.Format("-c:v mpeg4 -vtag xvid -qscale:v {0} -c:a libmp3lame -qscale:a {1} {2}", this.MPEG4QualityToQualityIndex(videoEncodingQuality), this.MP3VBRBitrateToQualityIndex(audioEncodingBitrate), transformArgs); - arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + string videoFilteringArgs = ConversionJob_FFMPEG.Encapsulate("-vf", transformArgs); + string encoderArgs = string.Format("-c:v mpeg4 -vtag xvid -qscale:v {0} {1} {2}", this.MPEG4QualityToQualityIndex(videoEncodingQuality), audioArgs, videoFilteringArgs); + string arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + + this.ffmpegArgumentStringByPass.Add(new FFMpegPass(arguments)); } break; @@ -150,7 +115,40 @@ protected override void Initialize() { // http://taer-naguur.blogspot.fr/2013/11/flac-audio-encoding-with-ffmpeg.html string encoderArgs = string.Format("-compression_level 12"); + string arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + + this.ffmpegArgumentStringByPass.Add(new FFMpegPass(arguments)); + } + + break; + + case OutputType.Gif: + { + // http://blog.pkh.me/p/21-high-quality-gif-with-ffmpeg.html + string fileName = Path.GetFileName(this.InputFilePath); + string tempPath = Path.GetTempPath(); + string paletteFilePath = PathHelpers.GenerateUniquePath(tempPath + fileName + " - palette.png"); + + string transformArgs = ConversionJob_FFMPEG.ComputeTransformArgs(this.ConversionPreset); + + // fps. + int framesPerSecond = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.VideoFramesPerSecond); + if (!string.IsNullOrEmpty(transformArgs)) + { + transformArgs += ","; + } + + transformArgs += string.Format("fps={0}", framesPerSecond); + + // Generate palette. + string encoderArgs = string.Format("-vf \"{0},palettegen\"", transformArgs); + string arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, paletteFilePath, encoderArgs); + this.ffmpegArgumentStringByPass.Add(new FFMpegPass("Indexing colors", arguments, paletteFilePath)); + + // Create gif. + encoderArgs = string.Format("-i \"{0}\" -lavfi \"{1},paletteuse\"", paletteFilePath, transformArgs); arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + this.ffmpegArgumentStringByPass.Add(new FFMpegPass(arguments)); } break; @@ -158,7 +156,9 @@ protected override void Initialize() case OutputType.Ico: { string encoderArgs = string.Empty; - arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + string arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + + this.ffmpegArgumentStringByPass.Add(new FFMpegPass(arguments)); } break; @@ -176,7 +176,9 @@ protected override void Initialize() string encoderArgs = string.Format("-q:v {0} {1}", this.JPGQualityToQualityIndex(encodingQuality), scaleArgs); - arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + string arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + + this.ffmpegArgumentStringByPass.Add(new FFMpegPass(arguments)); } break; @@ -200,10 +202,12 @@ protected override void Initialize() break; } - arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + string arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + + this.ffmpegArgumentStringByPass.Add(new FFMpegPass(arguments)); } - break; + break; case OutputType.Mkv: case OutputType.Mp4: @@ -214,65 +218,25 @@ protected override void Initialize() string videoEncodingSpeed = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.VideoEncodingSpeed); int audioEncodingBitrate = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.AudioBitrate); - float scaleFactor = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.VideoScale); - string scaleArgs = string.Empty; - if (Math.Abs(scaleFactor - 1f) >= 0.005f) - { - scaleArgs = string.Format("scale=iw*{0}:ih*{0}", scaleFactor.ToString("#.##", CultureInfo.InvariantCulture)); - } + string transformArgs = ConversionJob_FFMPEG.ComputeTransformArgs(this.ConversionPreset); + string videoFilteringArgs = ConversionJob_FFMPEG.Encapsulate("-vf", transformArgs); - float rotationAngleInDegrees = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.VideoRotation); - string rotationArgs = string.Empty; - if (Math.Abs(rotationAngleInDegrees - 0f) >= 0.05f) + string audioArgs = "-an"; + if (this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.EnableAudio)) { - // Transpose: - // 0: 90 CounterClockwise and vertical flip - // 1: 90 Clockwise - // 2: 90 CounterClockwise - // 3: 90 Clockwise and vertical flip - if (Math.Abs(rotationAngleInDegrees - 90f) <= 0.05f) - { - rotationArgs = "transpose=2"; - } - else if (Math.Abs(rotationAngleInDegrees - 180f) <= 0.05f) - { - rotationArgs = "vflip,hflip"; - } - else if (Math.Abs(rotationAngleInDegrees - 270f) <= 0.05f) - { - rotationArgs = "transpose=1"; - } - else - { - Diagnostics.Debug.LogError("Unsupported rotation: {0}°", rotationAngleInDegrees); - } + audioArgs = string.Format("-c:a aac -qscale:a {0}", this.AACBitrateToQualityIndex(audioEncodingBitrate)); } - // Build vf args (-vf "scale=..., transpose=...") - string transformArgs = string.Empty; - if (!string.IsNullOrEmpty(scaleArgs) || !string.IsNullOrEmpty(rotationArgs)) - { - transformArgs = "-vf \""; - if (!string.IsNullOrEmpty(scaleArgs)) - { - transformArgs += scaleArgs; - } - - if (!string.IsNullOrEmpty(rotationArgs)) - { - if (!string.IsNullOrEmpty(scaleArgs)) - { - transformArgs += ","; - } - - transformArgs += rotationArgs; - } + string encoderArgs = string.Format( + "-c:v libx264 -preset {0} -crf {1} {2} {3}", + this.H264EncodingSpeedToPreset(videoEncodingSpeed), + this.H264QualityToCRF(videoEncodingQuality), + audioArgs, + videoFilteringArgs); - transformArgs += "\""; - } + string arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); - string encoderArgs = string.Format("-c:v libx264 -preset {0} -crf {1} -c:a aac -q:a {2} {3}", this.H264EncodingSpeedToPreset(videoEncodingSpeed), this.H264QualityToCRF(videoEncodingQuality), this.AACBitrateToQualityIndex(audioEncodingBitrate), transformArgs); - arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + this.ffmpegArgumentStringByPass.Add(new FFMpegPass(arguments)); } break; @@ -280,8 +244,10 @@ protected override void Initialize() case OutputType.Ogg: { int encodingQuality = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.AudioBitrate); - string encoderArgs = string.Format("-codec:a libvorbis -qscale:a {0}", this.OGGVBRBitrateToQualityIndex(encodingQuality)); - arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + string encoderArgs = string.Format("-vn -codec:a libvorbis -qscale:a {0}", this.OGGVBRBitrateToQualityIndex(encodingQuality)); + string arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + + this.ffmpegArgumentStringByPass.Add(new FFMpegPass(arguments)); } break; @@ -298,7 +264,9 @@ protected override void Initialize() // http://www.howtogeek.com/203979/is-the-png-format-lossless-since-it-has-a-compression-parameter/ string encoderArgs = string.Format("-compression_level 100 {0}", scaleArgs); - arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + string arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + + this.ffmpegArgumentStringByPass.Add(new FFMpegPass(arguments)); } break; @@ -307,21 +275,69 @@ protected override void Initialize() { EncodingMode encodingMode = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.AudioEncodingMode); string encoderArgs = string.Format("-acodec {0}", this.WAVEncodingToCodecArgument(encodingMode)); - arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + string arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + + this.ffmpegArgumentStringByPass.Add(new FFMpegPass(arguments)); + } + + break; + + case OutputType.Webm: + { + // https://trac.ffmpeg.org/wiki/Encode/VP9 + int videoEncodingQuality = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.VideoQuality); + int audioEncodingQuality = this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.AudioBitrate); + + string encodingArgs = string.Empty; + if (videoEncodingQuality == 63) + { + // Replace maximum quality settings by lossless compression. + encodingArgs = "-lossless 1"; + } + else + { + encodingArgs = string.Format("-crf {0} -b:v 0", this.WebmQualityToCRF(videoEncodingQuality)); + } + + string transformArgs = ConversionJob_FFMPEG.ComputeTransformArgs(this.ConversionPreset); + string videoFilteringArgs = ConversionJob_FFMPEG.Encapsulate("-vf", transformArgs); + + string audioArgs = "-an"; + if (this.ConversionPreset.GetSettingsValue(ConversionPreset.ConversionSettingKeys.EnableAudio)) + { + audioArgs = string.Format("-c:a libvorbis -qscale:a {0}", this.OGGVBRBitrateToQualityIndex(audioEncodingQuality)); + } + + string encoderArgs = string.Format( + "-c:v libvpx-vp9 {0} {1} {2}", + encodingArgs, + audioArgs, + videoFilteringArgs); + + string arguments = string.Format("-n -stats -i \"{0}\" {2} \"{1}\"", this.InputFilePath, this.OutputFilePath, encoderArgs); + + this.ffmpegArgumentStringByPass.Add(new FFMpegPass(arguments)); } break; default: - throw new NotImplementedException("Converter not implemented for output file type " + this.ConversionPreset.OutputType); + throw new NotImplementedException("Converter not implemented for output file type " + + this.ConversionPreset.OutputType); } - if (string.IsNullOrEmpty(arguments)) + if (this.ffmpegArgumentStringByPass.Count == 0) { - throw new Exception("Invalid ffmpeg process arguments."); + throw new Exception("No ffmpeg arguments generated."); } - this.ffmpegProcessStartInfo.Arguments = arguments; + for (int index = 0; index < this.ffmpegArgumentStringByPass.Count; index++) + { + if (string.IsNullOrEmpty(this.ffmpegArgumentStringByPass[index].Arguments)) + { + throw new Exception("Invalid ffmpeg process arguments."); + } + } } protected override void Convert() @@ -331,37 +347,63 @@ protected override void Convert() throw new Exception("The conversion preset must be valid."); } - this.UserState = "Conversion"; + for (int index = 0; index < this.ffmpegArgumentStringByPass.Count; index++) + { + FFMpegPass currentPass = this.ffmpegArgumentStringByPass[index]; - Diagnostics.Debug.Log("Execute command: {0} {1}.", this.ffmpegProcessStartInfo.FileName, this.ffmpegProcessStartInfo.Arguments); - Diagnostics.Debug.Log(string.Empty); + this.UserState = currentPass.Name; + this.ffmpegProcessStartInfo.Arguments = currentPass.Arguments; - try - { - using (Process exeProcess = Process.Start(this.ffmpegProcessStartInfo)) + Diagnostics.Debug.Log("Execute command: {0} {1}.", this.ffmpegProcessStartInfo.FileName, this.ffmpegProcessStartInfo.Arguments); + Diagnostics.Debug.Log(string.Empty); + + try { - using (StreamReader reader = exeProcess.StandardError) + using (Process exeProcess = Process.Start(this.ffmpegProcessStartInfo)) { - while (!reader.EndOfStream) + using (StreamReader reader = exeProcess.StandardError) { - string result = reader.ReadLine(); + while (!reader.EndOfStream) + { + if (this.CancelIsRequested && !exeProcess.HasExited) + { + exeProcess.Kill(); + } - this.ParseFFMPEGOutput(result); + string result = reader.ReadLine(); - Diagnostics.Debug.Log("ffmpeg output: {0}", result); + this.ParseFFMPEGOutput(result); + + Diagnostics.Debug.Log("ffmpeg output: {0}", result); + } } - } - exeProcess.WaitForExit(); + exeProcess.WaitForExit(); + } + } + catch + { + this.ConversionFailed("Failed to launch FFMPEG process."); + throw; } - } - catch - { - this.ConversionFailed("Failed to launch FFMPEG process."); - throw; } Diagnostics.Debug.Log(string.Empty); + + // Clean intermediate files. + for (int index = 0; index < this.ffmpegArgumentStringByPass.Count; index++) + { + FFMpegPass currentPass = this.ffmpegArgumentStringByPass[index]; + + if (string.IsNullOrEmpty(currentPass.FileToDelete)) + { + continue; + } + + Diagnostics.Debug.Log("Delete intermediate file {0}.", currentPass.FileToDelete); + + File.Delete(currentPass.FileToDelete); + } } private void ParseFFMPEGOutput(string input) @@ -378,26 +420,58 @@ private void ParseFFMPEGOutput(string input) return; } - match = this.progressRegex.Match(input); - if (match.Success && match.Groups.Count >= 7) + if (this.fileDuration.Ticks > 0) { - int size = int.Parse(match.Groups[1].Value); - int hours = int.Parse(match.Groups[2].Value); - int minutes = int.Parse(match.Groups[3].Value); - int seconds = int.Parse(match.Groups[4].Value); - int milliseconds = int.Parse(match.Groups[5].Value); - float bitrate = 0f; - float.TryParse(match.Groups[6].Value, out bitrate); + match = this.progressRegex.Match(input); + if (match.Success && match.Groups.Count >= 7) + { + int size = int.Parse(match.Groups[1].Value); + int hours = int.Parse(match.Groups[2].Value); + int minutes = int.Parse(match.Groups[3].Value); + int seconds = int.Parse(match.Groups[4].Value); + int milliseconds = int.Parse(match.Groups[5].Value) * 10; + float bitrate = 0f; + float.TryParse(match.Groups[6].Value, out bitrate); + + this.actualConvertedDuration = new TimeSpan(0, hours, minutes, seconds, milliseconds); + + this.Progress = this.actualConvertedDuration.Ticks / (float)this.fileDuration.Ticks; + return; + } + } - this.actualConvertedDuration = new TimeSpan(0, hours, minutes, seconds, milliseconds); + if (input.Contains("Exiting.") || input.Contains("Error") || input.Contains("Unsupported dimensions") || input.Contains("No such file or directory")) + { + if (input.StartsWith("Error while decoding stream") && input.EndsWith("Invalid data found when processing input")) + { + // It is normal for a transport stream to start with a broken frame. + // https://trac.ffmpeg.org/ticket/1622 + } + else + { + this.ConversionFailed(input); + } + } + } - this.Progress = this.actualConvertedDuration.Ticks / (float)this.fileDuration.Ticks; - return; + private struct FFMpegPass + { + public string Name; + public string Arguments; + public string FileToDelete; + + public FFMpegPass(string name, string arguments, string fileToDelete) + { + this.Name = name; + this.Arguments = arguments; + this.FileToDelete = fileToDelete; } - if (input.Contains("Exiting.") || input.Contains("Error") || input.Contains("Unsupported dimensions") || input.Contains("No such file or directory")) + public FFMpegPass(string arguments) { - this.ConversionFailed(input); + this.Name = "Conversion"; + this.Arguments = arguments; + this.FileToDelete = string.Empty; } } } diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_Gif.cs b/Application/FileConverter/ConversionJobs/ConversionJob_Gif.cs new file mode 100644 index 00000000..6d0aaf1a --- /dev/null +++ b/Application/FileConverter/ConversionJobs/ConversionJob_Gif.cs @@ -0,0 +1,122 @@ +// License: http://www.gnu.org/licenses/gpl.html GPL version 3. + +namespace FileConverter.ConversionJobs +{ + using System; + using System.IO; + using System.Threading.Tasks; + + public class ConversionJob_Gif : ConversionJob + { + private string intermediateFilePath = string.Empty; + private ConversionJob pngConversionJob = null; + private ConversionJob gifConversionJob = null; + + public ConversionJob_Gif(ConversionPreset conversionPreset) : base(conversionPreset) + { + } + + protected override void Initialize() + { + base.Initialize(); + + if (this.ConversionPreset == null) + { + throw new Exception("The conversion preset must be valid."); + } + + string extension = System.IO.Path.GetExtension(this.InputFilePath); + extension = extension.ToLowerInvariant().Substring(1, extension.Length - 1); + + string inputFilePath = string.Empty; + + // If the output is an image start to convert it into png before send it to ffmpeg. + if (PathHelpers.GetExtensionCategory(extension) == PathHelpers.InputCategoryNames.Image && extension != "png") + { + // Generate intermediate file path. + string fileName = Path.GetFileName(this.OutputFilePath); + string tempPath = Path.GetTempPath(); + this.intermediateFilePath = PathHelpers.GenerateUniquePath(tempPath + fileName + ".png"); + + // Convert input in png file to send it to ffmpeg for the gif conversion. + ConversionPreset intermediatePreset = new ConversionPreset("To compatible image", OutputType.Png, this.ConversionPreset.InputTypes.ToArray()); + this.pngConversionJob = ConversionJobFactory.Create(intermediatePreset, this.InputFilePath); + this.pngConversionJob.PrepareConversion(this.InputFilePath, this.intermediateFilePath); + + inputFilePath = this.intermediateFilePath; + } + else + { + inputFilePath = this.InputFilePath; + } + + // Convert png file into ico. + this.gifConversionJob = new ConversionJob_FFMPEG(this.ConversionPreset); + this.gifConversionJob.PrepareConversion(inputFilePath, this.OutputFilePath); + } + + protected override void Convert() + { + if (this.ConversionPreset == null) + { + throw new Exception("The conversion preset must be valid."); + } + + Task updateProgress = this.UpdateProgress(); + + if (this.pngConversionJob != null) + { + this.UserState = "Read input image"; + + Diagnostics.Debug.Log(string.Empty); + Diagnostics.Debug.Log("Convert image to PNG (intermediate format)."); + this.pngConversionJob.StartConvertion(); + + if (this.pngConversionJob.State != ConversionState.Done) + { + this.ConversionFailed(this.pngConversionJob.ErrorMessage); + return; + } + } + + Diagnostics.Debug.Log(string.Empty); + Diagnostics.Debug.Log("Convert png intermediate image to gif."); + this.gifConversionJob.StartConvertion(); + + if (this.gifConversionJob.State != ConversionState.Done) + { + this.ConversionFailed(this.gifConversionJob.ErrorMessage); + return; + } + + if (!string.IsNullOrEmpty(this.intermediateFilePath)) + { + Diagnostics.Debug.Log("Delete intermediate file {0}.", this.intermediateFilePath); + + File.Delete(this.intermediateFilePath); + } + + updateProgress.Wait(); + } + + private async Task UpdateProgress() + { + while (this.gifConversionJob.State != ConversionState.Done && + this.gifConversionJob.State != ConversionState.Failed) + { + if (this.pngConversionJob != null && this.pngConversionJob.State == ConversionState.InProgress) + { + this.Progress = this.pngConversionJob.Progress; + } + + if (this.gifConversionJob != null && this.gifConversionJob.State == ConversionState.InProgress) + { + this.Progress = this.gifConversionJob.Progress; + this.UserState = this.gifConversionJob.UserState; + } + + await Task.Delay(40); + } + } + } +} diff --git a/Application/FileConverter/ConversionJobs/ConversionJob_ImageMagick.cs b/Application/FileConverter/ConversionJobs/ConversionJob_ImageMagick.cs index 81f86c43..204dd5e3 100644 --- a/Application/FileConverter/ConversionJobs/ConversionJob_ImageMagick.cs +++ b/Application/FileConverter/ConversionJobs/ConversionJob_ImageMagick.cs @@ -60,7 +60,7 @@ protected override void Convert() { int referenceSize = System.Math.Min(image.Width, image.Height); int size = 2; - while (size * 2 < referenceSize) + while (size * 2 <= referenceSize) { size *= 2; } diff --git a/Application/FileConverter/ConversionPreset/ConversionPreset.cs b/Application/FileConverter/ConversionPreset/ConversionPreset.cs index 2c65a7b0..38add562 100644 --- a/Application/FileConverter/ConversionPreset/ConversionPreset.cs +++ b/Application/FileConverter/ConversionPreset/ConversionPreset.cs @@ -96,6 +96,13 @@ public OutputType OutputType } } + [XmlAttribute] + public bool IsDefaultSettings + { + get; + set; + } = false; + [XmlElement] public List InputTypes { @@ -397,121 +404,13 @@ private void CoerceInputTypes() private bool IsRelevantSetting(string settingsKey) { - switch (this.OutputType) - { - case OutputType.Aac: - switch (settingsKey) - { - case ConversionPreset.ConversionSettingKeys.AudioBitrate: - return true; - } - - break; - - case OutputType.Avi: - switch (settingsKey) - { - case ConversionPreset.ConversionSettingKeys.AudioBitrate: - case ConversionPreset.ConversionSettingKeys.VideoQuality: - case ConversionPreset.ConversionSettingKeys.VideoScale: - case ConversionPreset.ConversionSettingKeys.VideoRotation: - return true; - } - - break; - - case OutputType.Flac: - case OutputType.Ico: - break; - - case OutputType.Jpg: - switch (settingsKey) - { - case ConversionPreset.ConversionSettingKeys.ImageQuality: - case ConversionPreset.ConversionSettingKeys.ImageScale: - case ConversionPreset.ConversionSettingKeys.ImageRotation: - case ConversionPreset.ConversionSettingKeys.ImageClampSizePowerOf2: - return true; - } - - break; - - case OutputType.Mkv: - switch (settingsKey) - { - case ConversionPreset.ConversionSettingKeys.AudioBitrate: - case ConversionPreset.ConversionSettingKeys.VideoQuality: - case ConversionPreset.ConversionSettingKeys.VideoScale: - case ConversionPreset.ConversionSettingKeys.VideoRotation: - case ConversionPreset.ConversionSettingKeys.VideoEncodingSpeed: - return true; - } - - break; - - case OutputType.Mp3: - switch (settingsKey) - { - case ConversionPreset.ConversionSettingKeys.AudioEncodingMode: - case ConversionPreset.ConversionSettingKeys.AudioBitrate: - return true; - } - - break; - - case OutputType.Mp4: - switch (settingsKey) - { - case ConversionPreset.ConversionSettingKeys.AudioBitrate: - case ConversionPreset.ConversionSettingKeys.VideoQuality: - case ConversionPreset.ConversionSettingKeys.VideoScale: - case ConversionPreset.ConversionSettingKeys.VideoRotation: - case ConversionPreset.ConversionSettingKeys.VideoEncodingSpeed: - return true; - } - - break; - - case OutputType.Ogg: - switch (settingsKey) - { - case ConversionPreset.ConversionSettingKeys.AudioBitrate: - return true; - } - - break; - - case OutputType.Png: - switch (settingsKey) - { - case ConversionPreset.ConversionSettingKeys.ImageScale: - case ConversionPreset.ConversionSettingKeys.ImageRotation: - case ConversionPreset.ConversionSettingKeys.ImageClampSizePowerOf2: - case ConversionPreset.ConversionSettingKeys.ImageMaximumSize: - return true; - } - - break; - - case OutputType.Wav: - switch (settingsKey) - { - case ConversionPreset.ConversionSettingKeys.AudioEncodingMode: - return true; - } - - break; - - default: - Debug.LogError("Relevant settings '{1}' are not define for output type {0}.", this.OutputType, settingsKey); - break; - } - - return false; + return this.Settings.ContainsKey(settingsKey); } private void InitializeDefaultSettings(OutputType outputType) { + this.settings.Clear(); + switch (outputType) { case OutputType.Aac: @@ -522,6 +421,12 @@ private void InitializeDefaultSettings(OutputType outputType) case OutputType.Flac: break; + case OutputType.Gif: + this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoScale, "1"); + this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoRotation, "0"); + this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoFramesPerSecond, "15"); + break; + case OutputType.Png: this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.ImageScale, "1"); this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.ImageRotation, "0"); @@ -538,6 +443,7 @@ private void InitializeDefaultSettings(OutputType outputType) break; case OutputType.Avi: + this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.EnableAudio, "True"); this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoQuality, "20"); this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoScale, "1"); this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoRotation, "0"); @@ -545,6 +451,7 @@ private void InitializeDefaultSettings(OutputType outputType) break; case OutputType.Mkv: + this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.EnableAudio, "True"); this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoQuality, "28"); this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoEncodingSpeed, "Medium"); this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoScale, "1"); @@ -558,6 +465,7 @@ private void InitializeDefaultSettings(OutputType outputType) break; case OutputType.Mp4: + this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.EnableAudio, "True"); this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoQuality, "28"); this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoEncodingSpeed, "Medium"); this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoScale, "1"); @@ -573,6 +481,14 @@ private void InitializeDefaultSettings(OutputType outputType) this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.AudioEncodingMode, EncodingMode.Wav16.ToString(), true); break; + case OutputType.Webm: + this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.EnableAudio, "True"); + this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.AudioBitrate, "160"); + this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoQuality, "40"); + this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoScale, "1"); + this.InitializeSettingsValue(ConversionPreset.ConversionSettingKeys.VideoRotation, "0"); + break; + default: throw new System.Exception("Missing default settings for type " + outputType); } @@ -734,6 +650,10 @@ public struct ConversionSettingKeys public const string VideoEncodingSpeed = "VideoEncodingSpeed"; public const string VideoScale = "VideoScale"; public const string VideoRotation = "VideoRotation"; + public const string VideoFramesPerSecond = "VideoFramesPerSecond"; + + public const string EnableAudio = "EnableAudio"; + public const string EnableVideo = "EnableVideo"; } } } diff --git a/Application/FileConverter/Diagnostics/Debug.cs b/Application/FileConverter/Diagnostics/Debug.cs index 396fe78a..2eb8fdfa 100644 --- a/Application/FileConverter/Diagnostics/Debug.cs +++ b/Application/FileConverter/Diagnostics/Debug.cs @@ -22,7 +22,7 @@ static Debug() // Delete old diagnostics folder (1 day). DateTime expirationDate = DateTime.Now.Subtract(new TimeSpan(1, 0, 0, 0)); - string[] diagnosticsDirectories = Directory.GetDirectories(path, "Diagnostics.*"); + string[] diagnosticsDirectories = Directory.GetDirectories(path, "Diagnostics-*"); for (int index = 0; index < diagnosticsDirectories.Length; index++) { string directory = diagnosticsDirectories[index]; @@ -33,7 +33,9 @@ static Debug() } } - Debug.diagnosticsFolderPath = Path.Combine(path, "Diagnostics." + Process.GetCurrentProcess().Id); + string diagnosticsFolderName = $"Diagnostics-{DateTime.Now.Hour}h{DateTime.Now.Minute}m{DateTime.Now.Second}s"; + + Debug.diagnosticsFolderPath = Path.Combine(path, diagnosticsFolderName); Debug.diagnosticsFolderPath = PathHelpers.GenerateUniquePath(Debug.diagnosticsFolderPath); Directory.CreateDirectory(Debug.diagnosticsFolderPath); } diff --git a/Application/FileConverter/FileConverter.csproj b/Application/FileConverter/FileConverter.csproj index d24f7ecc..3210eb68 100644 --- a/Application/FileConverter/FileConverter.csproj +++ b/Application/FileConverter/FileConverter.csproj @@ -87,14 +87,17 @@ MSBuild:Compile Designer + EncodingQualitySliderControl.xaml + + @@ -109,6 +112,7 @@ Resources.en.resx True True + True @@ -125,6 +129,7 @@ + @@ -233,6 +238,15 @@ + + + + + + + + + copy /Y "$(SolutionDir)Middleware\ffmpeg.exe" "$(TargetDir)ffmpeg.exe" diff --git a/Application/FileConverter/OutputType.cs b/Application/FileConverter/OutputType.cs index fbca11ac..e383e1b0 100644 --- a/Application/FileConverter/OutputType.cs +++ b/Application/FileConverter/OutputType.cs @@ -9,6 +9,7 @@ public enum OutputType Aac, Avi, Flac, + Gif, Ico, Jpg, Mkv, @@ -17,5 +18,6 @@ public enum OutputType Ogg, Png, Wav, + Webm, } } \ No newline at end of file diff --git a/Application/FileConverter/PathHelpers.cs b/Application/FileConverter/PathHelpers.cs index 7720bf4b..a0e73cb8 100644 --- a/Application/FileConverter/PathHelpers.cs +++ b/Application/FileConverter/PathHelpers.cs @@ -23,6 +23,28 @@ public static string GetPathDriveLetter(string path) return PathHelpers.driveLetterRegex.Match(path).Groups[0].Value; } + public static bool IsOnCDDrive(string path) + { + string pathDriveLetter = GetPathDriveLetter(path); + if (string.IsNullOrEmpty(pathDriveLetter)) + { + return false; + } + + char driveLetter = pathDriveLetter[0]; + + char[] driveLetters = Ripper.CDDrive.GetCDDriveLetters(); + for (int index = 0; index < driveLetters.Length; index++) + { + if (driveLetters[index] == driveLetter) + { + return true; + } + } + + return false; + } + public static int GetCDATrackNumber(string path) { Match match = PathHelpers.cdaTrackNumberRegex.Match(path); @@ -83,19 +105,23 @@ public static string GetExtensionCategory(string extension) case "flac": case "mp3": case "m4a": + case "oga": case "ogg": case "wav": case "wma": return InputCategoryNames.Audio; + case "3gp": case "avi": case "bik": - case "3gp": case "flv": case "m4v": case "mp4": + case "mpeg": case "mov": case "mkv": + case "ogv": + case "vob": case "webm": case "wmv": return InputCategoryNames.Video; @@ -112,6 +138,9 @@ public static string GetExtensionCategory(string extension) case "svg": case "xcf": return InputCategoryNames.Image; + + case "gif": + return InputCategoryNames.AnimatedImage; } return InputCategoryNames.Misc; @@ -137,13 +166,17 @@ public static bool IsOutputTypeCompatibleWithCategory(OutputType outputType, str case OutputType.Avi: case OutputType.Mkv: case OutputType.Mp4: - return category == InputCategoryNames.Video; - + case OutputType.Webm: + return category == InputCategoryNames.Video || category == InputCategoryNames.AnimatedImage; + case OutputType.Ico: - case OutputType.Png: case OutputType.Jpg: + case OutputType.Png: return category == InputCategoryNames.Image; + case OutputType.Gif: + return category == InputCategoryNames.Image || category == InputCategoryNames.Video || category == InputCategoryNames.AnimatedImage; + default: return false; } @@ -167,6 +200,8 @@ public static class InputCategoryNames public const string Audio = "Audio"; public const string Video = "Video"; public const string Image = "Image"; + public const string AnimatedImage = "Animated Image"; + public const string Misc = "Misc"; } } diff --git a/Application/FileConverter/Resources/CancelIcon.png b/Application/FileConverter/Resources/CancelIcon.png new file mode 100644 index 00000000..1f3948dc Binary files /dev/null and b/Application/FileConverter/Resources/CancelIcon.png differ diff --git a/Application/FileConverter/Resources/FailIcon.png b/Application/FileConverter/Resources/FailIcon.png new file mode 100644 index 00000000..6f9b86f5 Binary files /dev/null and b/Application/FileConverter/Resources/FailIcon.png differ diff --git a/Application/FileConverter/Resources/SuccessIcon.png b/Application/FileConverter/Resources/SuccessIcon.png new file mode 100644 index 00000000..00ce2bf3 Binary files /dev/null and b/Application/FileConverter/Resources/SuccessIcon.png differ diff --git a/Application/FileConverter/Settings.cs b/Application/FileConverter/Settings.cs index 83e126cb..45441603 100644 --- a/Application/FileConverter/Settings.cs +++ b/Application/FileConverter/Settings.cs @@ -129,10 +129,25 @@ public static void PostInstallationInitialization() System.IO.File.Delete(userFilePath); } - if (userSettings != null && userSettings.SerializationVersion != Version) + if (userSettings != null) { - Diagnostics.Debug.Log("File converter settings has been imported from version {0} to version {1}.", userSettings.SerializationVersion, Version); - userSettings.SerializationVersion = Version; + if (userSettings.SerializationVersion != Version) + { + Diagnostics.Debug.Log("File converter settings has been imported from version {0} to version {1}.", userSettings.SerializationVersion, Version); + userSettings.SerializationVersion = Version; + } + + // Remove default settings. + if (userSettings.ConversionPresets != null) + { + for (int index = userSettings.ConversionPresets.Count - 1; index >= 0; index--) + { + if (userSettings.ConversionPresets[index].IsDefaultSettings) + { + userSettings.ConversionPresets.RemoveAt(index); + } + } + } } } @@ -468,7 +483,7 @@ private Settings Merge(Settings settings) { return this; } - + for (int index = 0; index < settings.conversionPresets.Count; index++) { ConversionPreset conversionPreset = settings.conversionPresets[index]; diff --git a/Application/FileConverter/Settings.default.xml b/Application/FileConverter/Settings.default.xml index 9e1facde..fe492c53 100644 --- a/Application/FileConverter/Settings.default.xml +++ b/Application/FileConverter/Settings.default.xml @@ -2,7 +2,7 @@ true 3 - + 3gp avi bik @@ -11,9 +11,13 @@ mkv mov mp4 + mpeg + ogv webm wmv + gif None + @@ -21,7 +25,7 @@ (p)(f) - + avi bik flv @@ -32,7 +36,11 @@ webm wmv m4v + mpeg + ogv + gif None + @@ -40,18 +48,22 @@ (p)(f) - + + 3gp avi bik flv + m4v mkv mov mp4 - 3gp + mpeg + ogv webm wmv - m4v + gif None + @@ -59,18 +71,22 @@ (p)(f) - + + gif + 3gp avi bik flv + m4v mkv mov mp4 - 3gp + mpeg + ogv webm wmv - m4v None + @@ -78,7 +94,7 @@ (p)(f) - + avi bik flv @@ -89,7 +105,11 @@ webm wmv m4v + mpeg + ogv + gif None + @@ -97,7 +117,7 @@ (p)(f) - + avi bik flv @@ -108,54 +128,135 @@ webm wmv m4v + mpeg + ogv + gif None + + + + + + (p)(f) + + + 3gp + avi + bik + flv + m4v + mkv + mov + mp4 + mpeg + ogv + webm + wmv + gif + None + (p)(f) - - aac - aiff - ape - flac - m4a - mp3 - wma + + bmp + exr + ico + jpg + jpeg + png + psd + svg + tga + tiff + xcf + 3gp avi bik flv + m4v mkv mov mp4 - wav - 3gp + mpeg + ogv webm wmv - m4v + gif None - + + + (p)(f) - + + bmp + exr + ico + jpg + jpeg + png + psd + svg + tga + tiff + xcf + 3gp avi bik flv + m4v mkv mov mp4 - wav + mpeg + ogv + webm + wmv + gif + None + + + + (p)(f) + + + aac + aiff ape + flac + m4a + mp3 + wma + wav 3gp + avi + bik + flv + m4v + mkv + mov + mp4 + mpeg + ogv webm wmv + None + + (p)(f) + + + wav + ape wma - m4v + aiff None (p)(f) - + aac aiff ape @@ -174,11 +275,14 @@ webm wmv m4v + oga + ogv + mpeg None (p)(f) - + aac aiff ape @@ -197,12 +301,15 @@ webm wmv m4v + mpeg + ogv + oga None (p)(f) - + aiff ape m4a @@ -221,29 +328,72 @@ webm wmv m4v + mpeg + ogv + oga None (p)(f) - + + vob + None + + + + + + + (p:v)DVD Extraction\(f) + + + vob + None + + + + + + + (p:v)DVD Extraction\(f) + + + vob + None + (p:m)DVD Extraction\(f) + + + vob + None + + (p:m)DVD Extraction\(f) + + + vob + None + + + (p:m)DVD Extraction\(f) + + cda None (p:m)CDA Extraction\(f) - + cda None (p:m)CDA Extraction\(f) - + cda None (p:m)CDA Extraction\(f) - + bmp exr ico @@ -262,7 +412,7 @@ (p)(f) - + bmp exr ico @@ -281,7 +431,7 @@ (p)(f)-small - + bmp exr ico @@ -299,7 +449,7 @@ (p)(f) - + bmp exr ico @@ -317,7 +467,7 @@ (p)(f) - + bmp exr ico @@ -334,9 +484,10 @@ + (p)(f) - + bmp exr ico diff --git a/Application/FileConverter/ValueConverters/FileNameConverter.cs b/Application/FileConverter/ValueConverters/FileNameConverter.cs index b348b1c3..bd364072 100644 --- a/Application/FileConverter/ValueConverters/FileNameConverter.cs +++ b/Application/FileConverter/ValueConverters/FileNameConverter.cs @@ -41,8 +41,12 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur string fileName = System.IO.Path.GetFileName(inputPathWithoutExtension); string parentDirectory = System.IO.Path.GetDirectoryName(inputPathWithoutExtension); - string[] directories = parentDirectory.Split(System.IO.Path.DirectorySeparatorChar); - parentDirectory += System.IO.Path.DirectorySeparatorChar; + if (!parentDirectory.EndsWith(System.IO.Path.DirectorySeparatorChar.ToString())) + { + parentDirectory += System.IO.Path.DirectorySeparatorChar; + } + + string[] directories = parentDirectory.Substring(0, parentDirectory.Length - 1).Split(System.IO.Path.DirectorySeparatorChar); // Generate output path from template. string outputPath = outputFileTemplate; diff --git a/Application/FileConverter/ValueConverters/Generic/ValueToString.cs b/Application/FileConverter/ValueConverters/Generic/ValueToString.cs new file mode 100644 index 00000000..44ffdc26 --- /dev/null +++ b/Application/FileConverter/ValueConverters/Generic/ValueToString.cs @@ -0,0 +1,21 @@ +// License: http://www.gnu.org/licenses/gpl.html GPL version 3. + +namespace FileConverter.ValueConverters.Generic +{ + using System; + using System.Globalization; + using System.Windows.Data; + + public class ValueToString : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value?.ToString(); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/Application/FileConverter/Windows/ConversionSettingsTemplateSelector.cs b/Application/FileConverter/Windows/ConversionSettingsTemplateSelector.cs index f5cff624..ed05e921 100644 --- a/Application/FileConverter/Windows/ConversionSettingsTemplateSelector.cs +++ b/Application/FileConverter/Windows/ConversionSettingsTemplateSelector.cs @@ -49,6 +49,12 @@ public DataTemplate AacSettingsDataTemplate set; } + public DataTemplate GifSettingsDataTemplate + { + get; + set; + } + public DataTemplate JpgSettingsDataTemplate { get; @@ -67,6 +73,12 @@ public DataTemplate MkvSettingsDataTemplate set; } + public DataTemplate WebmSettingsDataTemplate + { + get; + set; + } + public override DataTemplate SelectTemplate(object item, DependencyObject container) { if (item == null) @@ -84,6 +96,9 @@ public override DataTemplate SelectTemplate(object item, DependencyObject contai case OutputType.Avi: return this.AviSettingsDataTemplate; + case OutputType.Gif: + return this.GifSettingsDataTemplate; + case OutputType.Jpg: return this.JpgSettingsDataTemplate; @@ -104,6 +119,9 @@ public override DataTemplate SelectTemplate(object item, DependencyObject contai case OutputType.Wav: return this.WavSettingsDataTemplate; + + case OutputType.Webm: + return this.WebmSettingsDataTemplate; } return this.DefaultDataTemplate; diff --git a/Application/FileConverter/Windows/MainWindow.xaml b/Application/FileConverter/Windows/MainWindow.xaml index 006ba1a1..471f97cf 100644 --- a/Application/FileConverter/Windows/MainWindow.xaml +++ b/Application/FileConverter/Windows/MainWindow.xaml @@ -4,11 +4,20 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:FileConverter" xmlns:valueConverters="clr-namespace:FileConverter.ValueConverters" - xmlns:conversionJobs="clr-namespace:FileConverter.ConversionJobs" mc:Ignorable="d" x:Class="FileConverter.MainWindow" - x:Name="FileConverterMainWindow" Height="500" Width="950" MinHeight="250" MinWidth="450" WindowStartupLocation="CenterScreen" Icon="/FileConverter;component/Resources/ApplicationIcon.ico"> + xmlns:conversionJobs="clr-namespace:FileConverter.ConversionJobs" + xmlns:generic="clr-namespace:FileConverter.ValueConverters.Generic" + mc:Ignorable="d" x:Class="FileConverter.MainWindow" + x:Name="FileConverterMainWindow" Height="500" Width="950" MinHeight="250" MinWidth="500" WindowStartupLocation="CenterScreen" Icon="/FileConverter;component/Resources/ApplicationIcon.ico"> + + + + + + + @@ -27,7 +36,10 @@ -