Dispenser/DispenserUI/Utils/ScreenRecorder.cs

183 lines
5.6 KiB
C#

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);
}
}