Program.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. using System.Windows.Forms;
  2. using ScreenRecorder;
  3. var app = new RecorderForm();
  4. Application.Run(app);
  5. public class RecorderForm : Form
  6. {
  7. private readonly Button _btnStart;
  8. private readonly Button _btnStop;
  9. private readonly Label _lblStatus;
  10. private readonly Label _lblTimer;
  11. private readonly Label _lblFps;
  12. private readonly Label _lblQuality;
  13. private readonly TrackBar _trackFps;
  14. private readonly ComboBox _cmbQuality;
  15. private readonly Label _lblFpsVal;
  16. private readonly CheckBox _chkHideCursor;
  17. private readonly System.Windows.Forms.Timer _timer;
  18. private readonly ProgressBar _progressBar;
  19. private readonly Label _lblProgress;
  20. private CaptureService? _capture;
  21. private DateTime _startTime;
  22. private string _outputDir = Environment.GetFolderPath(Environment.SpecialFolder.MyVideos);
  23. private string? _lastVideoPath;
  24. private static string? FindFfmpeg()
  25. {
  26. var local = Path.Combine(AppContext.BaseDirectory, "ffmpeg.exe");
  27. if (File.Exists(local)) return local;
  28. foreach (var dir in (Environment.GetEnvironmentVariable("PATH") ?? "").Split(';'))
  29. {
  30. var p = Path.Combine(dir.Trim(), "ffmpeg.exe");
  31. if (File.Exists(p)) return p;
  32. }
  33. return null;
  34. }
  35. // Value: 0=无损H265, 1-100=MJPEG quality, 10/18=H264有损
  36. private static readonly (string Label, int Value, string FfmpegArgs)[] QualityOptions =
  37. [
  38. ("无损 (H.265 CRF 0, yuv444p)", 0, "-c:v libx265 -preset fast -crf 0 -pix_fmt yuv444p -tag:v hvc1"),
  39. ("极高画质 (H.264 CRF 10)", 10, "-c:v libx264 -preset fast -crf 10 -pix_fmt yuv420p"),
  40. ("高画质 (H.264 CRF 18)", 18, "-c:v libx264 -preset fast -crf 18 -pix_fmt yuv420p"),
  41. ("中画质 MJPEG 100", 100, ""),
  42. ("低画质 MJPEG 80", 80, ""),
  43. ("极低画质 MJPEG 50", 50, ""),
  44. ];
  45. public RecorderForm()
  46. {
  47. Text = "屏幕录制";
  48. Size = new System.Drawing.Size(360, 340);
  49. FormBorderStyle = FormBorderStyle.FixedSingle;
  50. MaximizeBox = false;
  51. StartPosition = FormStartPosition.CenterScreen;
  52. _lblStatus = new Label
  53. {
  54. Text = "就绪",
  55. Location = new System.Drawing.Point(20, 12),
  56. Size = new System.Drawing.Size(320, 22),
  57. Font = new System.Drawing.Font("微软雅黑", 10)
  58. };
  59. _lblTimer = new Label
  60. {
  61. Text = "00:00:00",
  62. Location = new System.Drawing.Point(20, 36),
  63. Size = new System.Drawing.Size(320, 26),
  64. Font = new System.Drawing.Font("Consolas", 15),
  65. ForeColor = System.Drawing.Color.DarkBlue
  66. };
  67. _lblFps = new Label
  68. {
  69. Text = "帧率 (fps):",
  70. Location = new System.Drawing.Point(20, 74),
  71. Size = new System.Drawing.Size(80, 20),
  72. Font = new System.Drawing.Font("微软雅黑", 9)
  73. };
  74. _trackFps = new TrackBar
  75. {
  76. Minimum = 1, Maximum = 60, Value = 30,
  77. TickFrequency = 5, SmallChange = 1, LargeChange = 5,
  78. Location = new System.Drawing.Point(100, 67),
  79. Size = new System.Drawing.Size(190, 40)
  80. };
  81. _lblFpsVal = new Label
  82. {
  83. Text = "30",
  84. Location = new System.Drawing.Point(295, 74),
  85. Size = new System.Drawing.Size(40, 20),
  86. Font = new System.Drawing.Font("Consolas", 10),
  87. ForeColor = System.Drawing.Color.DarkBlue
  88. };
  89. _trackFps.ValueChanged += (_, _) => _lblFpsVal.Text = _trackFps.Value.ToString();
  90. _lblQuality = new Label
  91. {
  92. Text = "画质:",
  93. Location = new System.Drawing.Point(20, 120),
  94. Size = new System.Drawing.Size(80, 20),
  95. Font = new System.Drawing.Font("微软雅黑", 9)
  96. };
  97. _cmbQuality = new ComboBox
  98. {
  99. Location = new System.Drawing.Point(100, 117),
  100. Size = new System.Drawing.Size(230, 26),
  101. DropDownStyle = ComboBoxStyle.DropDownList,
  102. Font = new System.Drawing.Font("微软雅黑", 9)
  103. };
  104. foreach (var (label, _, _) in QualityOptions)
  105. _cmbQuality.Items.Add(label);
  106. _cmbQuality.SelectedIndex = 0;
  107. _chkHideCursor = new CheckBox
  108. {
  109. Text = "隐藏鼠标光标",
  110. Location = new System.Drawing.Point(20, 155),
  111. Size = new System.Drawing.Size(150, 22),
  112. Font = new System.Drawing.Font("微软雅黑", 9),
  113. Checked = false
  114. };
  115. _btnStart = new Button
  116. {
  117. Text = "开始录制",
  118. Location = new System.Drawing.Point(20, 185),
  119. Size = new System.Drawing.Size(140, 38),
  120. BackColor = System.Drawing.Color.ForestGreen,
  121. ForeColor = System.Drawing.Color.White,
  122. FlatStyle = FlatStyle.Flat
  123. };
  124. _btnStart.Click += BtnStart_Click;
  125. _btnStop = new Button
  126. {
  127. Text = "停止录制",
  128. Location = new System.Drawing.Point(185, 185),
  129. Size = new System.Drawing.Size(140, 38),
  130. BackColor = System.Drawing.Color.Crimson,
  131. ForeColor = System.Drawing.Color.White,
  132. FlatStyle = FlatStyle.Flat,
  133. Enabled = false
  134. };
  135. _btnStop.Click += BtnStop_Click;
  136. _progressBar = new ProgressBar
  137. {
  138. Location = new System.Drawing.Point(20, 235),
  139. Size = new System.Drawing.Size(305, 16),
  140. Style = ProgressBarStyle.Continuous,
  141. Minimum = 0, Maximum = 100, Value = 0,
  142. Visible = false
  143. };
  144. _lblProgress = new Label
  145. {
  146. Location = new System.Drawing.Point(20, 254),
  147. Size = new System.Drawing.Size(305, 18),
  148. Font = new System.Drawing.Font("Consolas", 8),
  149. ForeColor = System.Drawing.Color.DimGray,
  150. Text = "",
  151. Visible = false
  152. };
  153. _timer = new System.Windows.Forms.Timer { Interval = 1000 };
  154. _timer.Tick += (_, _) => _lblTimer.Text = (DateTime.Now - _startTime).ToString(@"hh\:mm\:ss");
  155. Controls.AddRange(new Control[]
  156. {
  157. _lblStatus, _lblTimer,
  158. _lblFps, _trackFps, _lblFpsVal,
  159. _lblQuality, _cmbQuality,
  160. _chkHideCursor,
  161. _btnStart, _btnStop,
  162. _progressBar,
  163. _lblProgress
  164. });
  165. }
  166. private void BtnStart_Click(object? sender, EventArgs e)
  167. {
  168. var (_, qualityValue, _) = QualityOptions[_cmbQuality.SelectedIndex];
  169. var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
  170. var videoPath = Path.Combine(_outputDir, $"recording_{timestamp}.avi");
  171. _lastVideoPath = videoPath;
  172. // H.264 模式先录无压缩 AVI,之后 ffmpeg 转码
  173. // MJPEG 模式直接录 MJPEG AVI
  174. int captureQuality = qualityValue <= 18 ? 101 : qualityValue; // 101 = 无压缩
  175. _capture = new CaptureService(fps: _trackFps.Value, quality: captureQuality, hideCursor: _chkHideCursor.Checked);
  176. _capture.Start(videoPath);
  177. _startTime = DateTime.Now;
  178. _timer.Start();
  179. _trackFps.Enabled = false;
  180. _cmbQuality.Enabled = false;
  181. _chkHideCursor.Enabled = false;
  182. _lblStatus.Text = $"录制中 → {Path.GetFileName(videoPath)}";
  183. _btnStart.Enabled = false;
  184. _btnStop.Enabled = true;
  185. }
  186. private void BtnStop_Click(object? sender, EventArgs e)
  187. {
  188. _timer.Stop();
  189. _capture?.Stop();
  190. _capture = null;
  191. _lblTimer.Text = "00:00:00";
  192. _btnStop.Enabled = false;
  193. var aviPath = _lastVideoPath;
  194. var (_, qualityValue, ffmpegArgs) = QualityOptions[_cmbQuality.SelectedIndex];
  195. bool needFfmpeg = qualityValue <= 18;
  196. if (aviPath != null && File.Exists(aviPath) && needFfmpeg)
  197. {
  198. var ffmpeg = FindFfmpeg();
  199. if (ffmpeg != null)
  200. {
  201. // 先获取视频总时长(秒),用于计算进度百分比
  202. double totalSeconds = GetAviDuration(ffmpeg, aviPath);
  203. _lblStatus.Text = "转码中,请稍候...";
  204. _progressBar.Value = 0;
  205. _progressBar.Visible = true;
  206. _lblProgress.Text = "";
  207. _lblProgress.Visible = true;
  208. var mp4Path = Path.ChangeExtension(aviPath, ".mp4");
  209. Task.Run(() =>
  210. {
  211. try
  212. {
  213. var psi = new System.Diagnostics.ProcessStartInfo
  214. {
  215. FileName = ffmpeg,
  216. Arguments = $"-y -i \"{aviPath}\" {ffmpegArgs} -progress pipe:1 \"{mp4Path}\"",
  217. CreateNoWindow = true,
  218. UseShellExecute = false,
  219. RedirectStandardOutput = true,
  220. RedirectStandardError = true
  221. };
  222. var proc = System.Diagnostics.Process.Start(psi)!;
  223. proc.ErrorDataReceived += (_, _) => { }; // 丢弃 stderr 避免阻塞
  224. proc.BeginErrorReadLine();
  225. // 逐行读取 stdout 进度
  226. string? line;
  227. while ((line = proc.StandardOutput.ReadLine()) != null)
  228. {
  229. // 格式: out_time_us=1234567
  230. if (line.StartsWith("out_time_us=") &&
  231. long.TryParse(line.AsSpan("out_time_us=".Length), out long us) &&
  232. totalSeconds > 0)
  233. {
  234. double done = us / 1_000_000.0;
  235. int pct = (int)Math.Min(100, done / totalSeconds * 100);
  236. string timeStr = TimeSpan.FromSeconds(done).ToString(@"hh\:mm\:ss");
  237. string totalStr = TimeSpan.FromSeconds(totalSeconds).ToString(@"hh\:mm\:ss");
  238. Invoke(() =>
  239. {
  240. _progressBar.Value = pct;
  241. _lblProgress.Text = $"{timeStr} / {totalStr} ({pct}%)";
  242. });
  243. }
  244. }
  245. proc.WaitForExit();
  246. bool ok = proc.ExitCode == 0 && File.Exists(mp4Path);
  247. if (ok) File.Delete(aviPath);
  248. Invoke(() =>
  249. {
  250. _progressBar.Visible = false;
  251. _lblProgress.Visible = false;
  252. _trackFps.Enabled = true;
  253. _cmbQuality.Enabled = true;
  254. _chkHideCursor.Enabled = true;
  255. _btnStart.Enabled = true;
  256. if (ok)
  257. {
  258. _lblStatus.Text = $"已保存至 {_outputDir}";
  259. MessageBox.Show($"录制完成:\n{mp4Path}", "完成",
  260. MessageBoxButtons.OK, MessageBoxIcon.Information);
  261. }
  262. else
  263. {
  264. _lblStatus.Text = "转码失败,AVI 已保留";
  265. MessageBox.Show($"转码失败,AVI 保留在:\n{aviPath}", "提示",
  266. MessageBoxButtons.OK, MessageBoxIcon.Warning);
  267. }
  268. });
  269. }
  270. catch (Exception ex)
  271. {
  272. Invoke(() =>
  273. {
  274. _progressBar.Visible = false;
  275. _lblProgress.Visible = false;
  276. _trackFps.Enabled = true;
  277. _cmbQuality.Enabled = true;
  278. _chkHideCursor.Enabled = true;
  279. _btnStart.Enabled = true;
  280. _lblStatus.Text = "转码出错";
  281. MessageBox.Show($"转码出错:{ex.Message}", "错误",
  282. MessageBoxButtons.OK, MessageBoxIcon.Error);
  283. });
  284. }
  285. });
  286. return;
  287. }
  288. else
  289. {
  290. MessageBox.Show(
  291. $"未找到 ffmpeg,无法转码。\nAVI 保留在:\n{aviPath}",
  292. "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
  293. }
  294. }
  295. else if (aviPath != null)
  296. {
  297. _lblStatus.Text = $"已保存至 {_outputDir}";
  298. MessageBox.Show($"录制完成:\n{aviPath}", "完成",
  299. MessageBoxButtons.OK, MessageBoxIcon.Information);
  300. }
  301. _trackFps.Enabled = true;
  302. _cmbQuality.Enabled = true;
  303. _chkHideCursor.Enabled = true;
  304. _btnStart.Enabled = true;
  305. }
  306. // 用 ffprobe 思路:让 ffmpeg 读文件头获取时长
  307. private static double GetAviDuration(string ffmpeg, string path)
  308. {
  309. try
  310. {
  311. var psi = new System.Diagnostics.ProcessStartInfo
  312. {
  313. FileName = ffmpeg,
  314. Arguments = $"-i \"{path}\"",
  315. CreateNoWindow = true,
  316. UseShellExecute = false,
  317. RedirectStandardError = true
  318. };
  319. var proc = System.Diagnostics.Process.Start(psi)!;
  320. string err = proc.StandardError.ReadToEnd();
  321. proc.WaitForExit();
  322. // 解析 "Duration: HH:MM:SS.xx"
  323. var m = System.Text.RegularExpressions.Regex.Match(err,
  324. @"Duration:\s*(\d+):(\d+):(\d+\.\d+)");
  325. if (m.Success)
  326. {
  327. double h = double.Parse(m.Groups[1].Value);
  328. double min = double.Parse(m.Groups[2].Value);
  329. double sec = double.Parse(m.Groups[3].Value);
  330. return h * 3600 + min * 60 + sec;
  331. }
  332. }
  333. catch { }
  334. return 0;
  335. }
  336. protected override void OnFormClosing(FormClosingEventArgs e)
  337. {
  338. _capture?.Dispose();
  339. base.OnFormClosing(e);
  340. }
  341. }