add sharp avi

This commit is contained in:
辉鸭蛋
2024-12-15 17:48:30 +08:00
parent ec8fa6e824
commit fa3e7fccb0
5 changed files with 289 additions and 6 deletions

View File

@@ -48,6 +48,7 @@
<PackageReference Include="Microsoft.ML.OnnxRuntime.DirectML" Version="1.18.1" />
<PackageReference Include="Microsoft.ML.OnnxRuntime.Managed" Version="1.18.1" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2592.51" />
<PackageReference Include="NAudio" Version="2.2.1" />
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
<PackageReference Include="OpenCvSharp4.WpfExtensions" Version="4.8.0.20230708" />
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.8.0.20230708" />
@@ -65,6 +66,7 @@
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.RichTextBoxEx.Wpf" Version="1.1.0.1" />
<PackageReference Include="SharpAvi" Version="3.0.1" />
<PackageReference Include="System.IO.Hashing" Version="8.0.0" />
<PackageReference Include="Vanara.PInvoke.NtDll" Version="4.0.2" />
<PackageReference Include="Vanara.PInvoke.SHCore" Version="4.0.2" />

View File

@@ -1,6 +1,7 @@
using BetterGenshinImpact.Core.Recorder;
using SharpDX.DirectInput;
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

View File

@@ -9,8 +9,13 @@ using SharpDX.DirectInput;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Video;
using NAudio.Wave;
using SharpAvi;
using Wpf.Ui.Violeta.Controls;
namespace BetterGenshinImpact.Core.Recorder;
@@ -20,6 +25,8 @@ public class GlobalKeyMouseRecord : Singleton<GlobalKeyMouseRecord>
private readonly ILogger<GlobalKeyMouseRecord> _logger = App.GetLogger<GlobalKeyMouseRecord>();
private KeyMouseRecorder? _recorder;
private SharpAviRecorder _sharpAviRecorder;
private readonly Dictionary<Keys, bool> _keyDownState = [];
@@ -58,18 +65,29 @@ public class GlobalKeyMouseRecord : Singleton<GlobalKeyMouseRecord>
_logger.LogInformation("录制:{Text}", "实时任务已暂停");
_logger.LogInformation("注意:录制时遇到主界面(鼠标永远在界面中心)和其他界面(鼠标可自由移动,比如地图等)的切换,请把手离开鼠标等待录制模式切换日志");
// 先实例化
_recorder = new KeyMouseRecorder();
_directInputMonitor = new DirectInputMonitor();
var videoPath = Global.Absolute(@"video");
if (!Directory.Exists(videoPath))
{
Directory.CreateDirectory(videoPath);
}
_sharpAviRecorder = new SharpAviRecorder( Path.Combine(videoPath, $"{DateTime.Now:yyyyMMddHH_mmssffff.avi}"),
CodecIds.MotionJpeg, 90, 0, SupportedWaveFormat.WAVE_FORMAT_44M16, false, 0);
TaskTriggerDispatcher.Instance().StopTimer();
for (var i = 3; i >= 1; i--)
{
_logger.LogInformation("{Sec}秒后启动录制...", i);
await Task.Delay(1000);
}
TaskTriggerDispatcher.Instance().StopTimer();
_timer.Start();
_recorder = new KeyMouseRecorder();
_directInputMonitor = new DirectInputMonitor();
// _timer.Start();
_sharpAviRecorder.Start();
_directInputMonitor.Start();
Status = KeyMouseRecorderStatus.Recording;
@@ -89,8 +107,10 @@ public class GlobalKeyMouseRecord : Singleton<GlobalKeyMouseRecord>
_directInputMonitor?.Stop();
_directInputMonitor?.Dispose();
_directInputMonitor = null;
_sharpAviRecorder.Dispose();
_timer.Stop();
// _timer.Stop();
_logger.LogInformation("录制:{Text}", "结束录制");
@@ -187,11 +207,11 @@ public class GlobalKeyMouseRecord : Singleton<GlobalKeyMouseRecord>
public void GlobalHookMouseMoveBy(MouseState state)
{
// Debug.WriteLine($"MouseMoveBy: {state.X}, {state.Y}");
if (state is { X: 0, Y: 0 } || !_isInMainUi)
{
return;
}
// Debug.WriteLine($"MouseMoveBy: {state.X}, {state.Y}");
_recorder?.MouseMoveBy(state);
}
}

View File

@@ -15,6 +15,8 @@ namespace BetterGenshinImpact.Core.Recorder;
public class KeyMouseRecorder
{
public List<MacroEvent> MacroEvents { get; } = [];
public List<MacroEvent> MouseMoveByMacroEvents { get; } = [];
public DateTime StartTime { get; set; } = DateTime.UtcNow;

View File

@@ -0,0 +1,258 @@
using NAudio.Wave;
using SharpAvi.Codecs;
using SharpAvi.Output;
using System;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using SharpAvi;
using FourCC = SharpAvi.FourCC;
using WaveFormat = NAudio.Wave.WaveFormat;
namespace BetterGenshinImpact.Core.Video;
public class SharpAviRecorder : IDisposable
{
// public static readonly FourCC MJPEG_IMAGE_SHARP = "IMG#";
private readonly int screenWidth;
private readonly int screenHeight;
private readonly AviWriter writer;
private readonly IAviVideoStream videoStream;
private readonly IAviAudioStream audioStream;
private readonly WaveInEvent audioSource;
private readonly Thread screenThread;
private readonly ManualResetEvent stopThread = new ManualResetEvent(false);
private readonly AutoResetEvent videoFrameWritten = new AutoResetEvent(false);
private readonly AutoResetEvent audioBlockWritten = new AutoResetEvent(false);
public SharpAviRecorder(string fileName,
FourCC codec, int quality,
int audioSourceIndex, SupportedWaveFormat audioWaveFormat, bool encodeAudio, int audioBitRate)
{
System.Windows.Media.Matrix toDevice;
using (var source = new HwndSource(new HwndSourceParameters()))
{
toDevice = source.CompositionTarget.TransformToDevice;
}
screenWidth = (int)Math.Round(SystemParameters.PrimaryScreenWidth * toDevice.M11);
screenHeight = (int)Math.Round(SystemParameters.PrimaryScreenHeight * toDevice.M22);
// Create AVI writer and specify FPS
writer = new AviWriter(fileName)
{
FramesPerSecond = 60,
EmitIndex1 = true,
};
// 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";
if (audioSourceIndex >= 0)
{
var waveFormat = ToWaveFormat(audioWaveFormat);
audioStream = CreateAudioStream(waveFormat, encodeAudio, audioBitRate);
// Set only name. Other properties were when creating stream,
// either explicitly by arguments or implicitly by the encoder used
audioStream.Name = "Voice";
audioSource = new WaveInEvent
{
DeviceNumber = audioSourceIndex,
WaveFormat = waveFormat,
// Buffer size to store duration of 1 frame
BufferMilliseconds = (int)Math.Ceiling(1000 / writer.FramesPerSecond),
NumberOfBuffers = 3,
};
audioSource.DataAvailable += audioSource_DataAvailable;
}
screenThread = new Thread(RecordScreen)
{
Name = typeof(SharpAviRecorder).Name + ".RecordScreen",
IsBackground = true
};
}
public void Start()
{
if (audioSource != null)
{
videoFrameWritten.Set();
audioBlockWritten.Reset();
audioSource.StartRecording();
}
screenThread.Start();
}
private IAviVideoStream CreateVideoStream(FourCC codec, int quality)
{
// Select encoder type based on FOURCC of codec
if (codec == CodecIds.Uncompressed)
{
return writer.AddUncompressedVideoStream(screenWidth, screenHeight);
}
else if (codec == CodecIds.MotionJpeg)
{
// Use M-JPEG based on WPF (Windows only)
return writer.AddMJpegWpfVideoStream(screenWidth, screenHeight, quality);
}
// else if (codec == MJPEG_IMAGE_SHARP)
// {
// // Use M-JPEG based on the SixLabors.ImageSharp package (cross-platform)
// // Included in the SharpAvi.ImageSharp package
// return writer.AddMJpegImageSharpVideoStream(screenWidth, screenHeight, quality);
// }
else
{
return writer.AddMpeg4VcmVideoStream(screenWidth, screenHeight, (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 IAviAudioStream CreateAudioStream(WaveFormat waveFormat, bool encode, int bitRate)
{
// Create encoding or simple stream based on settings
if (encode)
{
// LAME DLL path is set in App.OnStartup()
return writer.AddMp3LameAudioStream(waveFormat.Channels, waveFormat.SampleRate, bitRate);
}
else
{
return writer.AddAudioStream(
channelCount: waveFormat.Channels,
samplesPerSecond: waveFormat.SampleRate,
bitsPerSample: waveFormat.BitsPerSample);
}
}
private static WaveFormat ToWaveFormat(SupportedWaveFormat waveFormat)
{
switch (waveFormat)
{
case SupportedWaveFormat.WAVE_FORMAT_44M16:
return new WaveFormat(44100, 16, 1);
case SupportedWaveFormat.WAVE_FORMAT_44S16:
return new WaveFormat(44100, 16, 2);
default:
throw new NotSupportedException("Wave formats other than '16-bit 44.1kHz' are not currently supported.");
}
}
public void Dispose()
{
stopThread.Set();
screenThread.Join();
if (audioSource != null)
{
audioSource.StopRecording();
audioSource.DataAvailable -= audioSource_DataAvailable;
}
// 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[screenWidth * screenHeight * 4];
Task videoWriteTask = null;
var isFirstFrame = true;
var shotsTaken = 0;
var timeTillNextFrame = TimeSpan.Zero;
stopwatch.Start();
while (!stopThread.WaitOne(timeTillNextFrame))
{
GetScreenshot(buffer);
shotsTaken++;
// Wait for the previous frame is written
if (!isFirstFrame)
{
videoWriteTask.Wait();
videoFrameWritten.Set();
}
if (audioStream != null)
{
var signalled = WaitHandle.WaitAny(new WaitHandle[] { audioBlockWritten, stopThread });
if (signalled == 1)
break;
}
// Start asynchronous (encoding and) writing of the new frame
// Overloads with Memory parameters are available on .NET 5+
#if NET5_0_OR_GREATER
videoWriteTask = videoStream.WriteFrameAsync(true, buffer.AsMemory(0, buffer.Length));
#else
videoWriteTask = videoStream.WriteFrameAsync(true, buffer, 0, buffer.Length);
#endif
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(screenWidth, screenHeight))
using (var graphics = Graphics.FromImage(bitmap))
{
graphics.CopyFromScreen(0, 0, 0, 0, new System.Drawing.Size(screenWidth, screenHeight));
var bits = bitmap.LockBits(new Rectangle(0, 0, screenWidth, screenHeight), ImageLockMode.ReadOnly, PixelFormat.Format32bppRgb);
Marshal.Copy(bits.Scan0, buffer, 0, buffer.Length);
bitmap.UnlockBits(bits);
// Should also capture the mouse cursor here, but skipping for simplicity
// For those who are interested, look at http://www.codeproject.com/Articles/12850/Capturing-the-Desktop-Screen-with-the-Mouse-Cursor
}
}
private void audioSource_DataAvailable(object sender, WaveInEventArgs e)
{
var signalled = WaitHandle.WaitAny(new WaitHandle[] { videoFrameWritten, stopThread });
if (signalled == 0 && e.BytesRecorded > 0)
{
// Overloads with Span parameters are available on .NET 5+
#if NET5_0_OR_GREATER
audioStream.WriteBlock(e.Buffer.AsSpan(0, e.BytesRecorded));
#else
audioStream.WriteBlock(e.Buffer, 0, e.BytesRecorded);
#endif
audioBlockWritten.Set();
}
}
}