CaptureService.cs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. using System.Drawing;
  2. using System.Drawing.Imaging;
  3. using System.Runtime.InteropServices;
  4. using SharpAvi;
  5. using SharpAvi.Output;
  6. using SharpAvi.Codecs;
  7. namespace ScreenRecorder;
  8. public class CaptureService : IDisposable
  9. {
  10. private AviWriter? _writer;
  11. private IAviVideoStream? _videoStream;
  12. private Thread? _captureThread;
  13. private volatile bool _isRecording;
  14. private readonly int _fps;
  15. private readonly int _quality;
  16. private readonly bool _hideCursor;
  17. private readonly Rectangle _captureArea;
  18. private string? _outputPath;
  19. public CaptureService(int fps = 30, int quality = 100, bool hideCursor = false)
  20. {
  21. _fps = fps;
  22. _quality = quality;
  23. _hideCursor = hideCursor;
  24. // 录制主屏幕
  25. var screen = System.Windows.Forms.Screen.PrimaryScreen!;
  26. _captureArea = screen.Bounds;
  27. }
  28. public void Start(string outputPath)
  29. {
  30. _outputPath = outputPath;
  31. _isRecording = true;
  32. _writer = new AviWriter(outputPath)
  33. {
  34. FramesPerSecond = _fps,
  35. EmitIndex1 = true
  36. };
  37. if (_quality == 101) // 无损模式:无压缩 RGB
  38. {
  39. var rawStream = _writer.AddVideoStream(
  40. _captureArea.Width,
  41. _captureArea.Height,
  42. BitsPerPixel.Bpp32
  43. );
  44. rawStream.Codec = KnownFourCCs.Codecs.Uncompressed;
  45. _videoStream = rawStream;
  46. }
  47. else // MJPEG 有损模式
  48. {
  49. _videoStream = _writer.AddMotionJpegVideoStream(
  50. _captureArea.Width,
  51. _captureArea.Height,
  52. quality: _quality
  53. );
  54. }
  55. _captureThread = new Thread(CaptureLoop) { IsBackground = true };
  56. _captureThread.Start();
  57. }
  58. public void Stop()
  59. {
  60. _isRecording = false;
  61. _captureThread?.Join(3000);
  62. _writer?.Close();
  63. _writer = null;
  64. }
  65. private void CaptureLoop()
  66. {
  67. var interval = TimeSpan.FromMilliseconds(1000.0 / _fps);
  68. int w = _captureArea.Width, h = _captureArea.Height;
  69. bool lossless = _quality == 101;
  70. using var bitmap = new Bitmap(w, h, PixelFormat.Format32bppRgb);
  71. using var graphics = Graphics.FromImage(bitmap);
  72. int stride = w * 4;
  73. var frameData = new byte[stride * h];
  74. while (_isRecording)
  75. {
  76. var start = DateTime.UtcNow;
  77. graphics.CopyFromScreen(_captureArea.Location, Point.Empty, _captureArea.Size);
  78. if (!_hideCursor) DrawCursor(graphics);
  79. var bits = bitmap.LockBits(
  80. new Rectangle(0, 0, w, h),
  81. ImageLockMode.ReadOnly,
  82. PixelFormat.Format32bppRgb
  83. );
  84. if (lossless)
  85. {
  86. // 无压缩 AVI 需要 BGR 从下到上(行翻转)
  87. for (int row = 0; row < h; row++)
  88. {
  89. IntPtr src = bits.Scan0 + row * bits.Stride;
  90. int dstOffset = (h - 1 - row) * stride;
  91. Marshal.Copy(src, frameData, dstOffset, stride);
  92. }
  93. }
  94. else
  95. {
  96. Marshal.Copy(bits.Scan0, frameData, 0, frameData.Length);
  97. }
  98. bitmap.UnlockBits(bits);
  99. _videoStream!.WriteFrame(true, frameData, 0, frameData.Length);
  100. var elapsed = DateTime.UtcNow - start;
  101. var remaining = interval - elapsed;
  102. if (remaining > TimeSpan.Zero)
  103. Thread.Sleep(remaining);
  104. }
  105. }
  106. public void Dispose() => Stop();
  107. // Win32 API:获取鼠标位置和光标图标
  108. [StructLayout(LayoutKind.Sequential)]
  109. private struct CURSORINFO
  110. {
  111. public int cbSize;
  112. public int flags;
  113. public IntPtr hCursor;
  114. public POINT ptScreenPos;
  115. }
  116. [StructLayout(LayoutKind.Sequential)]
  117. private struct POINT { public int x, y; }
  118. [DllImport("user32.dll")] static extern bool GetCursorInfo(ref CURSORINFO ci);
  119. [DllImport("user32.dll")] static extern bool DrawIconEx(IntPtr hdc, int x, int y, IntPtr hIcon, int w, int h, int step, IntPtr brush, int flags);
  120. private const int CURSOR_SHOWING = 1;
  121. private const int DI_NORMAL = 3;
  122. private void DrawCursor(Graphics g)
  123. {
  124. var ci = new CURSORINFO { cbSize = Marshal.SizeOf<CURSORINFO>() };
  125. if (!GetCursorInfo(ref ci) || ci.flags != CURSOR_SHOWING) return;
  126. int x = ci.ptScreenPos.x - _captureArea.X;
  127. int y = ci.ptScreenPos.y - _captureArea.Y;
  128. var hdc = g.GetHdc();
  129. DrawIconEx(hdc, x, y, ci.hCursor, 0, 0, 0, IntPtr.Zero, DI_NORMAL);
  130. g.ReleaseHdc(hdc);
  131. }
  132. }