diff --git a/FP Auto Video Converter 2/EncoderSettings.cs b/FP Auto Video Converter 2/EncoderSettings.cs new file mode 100644 index 0000000..549c17f --- /dev/null +++ b/FP Auto Video Converter 2/EncoderSettings.cs @@ -0,0 +1,57 @@ +namespace FP_Auto_Video_Converter_2 +{ + enum EncoderKind { Cpu, Nvenc, Qsv } + + // Кожен варіант кодування налаштовується тут окремо і самодостатньо — щоб додати + // новий (напр. AMD AMF) пізніше, досить додати один case у кожен метод нижче плюс + // одну радіокнопку й одну пробу визначення заліза в Form1, більше нічого чіпати не треба. + static class EncoderSettings + { + public static string GetHwaccelPrefix(EncoderKind kind) + { + switch (kind) + { + case EncoderKind.Nvenc: return "-hwaccel cuda "; + default: return ""; // QSV: тестувався без -hwaccel qsv — воно апаратно не вміє декодувати ProRes/4:2:2, додавати без окремої перевірки ризиковано + } + } + + public static string BuildVideoCodecArgs(EncoderKind kind, int crf, string cpuPreset, string nvencPreset) + { + switch (kind) + { + case EncoderKind.Nvenc: + // Калібровано на 4 різних типах контенту (шумне відео, чистий ProRes-майстер, + // анімація, динамічна сцена). Будь-який фіксований офсет -cq (+1..+4), що рятував + // розмір на одному контенті, провалював якість на іншому (найгірше: +4 → VMAF + // впало з 97 до 85 на чистому ProRes). НЕ додавати офсет без повторного калібрування. + return $"-c:v hevc_nvenc -preset {nvencPreset} -tune hq -rc vbr -cq {crf} -b:v 0"; + case EncoderKind.Qsv: + // Калібрований лише на 2 з 4 типів контенту (там -global_quality≈CRF-4 підходив). + // З огляду на урок з NVENC (офсет з малої вибірки виявився шкідливим) — свідомо + // консервативно: CRF напряму, без офсету. Без -preset: тестувався лише з -global_quality. + return $"-c:v hevc_qsv -global_quality {crf}"; + default: + return $"-c:v libx265 -preset {cpuPreset} -crf {crf}"; + } + } + + // hevc_nvenc падає (0 байт, "Failed setup for format cuda") на 4:2:2-джерелах (напр. ProRes). + // QSV такого бага не має (перевірено). + public static bool NeedsPixelFormatFix(EncoderKind kind) => kind == EncoderKind.Nvenc; + + // В той самий -vf, що й scale (через кому) — ніколи окремим -pix_fmt (знищив би 10-біт HDR). + public const string PixelFormatFixFilter = "format=yuv420p|p010le"; + + // Пробне кодування 1 кадру — найпростіший універсальний спосіб перевірити, чи реально + // працює апаратний кодер на цій машині (а не просто "чи є відеокарта в списку"), без + // прав адміністратора і без нових залежностей (WMI тощо). + public static string GetProbeArguments(EncoderKind kind) + { + string codec = kind == EncoderKind.Nvenc ? "hevc_nvenc" : "hevc_qsv"; + // 64x64 виявилось замалим - і NVENC, і QSV відмовляють у кодуванні нижче свого + // мінімального розміру ("Current resolution is unsupported"). 320x240 - безпечний мінімум. + return $"-hide_banner -loglevel error -f lavfi -i testsrc=duration=1:size=320x240:rate=1 -c:v {codec} -f null -"; + } + } +} diff --git a/FP Auto Video Converter 2/FP Auto Video Converter 2.csproj b/FP Auto Video Converter 2/FP Auto Video Converter 2.csproj index 8bc50c7..4c7cd20 100644 --- a/FP Auto Video Converter 2/FP Auto Video Converter 2.csproj +++ b/FP Auto Video Converter 2/FP Auto Video Converter 2.csproj @@ -7,7 +7,7 @@ {31A0748C-2E9A-4E67-B5E8-643C68D8E320} WinExe FP_Auto_Video_Converter_2 - FP Auto Video Converter 2.5 + FP Auto Video Converter 2.7 v3.5 512 true @@ -52,6 +52,7 @@ Form1.cs + diff --git a/FP Auto Video Converter 2/Form1.Designer.cs b/FP Auto Video Converter 2/Form1.Designer.cs index 6ba2256..968ccb8 100644 --- a/FP Auto Video Converter 2/Form1.Designer.cs +++ b/FP Auto Video Converter 2/Form1.Designer.cs @@ -76,6 +76,11 @@ private void InitializeComponent() this.groupBox2 = new System.Windows.Forms.GroupBox(); this.labelCrfMeaning = new System.Windows.Forms.Label(); this.trackBarCRF = new System.Windows.Forms.TrackBar(); + this.groupBoxEncoder = new System.Windows.Forms.GroupBox(); + this.labelEncoderMeaning = new System.Windows.Forms.Label(); + this.radioButtonCpu = new System.Windows.Forms.RadioButton(); + this.radioButtonNvenc = new System.Windows.Forms.RadioButton(); + this.radioButtonQsv = new System.Windows.Forms.RadioButton(); this.label1 = new System.Windows.Forms.Label(); this.buttonStop = new System.Windows.Forms.Button(); this.buttonStart = new System.Windows.Forms.Button(); @@ -93,6 +98,7 @@ private void InitializeComponent() ((System.ComponentModel.ISupportInitialize)(this.trackBarPreset)).BeginInit(); this.groupBox2.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.trackBarCRF)).BeginInit(); + this.groupBoxEncoder.SuspendLayout(); this.panel2.SuspendLayout(); this.SuspendLayout(); // @@ -281,18 +287,19 @@ private void InitializeComponent() this.panel1.Controls.Add(this.checkBoxSkipIfBigger); this.panel1.Controls.Add(this.groupBox3); this.panel1.Controls.Add(this.groupBox2); + this.panel1.Controls.Add(this.groupBoxEncoder); this.panel1.Controls.Add(this.label1); this.panel1.Controls.Add(this.buttonStop); this.panel1.Controls.Add(this.buttonStart); this.panel1.Dock = System.Windows.Forms.DockStyle.Right; this.panel1.Location = new System.Drawing.Point(1171, 0); this.panel1.Name = "panel1"; - this.panel1.Size = new System.Drawing.Size(361, 904); + this.panel1.Size = new System.Drawing.Size(361, 954); this.panel1.TabIndex = 3; // // textBoxReduceFramerateValue // - this.textBoxReduceFramerateValue.Location = new System.Drawing.Point(259, 224); + this.textBoxReduceFramerateValue.Location = new System.Drawing.Point(259, 357); this.textBoxReduceFramerateValue.Name = "textBoxReduceFramerateValue"; this.textBoxReduceFramerateValue.Size = new System.Drawing.Size(53, 22); this.textBoxReduceFramerateValue.TabIndex = 23; @@ -308,7 +315,7 @@ private void InitializeComponent() this.groupBox1.Controls.Add(this.buttonRemoveLessMBit); this.groupBox1.Controls.Add(this.buttonClearSelected); this.groupBox1.Controls.Add(this.buttonRemoveH265); - this.groupBox1.Location = new System.Drawing.Point(16, 294); + this.groupBox1.Location = new System.Drawing.Point(16, 427); this.groupBox1.Name = "groupBox1"; this.groupBox1.Size = new System.Drawing.Size(331, 127); this.groupBox1.TabIndex = 22; @@ -415,7 +422,7 @@ private void InitializeComponent() this.panel3.Controls.Add(this.labelStats); this.panel3.Controls.Add(this.buttonOpenRecycle); this.panel3.Dock = System.Windows.Forms.DockStyle.Bottom; - this.panel3.Location = new System.Drawing.Point(0, 623); + this.panel3.Location = new System.Drawing.Point(0, 673); this.panel3.Name = "panel3"; this.panel3.Size = new System.Drawing.Size(361, 281); this.panel3.TabIndex = 16; @@ -500,7 +507,7 @@ private void InitializeComponent() // // textBoxScaleDownSmallerSide // - this.textBoxScaleDownSmallerSide.Location = new System.Drawing.Point(264, 253); + this.textBoxScaleDownSmallerSide.Location = new System.Drawing.Point(264, 386); this.textBoxScaleDownSmallerSide.Name = "textBoxScaleDownSmallerSide"; this.textBoxScaleDownSmallerSide.Size = new System.Drawing.Size(79, 22); this.textBoxScaleDownSmallerSide.TabIndex = 18; @@ -511,7 +518,7 @@ private void InitializeComponent() // this.checkBoxScaleDown.Checked = true; this.checkBoxScaleDown.CheckState = System.Windows.Forms.CheckState.Checked; - this.checkBoxScaleDown.Location = new System.Drawing.Point(21, 246); + this.checkBoxScaleDown.Location = new System.Drawing.Point(21, 379); this.checkBoxScaleDown.Name = "checkBoxScaleDown"; this.checkBoxScaleDown.Size = new System.Drawing.Size(267, 40); this.checkBoxScaleDown.TabIndex = 17; @@ -524,7 +531,7 @@ private void InitializeComponent() // this.checkBoxReduceFramerate.Checked = true; this.checkBoxReduceFramerate.CheckState = System.Windows.Forms.CheckState.Checked; - this.checkBoxReduceFramerate.Location = new System.Drawing.Point(21, 217); + this.checkBoxReduceFramerate.Location = new System.Drawing.Point(21, 350); this.checkBoxReduceFramerate.Name = "checkBoxReduceFramerate"; this.checkBoxReduceFramerate.Size = new System.Drawing.Size(305, 40); this.checkBoxReduceFramerate.TabIndex = 15; @@ -536,7 +543,7 @@ private void InitializeComponent() // this.checkBoxSkipIfBigger.Checked = true; this.checkBoxSkipIfBigger.CheckState = System.Windows.Forms.CheckState.Checked; - this.checkBoxSkipIfBigger.Location = new System.Drawing.Point(21, 188); + this.checkBoxSkipIfBigger.Location = new System.Drawing.Point(21, 321); this.checkBoxSkipIfBigger.Name = "checkBoxSkipIfBigger"; this.checkBoxSkipIfBigger.Size = new System.Drawing.Size(330, 40); this.checkBoxSkipIfBigger.TabIndex = 14; @@ -550,7 +557,7 @@ private void InitializeComponent() this.groupBox3.Controls.Add(this.labelPresetMeaning); this.groupBox3.Controls.Add(this.trackBarPreset); this.groupBox3.Controls.Add(this.labelPresetName); - this.groupBox3.Location = new System.Drawing.Point(16, 99); + this.groupBox3.Location = new System.Drawing.Point(16, 232); this.groupBox3.Name = "groupBox3"; this.groupBox3.Size = new System.Drawing.Size(331, 80); this.groupBox3.TabIndex = 13; @@ -595,7 +602,7 @@ private void InitializeComponent() // this.groupBox2.Controls.Add(this.labelCrfMeaning); this.groupBox2.Controls.Add(this.trackBarCRF); - this.groupBox2.Location = new System.Drawing.Point(16, 10); + this.groupBox2.Location = new System.Drawing.Point(16, 143); this.groupBox2.Name = "groupBox2"; this.groupBox2.Size = new System.Drawing.Size(331, 83); this.groupBox2.TabIndex = 11; @@ -624,13 +631,74 @@ private void InitializeComponent() this.trackBarCRF.TabIndex = 10; this.trackBarCRF.Value = 30; this.trackBarCRF.ValueChanged += new System.EventHandler(this.trackBarCRF_ValueChanged); - // + // + // groupBoxEncoder + // + this.groupBoxEncoder.Controls.Add(this.labelEncoderMeaning); + this.groupBoxEncoder.Controls.Add(this.radioButtonQsv); + this.groupBoxEncoder.Controls.Add(this.radioButtonNvenc); + this.groupBoxEncoder.Controls.Add(this.radioButtonCpu); + this.groupBoxEncoder.Location = new System.Drawing.Point(16, 10); + this.groupBoxEncoder.Name = "groupBoxEncoder"; + this.groupBoxEncoder.Size = new System.Drawing.Size(331, 123); + this.groupBoxEncoder.TabIndex = 25; + this.groupBoxEncoder.TabStop = false; + this.groupBoxEncoder.Text = "Кодування"; + this.toolTip1.SetToolTip(this.groupBoxEncoder, "GPU працює значно швидше за CPU. Показуються лише ті варіанти, які реально " + + "доступні на цьому комп\'ютері."); + // + // radioButtonCpu + // + this.radioButtonCpu.Checked = true; + this.radioButtonCpu.Location = new System.Drawing.Point(9, 19); + this.radioButtonCpu.Name = "radioButtonCpu"; + this.radioButtonCpu.Size = new System.Drawing.Size(316, 24); + this.radioButtonCpu.TabIndex = 0; + this.radioButtonCpu.TabStop = true; + this.radioButtonCpu.Text = "CPU (libx265)"; + this.radioButtonCpu.UseVisualStyleBackColor = true; + this.radioButtonCpu.CheckedChanged += new System.EventHandler(this.radioButtonEncoder_CheckedChanged); + // + // radioButtonNvenc + // + this.radioButtonNvenc.Location = new System.Drawing.Point(9, 42); + this.radioButtonNvenc.Name = "radioButtonNvenc"; + this.radioButtonNvenc.Size = new System.Drawing.Size(316, 24); + this.radioButtonNvenc.TabIndex = 1; + this.radioButtonNvenc.Text = "NVIDIA GPU (NVENC)"; + this.radioButtonNvenc.UseVisualStyleBackColor = true; + this.radioButtonNvenc.Visible = false; + this.radioButtonNvenc.CheckedChanged += new System.EventHandler(this.radioButtonEncoder_CheckedChanged); + // + // radioButtonQsv + // + this.radioButtonQsv.Location = new System.Drawing.Point(9, 65); + this.radioButtonQsv.Name = "radioButtonQsv"; + this.radioButtonQsv.Size = new System.Drawing.Size(316, 24); + this.radioButtonQsv.TabIndex = 2; + this.radioButtonQsv.Text = "Intel GPU (QSV)"; + this.radioButtonQsv.UseVisualStyleBackColor = true; + this.radioButtonQsv.Visible = false; + this.radioButtonQsv.CheckedChanged += new System.EventHandler(this.radioButtonEncoder_CheckedChanged); + // + // labelEncoderMeaning + // + this.labelEncoderMeaning.AutoSize = false; + this.labelEncoderMeaning.Font = new System.Drawing.Font("Microsoft Sans Serif", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(204))); + this.labelEncoderMeaning.ForeColor = System.Drawing.SystemColors.GrayText; + this.labelEncoderMeaning.Location = new System.Drawing.Point(6, 88); + this.labelEncoderMeaning.Name = "labelEncoderMeaning"; + this.labelEncoderMeaning.Size = new System.Drawing.Size(319, 32); + this.labelEncoderMeaning.TabIndex = 3; + this.labelEncoderMeaning.Text = "CPU: повільніше, але без обмежень до якості/сумісності."; + this.labelEncoderMeaning.TextAlign = System.Drawing.ContentAlignment.TopCenter; + // // label1 // this.label1.AutoSize = true; this.label1.Font = new System.Drawing.Font("Microsoft Sans Serif", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(204))); this.label1.ForeColor = System.Drawing.SystemColors.GrayText; - this.label1.Location = new System.Drawing.Point(18, 500); + this.label1.Location = new System.Drawing.Point(18, 633); this.label1.Name = "label1"; this.label1.Size = new System.Drawing.Size(313, 32); this.label1.TabIndex = 9; @@ -642,7 +710,7 @@ private void InitializeComponent() // this.buttonStop.Enabled = false; this.buttonStop.Image = global::FP_Auto_Video_Converter_2.Properties.Resources.stop; - this.buttonStop.Location = new System.Drawing.Point(275, 440); + this.buttonStop.Location = new System.Drawing.Point(275, 573); this.buttonStop.Name = "buttonStop"; this.buttonStop.Size = new System.Drawing.Size(74, 57); this.buttonStop.TabIndex = 7; @@ -655,7 +723,7 @@ private void InitializeComponent() // this.buttonStart.Font = new System.Drawing.Font("Microsoft Sans Serif", 9F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(204))); this.buttonStart.Image = global::FP_Auto_Video_Converter_2.Properties.Resources.play; - this.buttonStart.Location = new System.Drawing.Point(18, 440); + this.buttonStart.Location = new System.Drawing.Point(18, 573); this.buttonStart.Name = "buttonStart"; this.buttonStart.Size = new System.Drawing.Size(251, 57); this.buttonStart.TabIndex = 3; @@ -691,15 +759,15 @@ private void InitializeComponent() this.AllowDrop = true; this.AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; - this.ClientSize = new System.Drawing.Size(1532, 904); + this.ClientSize = new System.Drawing.Size(1532, 954); this.Controls.Add(this.splitContainer1); this.Controls.Add(this.panel2); this.Controls.Add(this.panel1); this.Font = new System.Drawing.Font("Microsoft Sans Serif", 7.8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(204))); this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.MinimumSize = new System.Drawing.Size(1298, 871); + this.MinimumSize = new System.Drawing.Size(1298, 921); this.Name = "Form1"; - this.Text = "FP AutoVideoConverter 2.5"; + this.Text = "FP AutoVideoConverter 2.7"; this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing); this.Load += new System.EventHandler(this.Form1_Load); this.DragDrop += new System.Windows.Forms.DragEventHandler(this.Form1_DragDrop); @@ -720,6 +788,7 @@ private void InitializeComponent() this.groupBox2.ResumeLayout(false); this.groupBox2.PerformLayout(); ((System.ComponentModel.ISupportInitialize)(this.trackBarCRF)).EndInit(); + this.groupBoxEncoder.ResumeLayout(false); this.panel2.ResumeLayout(false); this.ResumeLayout(false); @@ -753,6 +822,11 @@ private void InitializeComponent() private System.Windows.Forms.Panel panel3; private System.Windows.Forms.CheckBox checkBoxScaleDown; private System.Windows.Forms.TextBox textBoxScaleDownSmallerSide; + private System.Windows.Forms.GroupBox groupBoxEncoder; + private System.Windows.Forms.RadioButton radioButtonCpu; + private System.Windows.Forms.RadioButton radioButtonNvenc; + private System.Windows.Forms.RadioButton radioButtonQsv; + private System.Windows.Forms.Label labelEncoderMeaning; private System.Windows.Forms.Button buttonClearBacups; private System.Windows.Forms.ToolTip toolTip1; private System.Windows.Forms.Button buttonRemoveLessPx; diff --git a/FP Auto Video Converter 2/Form1.cs b/FP Auto Video Converter 2/Form1.cs index 2613021..d7697af 100644 --- a/FP Auto Video Converter 2/Form1.cs +++ b/FP Auto Video Converter 2/Form1.cs @@ -37,6 +37,16 @@ public partial class Form1 : Form Thread workingThread; Process ffmpeg = null; + // Кеш результатів проби апаратних кодерів на цій машині (null = проба ще триває). + static readonly Dictionary encoderAvailability = new Dictionary + { + { EncoderKind.Cpu, true }, + { EncoderKind.Nvenc, null }, + { EncoderKind.Qsv, null }, + }; + Thread nvencProbeThread; + Thread qsvProbeThread; + private Stopwatch convertTime = new Stopwatch(); //convert time private Stopwatch upTime = new Stopwatch(); //app start @@ -169,6 +179,15 @@ private void Form1_Load(object sender, EventArgs e) SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED); log("Комп’ютер не перейде в режим сну, поки працює ця програма."); + // Проба апаратних кодерів у фоні - не затримує старт вікна. + // CPU завжди доступний, тому пробуємо лише NVIDIA й Intel. + nvencProbeThread = new Thread(() => ProbeEncoderAsync(EncoderKind.Nvenc)); + nvencProbeThread.IsBackground = true; + nvencProbeThread.Start(); + qsvProbeThread = new Thread(() => ProbeEncoderAsync(EncoderKind.Qsv)); + qsvProbeThread.IsBackground = true; + qsvProbeThread.Start(); + processArgumentsOnLoad(); @@ -189,6 +208,10 @@ private string getArgumentsHelp() "\n\n -scaleN - Зняти галочку \"Зменшити роздільну здатність до\"." + "\n\n -crf33 - Задати CRF=33 (або інше число)." + "\n\n -preset4 - Задати preset=faster (або інший, число до 10)." + + "\n\n -gpuY - Вибрати кодування NVIDIA GPU (NVENC), якщо доступне на цій машині." + + "\n\n -gpuN - Вибрати кодування CPU." + + "\n\n -qsvY - Вибрати кодування Intel GPU (QSV), якщо доступне на цій машині." + + "\n\n -qsvN - Вибрати кодування CPU." + "\n\n -clearResolution1080 - Очистити зі списку всі файли менші за 1080 по меншій стороні (або інше число)." + "\n\n -clearBitrate10 - Очистити зі списку всі файли бітрейтом менше 10 мегабіт (або інше число)." + "\n\n -clearH265 - Очистити зі списку всі файли що вже в кодеку H265 (HEVC)." + @@ -234,6 +257,14 @@ void processArgumentsOnLoad() if (getArgument("-preset*", out int preset)) if (preset <= trackBarPreset.Maximum && preset >= trackBarPreset.Minimum) trackBarPreset.Value = preset; + if (isArgument("-gpuY")) + SelectEncoderIfAvailable(EncoderKind.Nvenc); + if (isArgument("-gpuN")) + radioButtonCpu.Checked = true; + if (isArgument("-qsvY")) + SelectEncoderIfAvailable(EncoderKind.Qsv); + if (isArgument("-qsvN")) + radioButtonCpu.Checked = true; // Перевірка, чи є адреса папки в аргументах @@ -777,6 +808,9 @@ public void buttonsActive(bool active) checkBoxSkipIfBigger.Enabled = active; textBoxScaleDownSmallerSide.Enabled = active; checkBoxScaleDown.Enabled = active; + radioButtonCpu.Enabled = active; + radioButtonNvenc.Enabled = active; + radioButtonQsv.Enabled = active; buttonExit.Enabled = active; if (active) { @@ -874,6 +908,8 @@ private void buttonStart_Click(object sender, EventArgs e) log("Збір даних..."); int crf = trackBarCRF.Value; getPresetInfo(trackBarPreset.Value, out string preset, out string description); + EncoderKind encoderKind = GetSelectedEncoderKind(); + string gpuPreset = getGpuPreset(trackBarPreset.Value); bool reduceFramerate = checkBoxReduceFramerate.Checked; int.TryParse(textBoxReduceFramerateValue.Text, out int reduceFramerateValue); if (reduceFramerateValue < 1 || reduceFramerateValue > 240) @@ -891,6 +927,7 @@ private void buttonStart_Click(object sender, EventArgs e) } log($"CRF = {crf}"); + log($"encoderKind = {encoderKind}"); if(reduceFramerate) log($"reduceFramerateValue = {reduceFramerateValue}"); log($"skipBigger = {skipBigger}"); @@ -901,7 +938,7 @@ private void buttonStart_Click(object sender, EventArgs e) stop = false; buttonStop.Enabled = true; convertTime.Start(); - workingThread = new Thread(() => runConvertAsync(crf, preset, reduceFramerate, reduceFramerateValue, skipBigger, downscale, downscaleSmallerSide)); + workingThread = new Thread(() => runConvertAsync(crf, preset, encoderKind, gpuPreset, reduceFramerate, reduceFramerateValue, skipBigger, downscale, downscaleSmallerSide)); workingThread.Start(); } catch (Exception ex) @@ -910,7 +947,7 @@ private void buttonStart_Click(object sender, EventArgs e) } } - private void runConvertAsync(int crf, string preset, bool reduceFramerate, int reduceFramerateValue, bool skipBigger, bool downscale, double targetSmallerSide) + private void runConvertAsync(int crf, string preset, EncoderKind encoderKind, string gpuPreset, bool reduceFramerate, int reduceFramerateValue, bool skipBigger, bool downscale, double targetSmallerSide) { try { @@ -944,7 +981,7 @@ private void runConvertAsync(int crf, string preset, bool reduceFramerate, int r //Стиснути відео в тимчасовий файл double videoDuration = GetVideoDurationInSeconds(filePath); - string resolution = ""; + List vfFilters = new List(); if (downscale && smallerSide > targetSmallerSide) { double scaleFactor = targetSmallerSide / smallerSide; //less 1 @@ -960,10 +997,16 @@ private void runConvertAsync(int crf, string preset, bool reduceFramerate, int r newWidth = newHeight; newHeight = temp; } - resolution = $"-vf \"scale={newWidth}:{newHeight}\" "; + vfFilters.Add($"scale={newWidth}:{newHeight}"); } + if (EncoderSettings.NeedsPixelFormatFix(encoderKind)) + vfFilters.Add(EncoderSettings.PixelFormatFixFilter); + string resolution = vfFilters.Count > 0 ? $"-vf \"{string.Join(",", vfFilters.ToArray())}\" " : ""; + string framerate = reduceFramerate? $"-r {reduceFramerateValue} " : ""; - string arguments = $"-i \"{filePath}\" -c:v libx265 -preset {preset} -crf {crf} {framerate}{resolution}-progress pipe:1 \"{tmpfile}\""; + string hwaccel = EncoderSettings.GetHwaccelPrefix(encoderKind); + string videoCodec = EncoderSettings.BuildVideoCodecArgs(encoderKind, crf, preset, gpuPreset); + string arguments = $"{hwaccel}-i \"{filePath}\" {videoCodec} {framerate}{resolution}-progress pipe:1 \"{tmpfile}\""; string exe = "ffmpeg.exe"; log(exe + " " + arguments); ffmpeg = new Process(); @@ -980,10 +1023,16 @@ private void runConvertAsync(int crf, string preset, bool reduceFramerate, int r { log(args.Data); string timeStr = args.Data.Split('=')[1].Split(' ')[0]; //Парсимо поточний Час у форматі hh:mm:ss.xx - TimeSpan currentTime = TimeSpan.Parse(timeStr); - double percentage = (currentTime.TotalSeconds / videoDuration) * 100; - status($"Прогрес файлу: {percentage:F2}%"); - updateStats(); + // На перших кадрах (буфер/lookahead кодека) ffmpeg іноді видає від'ємний + // time= (напр. "-00:00:00.03") - TimeSpan.Parse на такому падає з + // необробленим винятком у фоновому потоці, що аварійно завершує програму. + // TryParse замість Parse - пропускаємо один кадр прогресу, не падаємо. + if (TimeSpan.TryParse(timeStr, out TimeSpan currentTime)) + { + double percentage = (currentTime.TotalSeconds / videoDuration) * 100; + status($"Прогрес файлу: {percentage:F2}%"); + updateStats(); + } } } }; @@ -1259,6 +1308,121 @@ private void trackBarPreset_ValueChanged(object sender, EventArgs e) labelPresetMeaning.Text = description; } + EncoderKind GetSelectedEncoderKind() + { + if (radioButtonNvenc.Checked) return EncoderKind.Nvenc; + if (radioButtonQsv.Checked) return EncoderKind.Qsv; + return EncoderKind.Cpu; + } + + private void radioButtonEncoder_CheckedChanged(object sender, EventArgs e) + { + switch (GetSelectedEncoderKind()) + { + case EncoderKind.Nvenc: + labelEncoderMeaning.Text = "NVIDIA GPU (hevc_nvenc): значно швидше, потребує підтримки NVENC."; + break; + case EncoderKind.Qsv: + labelEncoderMeaning.Text = "Intel GPU (hevc_qsv): значно швидше, потребує підтримки Quick Sync."; + break; + default: + labelEncoderMeaning.Text = "CPU (libx265): повільніше, але без обмежень до якості/сумісності."; + break; + } + } + + // Пробує реально закодувати 1 кадр через апаратний кодер і перевіряє, чи запрацювало. + // Запускається у фоновому потоці з Form1_Load, щоб не затримувати старт вікна. + void ProbeEncoderAsync(EncoderKind kind) + { + bool available = false; + string debugInfo = ""; + try + { + string baseDir = AppDomain.CurrentDomain.BaseDirectory; + Process probe = new Process(); + probe.StartInfo.FileName = Path.Combine(baseDir, "ffmpeg.exe"); + probe.StartInfo.Arguments = EncoderSettings.GetProbeArguments(kind); + probe.StartInfo.WorkingDirectory = baseDir; + probe.StartInfo.UseShellExecute = false; + probe.StartInfo.CreateNoWindow = true; + probe.StartInfo.RedirectStandardOutput = true; + probe.StartInfo.RedirectStandardError = true; + probe.Start(); + string stderr = probe.StandardError.ReadToEnd(); + bool finished = probe.WaitForExit(4000); + if (!finished) + { + try { probe.Kill(); } catch { } + available = false; + debugInfo = "timeout"; + } + else + { + available = probe.ExitCode == 0; + debugInfo = $"exitCode={probe.ExitCode} stderr={stderr}"; + } + } + catch (Exception ex) + { + available = false; + debugInfo = "exception: " + ex.Message; + } + // Спершу записуємо результат, і лише потім чіпаємо UI. Це важливо: коли пробу + // очікують через Join() (SelectEncoderIfAvailable, CLI-автоматизація), UI-потік + // заблокований — якби ми чіпали UI (через Invoke) РАНІШЕ за цей запис, вийшов би + // deadlock (UI чекає на потік проби, потік проби чекає на Invoke до UI). + encoderAvailability[kind] = available; + log($"{kind}: {(available ? "доступний" : "недоступний")}" + (debugInfo.Length > 0 && !available ? $" ({debugInfo})" : "")); + if (available) + RevealEncoderOption(kind); + } + + // Використовується лише з CLI-прапорців (-gpuY/-qsvY) для автоматизації через BAT. + // На відміну від звичайного інтерактивного старту, тут свідомо чекаємо (обмежено) на + // пробу заліза - інакше швидкий автоматизований запуск міг би тихо впасти на CPU, + // навіть якщо потрібне апаратне кодування насправді доступне. + void SelectEncoderIfAvailable(EncoderKind kind) + { + Thread probeThread = kind == EncoderKind.Nvenc ? nvencProbeThread : qsvProbeThread; + probeThread?.Join(4500); + + if (encoderAvailability.TryGetValue(kind, out bool? avail) && avail == true) + { + RadioButton rb = kind == EncoderKind.Nvenc ? radioButtonNvenc : radioButtonQsv; + rb.Visible = true; + rb.Checked = true; + } + else + { + log($"Аргумент запросив {kind}, але цей енкодер недоступний на цій машині. Залишено CPU."); + } + } + + void RevealEncoderOption(EncoderKind kind) + { + RadioButton rb = kind == EncoderKind.Nvenc ? radioButtonNvenc : radioButtonQsv; + // BeginInvoke (не Invoke) навмисно: якщо UI-потік у цю мить чекає в Join() + // (SelectEncoderIfAvailable), синхронний Invoke тут заблокував би обидва потоки + // назавжди. BeginInvoke лише ставить виклик у чергу й не чекає на відповідь. + if (rb.InvokeRequired) + { + rb.BeginInvoke(new MethodInvoker(() => RevealEncoderOption(kind))); + return; + } + rb.Visible = true; + } + + // NVENC-пресети (p1..p7) не збігаються з x264/x265-пресетами. + // Мапимо той самий повзунок 0..9 пропорційно на p1 (найшвидший) .. p7 (найповільніший). + string getGpuPreset(int sliderValue) + { + int max = trackBarPreset.Maximum; + int nvencLevel = 1 + (int)Math.Round(sliderValue * 6.0 / max); + nvencLevel = Math.Max(1, Math.Min(7, nvencLevel)); + return "p" + nvencLevel; + } + private void buttonClearBacups_Click(object sender, EventArgs e) { DialogResult result = MessageBox.Show( @@ -1414,7 +1578,7 @@ private void buttonExit_Click(object sender, EventArgs e) private void buttonAbout_Click(object sender, EventArgs e) { - string description = "FP AutoVideoConverter 2.5" + + string description = "FP AutoVideoConverter 2.7" + "\n" + "\nЦя програма дозволяє автоматизувати процес стиснення відеофайлів у кодек H.265 (HEVC), " + "надаючи зручний інтерфейс для пакетного стиснення великої кількості відео." + @@ -1557,4 +1721,20 @@ private void buttonHelpTerminal_Click(object sender, EventArgs e) - Заповнено інформацію про файл - Допрацьована обробка помилок - програма не вилітає якщо помилка обробки файлу - Допрацьовано отримання формату файлу - не виникає помилки якщо в відео файлі присутні кілька потоків відео + +2.6 +- Додано підтримку апаратного кодування NVIDIA NVENC (GPU) як альтернативу CPU (libx265) +- В інтерфейсі додано перемикач "Використовувати GPU (NVIDIA NVENC)" +- Додано аргументи командного рядка -gpuY / -gpuN +- Оновлено ffmpeg.exe до версії 8.1.2 (gyan.dev full build) - стара збірка 2016 року не мала NVENC/CUDA + +2.7 +- Додано підтримку Intel Quick Sync Video (QSV) як третій варіант кодування поруч з CPU/NVIDIA +- Перемикач замінено з чекбоксу на 3 радіокнопки (CPU/NVIDIA GPU/Intel GPU), перенесено нагору панелі налаштувань +- Додано автовизначення заліза при старті - показуються лише ті варіанти, що реально працюють на цій машині (без потреби в правах адміністратора; перевірка пробним кодуванням, а не через WMI) +- Логіка кожного кодера винесена в окремий файл EncoderSettings.cs - додати новий варіант (напр. AMD AMF) у майбутньому можна без змін в іншому коді +- Виправлено баг: hevc_nvenc падав (0 байт) на 4:2:2-джерелах (напр. ProRes) - тепер формат пікселів нормалізується перед кодуванням, з збереженням 10-біт HDR де він є +- Виправлено deadlock, через який -gpuY міг тихо відкочуватись на CPU при автоматизованому запуску +- Виправлено крах програми при від'ємному time= у виводі ffmpeg (буває на перших кадрах при кодуванні з B-кадрами) +- Додано аргументи командного рядка -qsvY / -qsvN */ \ No newline at end of file diff --git a/FP Auto Video Converter 2/Properties/AssemblyInfo.cs b/FP Auto Video Converter 2/Properties/AssemblyInfo.cs index 2c1365f..3093b51 100644 --- a/FP Auto Video Converter 2/Properties/AssemblyInfo.cs +++ b/FP Auto Video Converter 2/Properties/AssemblyInfo.cs @@ -6,7 +6,7 @@ // Общие сведения об этой сборке предоставляются следующим набором // набора атрибутов. Измените значения этих атрибутов для изменения сведений, // связанных со сборкой. -[assembly: AssemblyTitle("FP Auto Video Converter 2.5")] +[assembly: AssemblyTitle("FP Auto Video Converter 2.7")] [assembly: AssemblyDescription("Auto Video Converter")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("Dr. Failov")] @@ -33,6 +33,6 @@ // Можно задать все значения или принять номера сборки и редакции по умолчанию // используя "*", как показано ниже: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2.5.0.0")] -[assembly: AssemblyFileVersion("2.5.0.0")] +[assembly: AssemblyVersion("2.7.0.0")] +[assembly: AssemblyFileVersion("2.7.0.0")] [assembly: NeutralResourcesLanguage("uk")] diff --git a/README.md b/README.md index 780d125..9975fb1 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

-# 🎬 FP Auto Video Converter 2.5 +# 🎬 FP Auto Video Converter 2.7

@@ -22,7 +22,8 @@ ✅ **Можливість запуску з командного рядка** – для автоматизації процесу. ✅ **Підтримка різних відеоформатів** – програма працює з багатьма популярними форматами. ✅ **Пакетна обробка файлів** – можна додавати **цілі папки** для обробки. -✅ **Гнучкі налаштування** – дозволяє задавати **параметри стиснення**. +✅ **Гнучкі налаштування** – дозволяє задавати **параметри стиснення**. +✅ **Апаратне прискорення** – окрім CPU підтримує **NVIDIA (NVENC)** та **Intel Quick Sync (QSV)**; програма сама визначає, що доступно на вашому ПК. --- @@ -90,6 +91,8 @@ - `-scaleN` — Зняти галочку **"Зменшити роздільну здатність до"**. - `-crf33` — Задати **CRF=33** (або інше число). - `-preset4` — Задати **preset=faster** (або інший, число до 10). +- `-gpuY` / `-gpuN` — Обрати кодування через **NVIDIA GPU (NVENC)** / повернутись на CPU. +- `-qsvY` / `-qsvN` — Обрати кодування через **Intel GPU (QSV)** / повернутись на CPU. ## 🗑 Очищення списку файлів - `-clearResolution1080` — Видалити файли, менші за **1080** по меншій стороні (або інше число). @@ -110,4 +113,4 @@ --- **Розробник:** Dr. Failov -📅 2025 +📅 2025-2026 diff --git a/Releases/FP Auto Video Converter 2.6 Release 20260715.zip b/Releases/FP Auto Video Converter 2.6 Release 20260715.zip new file mode 100644 index 0000000..ec694d2 Binary files /dev/null and b/Releases/FP Auto Video Converter 2.6 Release 20260715.zip differ diff --git a/Releases/FP Auto Video Converter 2.7 Release 20260716.zip b/Releases/FP Auto Video Converter 2.7 Release 20260716.zip new file mode 100644 index 0000000..b680318 Binary files /dev/null and b/Releases/FP Auto Video Converter 2.7 Release 20260716.zip differ