using System; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using SharpAvi; using SharpAvi.Codecs; using SharpAvi.Output; namespace DispenserUI.ViewModels.Recorder; public class ScreenRecorder { private static readonly FourCC MJPEG_IMAGE_SHARP = "IMG#"; private readonly AviWriter _writer; private IAviVideoStream _videoStream; private Thread _screenThread; private readonly ManualResetEvent _stopThread = new(false); private readonly AutoResetEvent _videoFrameWritten = new(false); private readonly FourCC _codec; private readonly int _quality; private readonly string _fileName; public int Width { get; set; } public int Height { get; set; } public int Left { get; set; } public int Top { get; set; } public bool IsRecording { get; set; } public ScreenRecorder(string fileName, FourCC codec, int fps, int quality) { _codec = codec; _quality = quality; _fileName = fileName; // Create AVI writer and specify FPS _writer = new AviWriter(fileName) { FramesPerSecond = fps, EmitIndex1 = true, }; } public void Start() { // Create video stream _videoStream = CreateVideoStream(_codec, _quality); // Set only name. Other properties were when creating stream, // either explicitly by arguments or implicitly by the encoder used _videoStream.Name = "Screencast"; _screenThread = new Thread(RecordScreen) { Name = nameof(ScreenRecorder) + ".RecordScreen", IsBackground = true }; IsRecording = true; _screenThread.Start(); } private IAviVideoStream CreateVideoStream(FourCC codec, int quality) { // Select encoder type based on FOURCC of codec if (codec == CodecIds.Uncompressed) { return _writer.AddUncompressedVideoStream(Width, Height); } if (codec == MJPEG_IMAGE_SHARP) { // Use M-JPEG based on the SixLabors.ImageSharp package (cross-platform) // Included in the SharpAvi.ImageSharp package return AddMJpegImageSharpVideoStream(_writer, Width, Height, quality); } return _writer.AddMpeg4VcmVideoStream(Width, Height, (double)_writer.FramesPerSecond, // It seems that all tested MPEG-4 VfW codecs ignore the quality affecting parameters passed through VfW API // They only respect the settings from their own configuration dialogs, and Mpeg4VideoEncoder currently has no support for this quality: quality, codec: codec, // Most of VfW codecs expect single-threaded use, so we wrap this encoder to special wrapper // Thus all calls to the encoder (including its instantiation) will be invoked on a single thread although encoding (and writing) is performed asynchronously forceSingleThreadedAccess: true); } private static IAviVideoStream AddMJpegImageSharpVideoStream(AviWriter writer, int width, int height, int quality = 70) { var encoder = new MJpegImageVideoEncoder(width, height, quality); return writer.AddEncodingVideoStream(encoder, width: width, height: height); } public void Stop() { IsRecording = false; _stopThread.Set(); _screenThread.Join(); // Close writer: the remaining data is written to a file and file is closed _writer.Close(); _stopThread.Close(); } private void RecordScreen() { var stopwatch = new Stopwatch(); var buffer = new byte[Width * Height * 4]; Task videoWriteTask = null; var isFirstFrame = true; var shotsTaken = 0; var timeTillNextFrame = TimeSpan.Zero; stopwatch.Start(); while (!_stopThread.WaitOne(timeTillNextFrame)) { try { GetScreenshot(buffer); } catch (Exception _) { continue; } shotsTaken++; // Wait for the previous frame is written if (!isFirstFrame) { videoWriteTask.Wait(); _videoFrameWritten.Set(); } videoWriteTask = _videoStream.WriteFrameAsync(true, buffer.AsMemory(0, buffer.Length)); timeTillNextFrame = TimeSpan.FromSeconds(shotsTaken / (double)_writer.FramesPerSecond - stopwatch.Elapsed.TotalSeconds); if (timeTillNextFrame < TimeSpan.Zero) timeTillNextFrame = TimeSpan.Zero; isFirstFrame = false; } stopwatch.Stop(); // Wait for the last frame is written if (!isFirstFrame) { videoWriteTask.Wait(); } } private void GetScreenshot(byte[] buffer) { using (var bitmap = new Bitmap(Width, Height)) { using (var graphics = Graphics.FromImage(bitmap)) { graphics.CopyFromScreen(Left, Top, 0, 0, new Size(Width, Height)); var bits = bitmap.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppRgb); Marshal.Copy(bits.Scan0, buffer, 0, buffer.Length); bitmap.UnlockBits(bits); } } } public string? GetDirectory() { return Path.GetDirectoryName(_fileName); } }