Open Luigi38 opened 1 year ago
HighlightManager.cs
파일
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using log4net;
using MessagePack;
using OpenCvSharp;
using NAudio;
using NAudio.Lame;
using NAudio.Mixer;
using NAudio.Wave;
using NAudio.Wave.SampleProviders;
using IGReinforced.Extensions;
using IGReinforced.Recording.Audio.Wasapi;
using IGReinforced.Recording.Types;
using IGReinforced.Recording.Video;
using IGReinforced.Recording.Video.NvColorSpace;
namespace IGReinforced.Recording.Highlights
{
public class HighlightManager
{
public static string LocalPath { get; set; } = string.Empty;
public static string FFmpegExecutablePath { get; set; } = string.Empty;
public static List<string> TempoaryPaths { get; private set; } = new List<string>();
public static Stopwatch Flow { get; private set; } = new Stopwatch();
private static readonly ILog log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public static Highlight FromRescreen()
{
List<Buffered> screen = Rescreen.ScreenQueue.ToList();
List<Buffered> audio = Rescreen.AudioQueue.ToList();
List<Buffered> mic = Rescreen.MicQueue.ToList();
string gameName = "Desktop"; //need to support
return new Highlight(screen, audio, mic, gameName);
}
public static void AddHighlight()
{
Flow.Stop();
string path = GetTempFile("buf");
Highlight highlight = FromRescreen();
byte[] buffer = MessagePackSerializer.Serialize(highlight, BitmapConverter.LZ4_OPTIONS);
File.WriteAllBytes(path, buffer);
TempoaryPaths.Add(path);
Rescreen.ClearAllBuffer();
highlight.ScreenBuffers.Clear();
highlight.AudioBuffers.Clear();
highlight.MicBuffers.Clear();
Flow.Restart();
}
private static string GetTempFile(string extension)
{
return $"{Path.GetTempPath()}{Guid.NewGuid()}.{extension}";
}
public static async Task<string> ConvertToVideoAsync(string tempoaryPath)
{
if (!File.Exists(tempoaryPath)) return string.Empty;
byte[] buffer = File.ReadAllBytes(tempoaryPath);
Highlight highlight = MessagePackSerializer.Deserialize<Highlight>(buffer, BitmapConverter.LZ4_OPTIONS);
string fileName = GetHighlightName(highlight, "mp4");
string videoPath = await Task.Run(() => ProcessVideo(highlight));
string audioPath = await ProcessAudioAsync(highlight);
string outPath = string.Empty;
if (!string.IsNullOrWhiteSpace(audioPath))
{
outPath = await CombineAsync(videoPath, audioPath, fileName);
}
else
{
outPath = Path.Combine(LocalPath, fileName);
File.Copy(videoPath, outPath);
}
File.Delete(videoPath);
if (!string.IsNullOrWhiteSpace(audioPath)) File.Delete(audioPath);
File.Delete(tempoaryPath);
highlight.ScreenBuffers.Clear();
highlight.AudioBuffers.Clear();
highlight.MicBuffers.Clear();
return outPath;
}
public static string GetHighlightName(Highlight highlight, string extension)
{
DateTime date = highlight.ScreenBuffers.LastOrDefault()?.Time ?? DateTime.Now;
string dateName = date.ToString("yy-MM-dd-HH-mm-ss");
return $"{highlight.GameName} {dateName}.{extension}";
}
private static string ProcessVideo(Highlight highlight)
{
Mat ConvertLegacy(IntPtr ptr, int size, int width, int height)
{
Mat mat = new Mat(height, width, MatType.CV_8UC4, ptr);
Mat mat2 = mat.CvtColor(ColorConversionCodes.RGBA2BGRA);
mat.Dispose();
return mat2;
}
Mat ConvertNvColorSpace(IntPtr ptr, int size, int width, int height, out IntPtr bgra)
{
bgra = Marshal.AllocHGlobal(size);
int status = NvColorSpace.RGBA32ToBGRA32(ptr, bgra, width, height);
Mat mat = new Mat(height, width, MatType.CV_8UC4, bgra);
return mat;
}
string path = GetTempFile("mp4");
VideoWriter writer = new VideoWriter(path, FourCC.H265, Rescreen.FpsIfUnfixed60, Rescreen.ScreenSize.ToCvSize());
Rescreen.IsSaving = true;
Rescreen.OnDecoded = (e) =>
{
Mat mat = ConvertNvColorSpace(e.Item1, e.Item2, Rescreen.Decoder.width, Rescreen.Decoder.height, out IntPtr bgra);
writer.Write(mat);
Marshal.FreeHGlobal(bgra);
mat.Dispose();
};
foreach (Buffered buffered in highlight.ScreenBuffers)
{
byte[] buffer = buffered.Buffer.Decompress();
GCHandle pinnedArray = GCHandle.Alloc(buffer, GCHandleType.Pinned);
IntPtr ptr = pinnedArray.AddrOfPinnedObject();
Rescreen.Decoder.Decode(ptr, buffer.Length);
pinnedArray.Free();
}
writer.Release();
Rescreen.OnDecoded = null;
Rescreen.IsSaving = false;
return path;
}
private static async Task<string> ProcessAudioAsync(Highlight highlight)
{
string path = GetTempFile("mp3");
byte[] buffer = MergeAudio(highlight);
if (buffer.Length == 1 && buffer[0] == 0) //Is Buffer Null (Because of Not Sound Existing)
{
return string.Empty;
}
using (var writer = new LameMP3FileWriter(path, WasapiCapture.DeviceOutWaveFormat, 128))
{
await writer.WriteAsync(buffer, 0, buffer.Length);
}
return path;
}
private static byte[] MergeAudio(Highlight highlight)
{
void Apply(BufferedWaveProvider bwp, List<Buffered> buffers2)
{
foreach (Buffered buffered in buffers2)
{
byte[] buffer2 = buffered.Buffer.Decompress();
bwp.AddSamples(buffer2, 0, buffer2.Length);
}
}
byte[] buffer = new byte[0];
//2 : In,Out
//1 : In or Out
//0 : Not Exists (Null)
int availableAudioCount = 2;
bool isInAvailable = WasapiCapture.DeviceInWaveFormat != null;
bool isOutAvailable = WasapiCapture.DeviceOutWaveFormat != null;
//Sound Null
if (!isInAvailable) availableAudioCount--;
if (!isOutAvailable) availableAudioCount--;
BufferedWaveProvider bwpIn = null;
BufferedWaveProvider bwpOut = null;
MixingSampleProvider mixer = null;
VolumeSampleProvider volumeIn = null;
VolumeSampleProvider volumeOut = null;
MediaFoundationResampler resampledIn = null;
if (isInAvailable)
{
bwpIn = new BufferedWaveProvider(WasapiCapture.DeviceInWaveFormat)
{
DiscardOnBufferOverflow = true,
BufferDuration = TimeSpan.FromSeconds(Rescreen.ReplayLength + 2)
};
}
if (isOutAvailable)
{
bwpOut = new BufferedWaveProvider(WasapiCapture.DeviceOutWaveFormat)
{
DiscardOnBufferOverflow = true,
BufferDuration = TimeSpan.FromSeconds(Rescreen.ReplayLength + 2)
};
}
if (availableAudioCount > 0)
{
mixer = new MixingSampleProvider(WasapiCapture.DeviceOutWaveFormat)
{
ReadFully = true
};
}
if (isInAvailable) Apply(bwpIn, highlight.MicBuffers);
if (isOutAvailable) Apply(bwpOut, highlight.AudioBuffers);
if (isInAvailable)
{
volumeIn = new VolumeSampleProvider(bwpIn.ToSampleProvider())
{
Volume = availableAudioCount == 2 ? 0.5f : 1f
};
}
if (isOutAvailable)
{
volumeOut = new VolumeSampleProvider(bwpOut.ToSampleProvider())
{
Volume = availableAudioCount == 2 ? 0.5f : 1f
};
}
if (isInAvailable)
{
resampledIn = new MediaFoundationResampler(volumeIn.ToWaveProvider(), WasapiCapture.DeviceOutWaveFormat);
mixer.AddMixerInput(resampledIn);
}
if (isOutAvailable)
{
mixer.AddMixerInput(volumeOut);
}
if (availableAudioCount > 0)
{
//여기 코드 보충 필요
int bufferLength = Math.Max(bwpIn.BufferedBytes, bwpOut.BufferedBytes);
if (bufferLength > 0)
{
float[] bufferFloated = new float[bufferLength];
int read = mixer.Read(bufferFloated, 0, bufferLength);
buffer = new byte[read];
Buffer.BlockCopy(bufferFloated, 0, buffer, 0, read);
}
mixer.RemoveAllMixerInputs();
}
if (isInAvailable)
{
resampledIn.Dispose();
bwpIn.ClearBuffer();
}
if (isOutAvailable)
{
bwpOut.ClearBuffer();
}
return buffer;
}
private static async Task<string> CombineAsync(string inVideoPath, string inAudioPath, string outFileName)
{
string outPath = Path.Combine(LocalPath, outFileName);
string args = $"-i \"{inVideoPath}\" -i \"{inAudioPath}\" -preset ultrafast -tune fastdecode -shortest \"{outPath}\"";
ProcessStartInfo startInfo = new ProcessStartInfo
{
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
FileName = "ffmpeg.exe",
WorkingDirectory = FFmpegExecutablePath,
Arguments = args
};
if (File.Exists(outPath))
{
File.Delete(outPath);
log.Info($"{outFileName} has overwritten because of same file name.");
}
if (!Directory.Exists(LocalPath))
{
Directory.CreateDirectory(LocalPath);
var info = new DirectoryInfo(LocalPath);
log.Info($"{info.Name} Directory has created.");
}
using (Process process = Process.Start(startInfo))
{
await process.WaitForExitAsync();
}
return outPath;
}
}
}
나머지 코드 보충 필요
2023-04-22 13:41:17,594 [1] INFO IGReinforced.App - ============= Started application ============= 2023-04-22 13:43:02,943 [7] INFO IGReinforced.MainWindow - Highlight (30s) Recorded at 37s. Info : Resolution Per Frame : 1.55ms Delay Per Frame : 16.19ms Fps : 47.39 Fps (DPF) : 61.75 Mbps : 1.27 2023-04-22 13:43:17,916 [8] ERROR IGReinforced.MainWindow - Error occured System.NullReferenceException: 개체 참조가 개체의 인스턴스로 설정되지 않았습니다. 위치: NAudio.Wave.BufferedWaveProvider..ctor(WaveFormat waveFormat) 위치: IGReinforced.Recording.Highlights.HighlightManager.MergeAudio(Highlight highlight) 위치: IGReinforced.Recording.Highlights.HighlightManager.d23.MoveNext()
--- 예외가 throw된 이전 위치의 스택 추적 끝 ---
위치: System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
위치: System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
위치: System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
위치: IGReinforced.Recording.Highlights.HighlightManager.d 20.MoveNext()
--- 예외가 throw된 이전 위치의 스택 추적 끝 ---
위치: System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
위치: System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
위치: System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
위치: IGReinforced.MainWindow.d__12.MoveNext()
--- 예외가 throw된 이전 위치의 스택 추적 끝 ---
위치: System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
위치: System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
위치: System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
위치: System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
위치: System.Threading.ThreadPoolWorkQueue.Dispatch()