From 666e8f757a172688ac01e7eb91693aa5ebd084ce Mon Sep 17 00:00:00 2001 From: alantan Date: Sun, 5 Jul 2020 01:09:06 +0300 Subject: [PATCH 1/2] Avoid flickering when using SharpAvi. When using SharpAvi, frames occassionally get dropped and the Texture2DFrame.CopyTo method produces a transparent frame. This has been observed to happen when recording the Windows desktop applications such as File Explorer. This causes the recording to flicker and have flashes of empty frames. The fix checks for transparency by comparing the current frame to all zeros, and uses the previously saved frame if necessary. For performance, it uses memcmp to compare to a byte array prefilled with zeros. --- src/Captura.SharpAvi/AviWriter.cs | 74 ++++++++++++++++++++ src/Captura.SharpAvi/Captura.SharpAvi.csproj | 1 + src/Captura.Windows/Native/Msvcrt.cs | 15 ++++ 3 files changed, 90 insertions(+) create mode 100644 src/Captura.Windows/Native/Msvcrt.cs diff --git a/src/Captura.SharpAvi/AviWriter.cs b/src/Captura.SharpAvi/AviWriter.cs index 67a0ac71d..f9ee4f21d 100644 --- a/src/Captura.SharpAvi/AviWriter.cs +++ b/src/Captura.SharpAvi/AviWriter.cs @@ -1,6 +1,7 @@ using System; using Captura.Audio; using Captura.Video; +using Captura.Windows.Native; using SharpAvi.Codecs; using SharpAvi.Output; using AviInternalWriter = SharpAvi.Output.AviWriter; @@ -17,6 +18,9 @@ class AviWriter : IVideoFileWriter IAviVideoStream _videoStream; IAviAudioStream _audioStream; byte[] _videoBuffer; + byte[] _prevVideoBuffer; + readonly byte[] _zeroBuffer; + bool _hasOneGoodFrame = false; readonly AviCodec _codec; readonly object _syncLock = new object(); @@ -39,6 +43,12 @@ public AviWriter(string FileName, AviCodec Codec, IImageProvider ImageProvider, _codec = Codec; _videoBuffer = new byte[ImageProvider.Width * ImageProvider.Height * 4]; + _prevVideoBuffer = new byte[_videoBuffer.Length]; + _zeroBuffer = new byte[_videoBuffer.Length]; + for (int i = 0; i < _zeroBuffer.Length; i++) + { + _zeroBuffer[i] = 0; + } _writer = new AviInternalWriter(FileName) { @@ -66,7 +76,71 @@ public void WriteFrame(IBitmapFrame Frame) } lock (_syncLock) + { + if (IsTransparentOrTruncatedFrame(_videoBuffer)) + { + // To avoid dropped frames, just repeat the previous one + + if (_hasOneGoodFrame) + { + // Use previous frame instead + + _videoStream.WriteFrame(true, _prevVideoBuffer, 0, _prevVideoBuffer.Length); + } + else + { + // Just need to make do with what we have + + _videoStream.WriteFrame(true, _videoBuffer, 0, _videoBuffer.Length); + } + + return; + } + + if (!_hasOneGoodFrame) + { + _hasOneGoodFrame = true; + } + _videoStream.WriteFrame(true, _videoBuffer, 0, _videoBuffer.Length); + + // Save frame in case we need it as stand-in for next one + + Buffer.BlockCopy(_videoBuffer, 0, _prevVideoBuffer, 0, _videoBuffer.Length); + } + } + + bool IsTransparentOrTruncatedFrame(byte[] buffer) + { + if (_videoBuffer.Length != _zeroBuffer.Length) + { + return true; + } + + // First check first 100 bytes - assuming that in most cases they will not be transparent + // and therefore this will avoid having to check the entire frame + if (IsStartTransparent(buffer, 100)) + { + // If start is transparent compare the whole frame using P/invoke for performance + // We don't check the length as we already know that it is equal + return Msvcrt.memcmp(buffer, _zeroBuffer, buffer.Length) == 0; + } + else + { + return false; + } + } + + bool IsStartTransparent(byte[] buffer, int bytesToCheck) + { + for (int i = 0; i < bytesToCheck; i++) + { + if (buffer[i] != _zeroBuffer[i]) + { + return false; + } + } + return true; } void CreateVideoStream(int Width, int Height) diff --git a/src/Captura.SharpAvi/Captura.SharpAvi.csproj b/src/Captura.SharpAvi/Captura.SharpAvi.csproj index e679c3d0a..b3b25e5ce 100644 --- a/src/Captura.SharpAvi/Captura.SharpAvi.csproj +++ b/src/Captura.SharpAvi/Captura.SharpAvi.csproj @@ -4,6 +4,7 @@ + diff --git a/src/Captura.Windows/Native/Msvcrt.cs b/src/Captura.Windows/Native/Msvcrt.cs new file mode 100644 index 000000000..a870b8538 --- /dev/null +++ b/src/Captura.Windows/Native/Msvcrt.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Captura.Windows.Native +{ + public static class Msvcrt + { + [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)] + public static extern int memcmp(byte[] b1, byte[] b2, long count); + } +} From fe68e5144ff9d61b90141d60dee15608be71c67c Mon Sep 17 00:00:00 2001 From: alantan Date: Sun, 5 Jul 2020 10:12:22 +0300 Subject: [PATCH 2/2] Add --stop argument to CLI Use a token, such as a GUID, which can be used to stop a recording. Can be used with or without length: - If no length is provided, recording will continue indefinitely until a token signal is sent. - If a length is provided, recording will continue until the time has expired or a token signal is sent, whichever arrives first. Example code to stop a recording that was started using --stop ABCDE: var doneWithInit = new EventWaitHandle( false, EventResetMode.ManualReset, "ABCDE"); doneWithInit.Set(); --- .../CmdOptions/StartCmdOptions.cs | 3 +++ src/Captura.Console/ConsoleManager.cs | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Captura.Console/CmdOptions/StartCmdOptions.cs b/src/Captura.Console/CmdOptions/StartCmdOptions.cs index 3bba0fa12..69c7d9c17 100644 --- a/src/Captura.Console/CmdOptions/StartCmdOptions.cs +++ b/src/Captura.Console/CmdOptions/StartCmdOptions.cs @@ -54,6 +54,9 @@ class StartCmdOptions : CommonCmdOptions, ICmdlineVerb [Option("settings", HelpText = "Settings file to use for overlay settings, ffmpeg path and output path")] public string Settings { get; set; } + [Option("stop", HelpText = "Token, such as a GUID, to use to stop recording. Can be used with or without length")] + public string StopToken { get; set; } + [Usage] public static IEnumerable Examples { diff --git a/src/Captura.Console/ConsoleManager.cs b/src/Captura.Console/ConsoleManager.cs index 8668d5564..bb8109b6b 100644 --- a/src/Captura.Console/ConsoleManager.cs +++ b/src/Captura.Console/ConsoleManager.cs @@ -284,6 +284,22 @@ void HandleWebcam(StartCmdOptions StartOptions) void Loop(StartCmdOptions StartOptions) { + if (!string.IsNullOrEmpty(StartOptions.StopToken)) + { + var waitForSignal = new EventWaitHandle( + false, EventResetMode.ManualReset, StartOptions.StopToken); + + if (StartOptions.Length > 0) + { + waitForSignal.WaitOne(StartOptions.Length * 1000); + } + else + { + waitForSignal.WaitOne(); + } + return; + } + if (StartOptions.Length > 0) { var elapsed = 0;