mirror of
https://github.com/babalae/better-genshin-impact.git
synced 2026-05-21 09:45:48 +08:00
add sharp avi
This commit is contained in:
@@ -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" />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using BetterGenshinImpact.Core.Recorder;
|
||||
using SharpDX.DirectInput;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
258
BetterGenshinImpact/Core/Video/SharpAviRecorder.cs
Normal file
258
BetterGenshinImpact/Core/Video/SharpAviRecorder.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user