using System.Windows.Forms; using ScreenRecorder; var app = new RecorderForm(); Application.Run(app); public class RecorderForm : Form { private readonly Button _btnStart; private readonly Button _btnStop; private readonly Label _lblStatus; private readonly Label _lblTimer; private readonly Label _lblFps; private readonly Label _lblQuality; private readonly TrackBar _trackFps; private readonly ComboBox _cmbQuality; private readonly Label _lblFpsVal; private readonly CheckBox _chkHideCursor; private readonly System.Windows.Forms.Timer _timer; private readonly ProgressBar _progressBar; private readonly Label _lblProgress; private CaptureService? _capture; private DateTime _startTime; private string _outputDir = Environment.GetFolderPath(Environment.SpecialFolder.MyVideos); private string? _lastVideoPath; private static string? FindFfmpeg() { var local = Path.Combine(AppContext.BaseDirectory, "ffmpeg.exe"); if (File.Exists(local)) return local; foreach (var dir in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(';')) { var p = Path.Combine(dir.Trim(), "ffmpeg.exe"); if (File.Exists(p)) return p; } return null; } // Value: 0=无损H265, 1-100=MJPEG quality, 10/18=H264有损 private static readonly (string Label, int Value, string FfmpegArgs)[] QualityOptions = [ ("无损 (H.265 CRF 0, yuv444p)", 0, "-c:v libx265 -preset fast -crf 0 -pix_fmt yuv444p -tag:v hvc1"), ("极高画质 (H.264 CRF 10)", 10, "-c:v libx264 -preset fast -crf 10 -pix_fmt yuv420p"), ("高画质 (H.264 CRF 18)", 18, "-c:v libx264 -preset fast -crf 18 -pix_fmt yuv420p"), ("中画质 MJPEG 100", 100, ""), ("低画质 MJPEG 80", 80, ""), ("极低画质 MJPEG 50", 50, ""), ]; public RecorderForm() { Text = "屏幕录制"; Size = new System.Drawing.Size(360, 340); FormBorderStyle = FormBorderStyle.FixedSingle; MaximizeBox = false; StartPosition = FormStartPosition.CenterScreen; _lblStatus = new Label { Text = "就绪", Location = new System.Drawing.Point(20, 12), Size = new System.Drawing.Size(320, 22), Font = new System.Drawing.Font("微软雅黑", 10) }; _lblTimer = new Label { Text = "00:00:00", Location = new System.Drawing.Point(20, 36), Size = new System.Drawing.Size(320, 26), Font = new System.Drawing.Font("Consolas", 15), ForeColor = System.Drawing.Color.DarkBlue }; _lblFps = new Label { Text = "帧率 (fps):", Location = new System.Drawing.Point(20, 74), Size = new System.Drawing.Size(80, 20), Font = new System.Drawing.Font("微软雅黑", 9) }; _trackFps = new TrackBar { Minimum = 1, Maximum = 60, Value = 30, TickFrequency = 5, SmallChange = 1, LargeChange = 5, Location = new System.Drawing.Point(100, 67), Size = new System.Drawing.Size(190, 40) }; _lblFpsVal = new Label { Text = "30", Location = new System.Drawing.Point(295, 74), Size = new System.Drawing.Size(40, 20), Font = new System.Drawing.Font("Consolas", 10), ForeColor = System.Drawing.Color.DarkBlue }; _trackFps.ValueChanged += (_, _) => _lblFpsVal.Text = _trackFps.Value.ToString(); _lblQuality = new Label { Text = "画质:", Location = new System.Drawing.Point(20, 120), Size = new System.Drawing.Size(80, 20), Font = new System.Drawing.Font("微软雅黑", 9) }; _cmbQuality = new ComboBox { Location = new System.Drawing.Point(100, 117), Size = new System.Drawing.Size(230, 26), DropDownStyle = ComboBoxStyle.DropDownList, Font = new System.Drawing.Font("微软雅黑", 9) }; foreach (var (label, _, _) in QualityOptions) _cmbQuality.Items.Add(label); _cmbQuality.SelectedIndex = 0; _chkHideCursor = new CheckBox { Text = "隐藏鼠标光标", Location = new System.Drawing.Point(20, 155), Size = new System.Drawing.Size(150, 22), Font = new System.Drawing.Font("微软雅黑", 9), Checked = false }; _btnStart = new Button { Text = "开始录制", Location = new System.Drawing.Point(20, 185), Size = new System.Drawing.Size(140, 38), BackColor = System.Drawing.Color.ForestGreen, ForeColor = System.Drawing.Color.White, FlatStyle = FlatStyle.Flat }; _btnStart.Click += BtnStart_Click; _btnStop = new Button { Text = "停止录制", Location = new System.Drawing.Point(185, 185), Size = new System.Drawing.Size(140, 38), BackColor = System.Drawing.Color.Crimson, ForeColor = System.Drawing.Color.White, FlatStyle = FlatStyle.Flat, Enabled = false }; _btnStop.Click += BtnStop_Click; _progressBar = new ProgressBar { Location = new System.Drawing.Point(20, 235), Size = new System.Drawing.Size(305, 16), Style = ProgressBarStyle.Continuous, Minimum = 0, Maximum = 100, Value = 0, Visible = false }; _lblProgress = new Label { Location = new System.Drawing.Point(20, 254), Size = new System.Drawing.Size(305, 18), Font = new System.Drawing.Font("Consolas", 8), ForeColor = System.Drawing.Color.DimGray, Text = "", Visible = false }; _timer = new System.Windows.Forms.Timer { Interval = 1000 }; _timer.Tick += (_, _) => _lblTimer.Text = (DateTime.Now - _startTime).ToString(@"hh\:mm\:ss"); Controls.AddRange(new Control[] { _lblStatus, _lblTimer, _lblFps, _trackFps, _lblFpsVal, _lblQuality, _cmbQuality, _chkHideCursor, _btnStart, _btnStop, _progressBar, _lblProgress }); } private void BtnStart_Click(object? sender, EventArgs e) { var (_, qualityValue, _) = QualityOptions[_cmbQuality.SelectedIndex]; var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); var videoPath = Path.Combine(_outputDir, $"recording_{timestamp}.avi"); _lastVideoPath = videoPath; // H.264 模式先录无压缩 AVI,之后 ffmpeg 转码 // MJPEG 模式直接录 MJPEG AVI int captureQuality = qualityValue <= 18 ? 101 : qualityValue; // 101 = 无压缩 _capture = new CaptureService(fps: _trackFps.Value, quality: captureQuality, hideCursor: _chkHideCursor.Checked); _capture.Start(videoPath); _startTime = DateTime.Now; _timer.Start(); _trackFps.Enabled = false; _cmbQuality.Enabled = false; _chkHideCursor.Enabled = false; _lblStatus.Text = $"录制中 → {Path.GetFileName(videoPath)}"; _btnStart.Enabled = false; _btnStop.Enabled = true; } private void BtnStop_Click(object? sender, EventArgs e) { _timer.Stop(); _capture?.Stop(); _capture = null; _lblTimer.Text = "00:00:00"; _btnStop.Enabled = false; var aviPath = _lastVideoPath; var (_, qualityValue, ffmpegArgs) = QualityOptions[_cmbQuality.SelectedIndex]; bool needFfmpeg = qualityValue <= 18; if (aviPath != null && File.Exists(aviPath) && needFfmpeg) { var ffmpeg = FindFfmpeg(); if (ffmpeg != null) { // 先获取视频总时长(秒),用于计算进度百分比 double totalSeconds = GetAviDuration(ffmpeg, aviPath); _lblStatus.Text = "转码中,请稍候..."; _progressBar.Value = 0; _progressBar.Visible = true; _lblProgress.Text = ""; _lblProgress.Visible = true; var mp4Path = Path.ChangeExtension(aviPath, ".mp4"); Task.Run(() => { try { var psi = new System.Diagnostics.ProcessStartInfo { FileName = ffmpeg, Arguments = $"-y -i \"{aviPath}\" {ffmpegArgs} -progress pipe:1 \"{mp4Path}\"", CreateNoWindow = true, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true }; var proc = System.Diagnostics.Process.Start(psi)!; proc.ErrorDataReceived += (_, _) => { }; // 丢弃 stderr 避免阻塞 proc.BeginErrorReadLine(); // 逐行读取 stdout 进度 string? line; while ((line = proc.StandardOutput.ReadLine()) != null) { // 格式: out_time_us=1234567 if (line.StartsWith("out_time_us=") && long.TryParse(line.AsSpan("out_time_us=".Length), out long us) && totalSeconds > 0) { double done = us / 1_000_000.0; int pct = (int)Math.Min(100, done / totalSeconds * 100); string timeStr = TimeSpan.FromSeconds(done).ToString(@"hh\:mm\:ss"); string totalStr = TimeSpan.FromSeconds(totalSeconds).ToString(@"hh\:mm\:ss"); Invoke(() => { _progressBar.Value = pct; _lblProgress.Text = $"{timeStr} / {totalStr} ({pct}%)"; }); } } proc.WaitForExit(); bool ok = proc.ExitCode == 0 && File.Exists(mp4Path); if (ok) File.Delete(aviPath); Invoke(() => { _progressBar.Visible = false; _lblProgress.Visible = false; _trackFps.Enabled = true; _cmbQuality.Enabled = true; _chkHideCursor.Enabled = true; _btnStart.Enabled = true; if (ok) { _lblStatus.Text = $"已保存至 {_outputDir}"; MessageBox.Show($"录制完成:\n{mp4Path}", "完成", MessageBoxButtons.OK, MessageBoxIcon.Information); } else { _lblStatus.Text = "转码失败,AVI 已保留"; MessageBox.Show($"转码失败,AVI 保留在:\n{aviPath}", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); } }); } catch (Exception ex) { Invoke(() => { _progressBar.Visible = false; _lblProgress.Visible = false; _trackFps.Enabled = true; _cmbQuality.Enabled = true; _chkHideCursor.Enabled = true; _btnStart.Enabled = true; _lblStatus.Text = "转码出错"; MessageBox.Show($"转码出错:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); }); } }); return; } else { MessageBox.Show( $"未找到 ffmpeg,无法转码。\nAVI 保留在:\n{aviPath}", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } else if (aviPath != null) { _lblStatus.Text = $"已保存至 {_outputDir}"; MessageBox.Show($"录制完成:\n{aviPath}", "完成", MessageBoxButtons.OK, MessageBoxIcon.Information); } _trackFps.Enabled = true; _cmbQuality.Enabled = true; _chkHideCursor.Enabled = true; _btnStart.Enabled = true; } // 用 ffprobe 思路:让 ffmpeg 读文件头获取时长 private static double GetAviDuration(string ffmpeg, string path) { try { var psi = new System.Diagnostics.ProcessStartInfo { FileName = ffmpeg, Arguments = $"-i \"{path}\"", CreateNoWindow = true, UseShellExecute = false, RedirectStandardError = true }; var proc = System.Diagnostics.Process.Start(psi)!; string err = proc.StandardError.ReadToEnd(); proc.WaitForExit(); // 解析 "Duration: HH:MM:SS.xx" var m = System.Text.RegularExpressions.Regex.Match(err, @"Duration:\s*(\d+):(\d+):(\d+\.\d+)"); if (m.Success) { double h = double.Parse(m.Groups[1].Value); double min = double.Parse(m.Groups[2].Value); double sec = double.Parse(m.Groups[3].Value); return h * 3600 + min * 60 + sec; } } catch { } return 0; } protected override void OnFormClosing(FormClosingEventArgs e) { _capture?.Dispose(); base.OnFormClosing(e); } }