|
|
@@ -0,0 +1,373 @@
|
|
|
+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);
|
|
|
+ }
|
|
|
+}
|