183 lines
5.6 KiB
C#
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);
|
|||
|
}
|
|||
|
}
|