c#屏幕传输超过socket有效改进方式

多数民众赞成我如何编写你的美丽代码(为了便于理解我的一些简单的改变)

private void Form1_Load(object sender, EventArgs e) { prev = GetDesktopImage();//get a screenshot of the desktop; cur = GetDesktopImage();//get a screenshot of the desktop; var locked1 = cur.LockBits(new Rectangle(0, 0, cur.Width, cur.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); var locked2 = prev.LockBits(new Rectangle(0, 0, prev.Width, prev.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); ApplyXor(locked1, locked2); compressionBuffer = new byte[1920* 1080 * 4]; // Compressed buffer -- where the data goes that we'll send. int backbufSize = LZ4.LZ4Codec.MaximumOutputLength(this.compressionBuffer.Length) + 4; backbuf = new CompressedCaptureScreen(backbufSize); MessageBox.Show(compressionBuffer.Length.ToString()); int length = Compress(); MessageBox.Show(backbuf.Data.Length.ToString());//prints the new buffer size } 

压缩缓冲区长度例如是8294400 ,backbuff.Data.length是8326947

我不喜欢压缩建议,所以这就是我要做的。

你不想压缩video流(所以MPEG,AVI等是不可能的 – 这些不一定是实时的)你不想压缩单个图片(因为那只是愚蠢的)。

基本上你想要做的是检测事情是否发生变化并发送差异。 你正走在正确的轨道上; 大多数video压缩器都这样做。 您还需要一种快速压缩/解压缩算法; 特别是如果你去更多相关的FPS。

差异。 首先,消除代码中的所有分支,并确保内存访问是顺序的(例如,在内部循环中迭代x)。 后者将为您提供缓存局部性。 至于差异,我可能会使用64位异或; 它简单,无分支,快速。

如果你想要性能,最好在C ++中做到这一点:当前的C#实现不会对代码进行矢量化,这对你有很大的帮助。

做这样的事情(我假设是32位像素格式):

 for (int y=0; y 

快速压缩和解压缩通常意味着更简单的压缩算法。 https://code.google.com/p/lz4/就是这样一种算法,并且还有适当的.NET端口。 您可能想要了解它的工作原理; 在LZ4中有一个流媒体function,如果你可以让它处理2个图像而不是1个,这可能会给你一个很好的压缩提升。

总而言之,如果你试图压缩白噪声,它根本不起作用,你的帧速率会下降。 解决此问题的一种方法是,如果框架中有太多“随机性”,请减少颜色。 随机性的度量是熵,并且有几种方法来获得图片的熵的度量( https://en.wikipedia.org/wiki/Entropy_(information_theory) ))。 我会坚持一个非常简单的方法:检查压缩图片的大小 - 如果它高于某个限制,减少位数; 如果低于,则增加位数。

注意,在这种情况下,不会通过移位来增加和减少位; 你不需要删除你的位,你只需要你的压缩工作更好。 使用带有位掩码的简单“AND”可能同样出色。 例如,如果要删除2位,可以这样做:

 for (int y=0; y 

PS:我不确定我会用alpha组件做什么,我会把它留给你的实验。

祝好运!


答案很长

我有空闲时间,所以我只测试了这种方法。 这里有一些支持它的代码。

这段代码通常运行超过130 FPS,笔记本电脑上有一个很好的恒定内存压力,所以瓶颈不应该在这里了。 请注意,您需要LZ4来实现这一function,并且LZ4的目标是高速 ,而不是高压缩比 。 稍后再谈一点。

首先,我们需要一些东西来保存我们要发送的所有数据。 我不是在这里实现套接字的东西(尽管使用它作为开始应该非常简单),我主要专注于获取发送内容所需的数据。

 // The thing you send over a socket public class CompressedCaptureScreen { public CompressedCaptureScreen(int size) { this.Data = new byte[size]; this.Size = 4; } public int Size; public byte[] Data; } 

我们还需要一个能掌握所有魔力的课程:

 public class CompressScreenCapture { 

接下来,如果我正在运行高性能代码,我会习惯首先预先分配所有缓冲区。 这将节省您在实际算法期间的时间。 1080p的4个缓冲区大约是33 MB,这很好 - 所以让我们分配它。

 public CompressScreenCapture() { // Initialize with black screen; get bounds from screen. this.screenBounds = Screen.PrimaryScreen.Bounds; // Initialize 2 buffers - 1 for the current and 1 for the previous image prev = new Bitmap(screenBounds.Width, screenBounds.Height, PixelFormat.Format32bppArgb); cur = new Bitmap(screenBounds.Width, screenBounds.Height, PixelFormat.Format32bppArgb); // Clear the 'prev' buffer - this is the initial state using (Graphics g = Graphics.FromImage(prev)) { g.Clear(Color.Black); } // Compression buffer -- we don't really need this but I'm lazy today. compressionBuffer = new byte[screenBounds.Width * screenBounds.Height * 4]; // Compressed buffer -- where the data goes that we'll send. int backbufSize = LZ4.LZ4Codec.MaximumOutputLength(this.compressionBuffer.Length) + 4; backbuf = new CompressedCaptureScreen(backbufSize); } private Rectangle screenBounds; private Bitmap prev; private Bitmap cur; private byte[] compressionBuffer; private int backbufSize; private CompressedCaptureScreen backbuf; private int n = 0; 

首先要做的是捕获屏幕。 这是一个简单的部分:只需填写当前屏幕的位图:

 private void Capture() { // Fill 'cur' with a screenshot using (var gfxScreenshot = Graphics.FromImage(cur)) { gfxScreenshot.CopyFromScreen(screenBounds.X, screenBounds.Y, 0, 0, screenBounds.Size, CopyPixelOperation.SourceCopy); } } 

正如我所说,我不想压缩'原始'像素。 相反,我更喜欢压缩先前和当前图像的XOR蒙版。 大多数时候这会给你很多0,这很容易压缩:

 private unsafe void ApplyXor(BitmapData previous, BitmapData current) { byte* prev0 = (byte*)previous.Scan0.ToPointer(); byte* cur0 = (byte*)current.Scan0.ToPointer(); int height = previous.Height; int width = previous.Width; int halfwidth = width / 2; fixed (byte* target = this.compressionBuffer) { ulong* dst = (ulong*)target; for (int y = 0; y < height; ++y) { ulong* prevRow = (ulong*)(prev0 + previous.Stride * y); ulong* curRow = (ulong*)(cur0 + current.Stride * y); for (int x = 0; x < halfwidth; ++x) { *(dst++) = curRow[x] ^ prevRow[x]; } } } } 

对于压缩算法,我只需将缓冲区传递给LZ4,让它发挥其魔力。

 private int Compress() { // Grab the backbuf in an attempt to update it with new data var backbuf = this.backbuf; backbuf.Size = LZ4.LZ4Codec.Encode( this.compressionBuffer, 0, this.compressionBuffer.Length, backbuf.Data, 4, backbuf.Data.Length-4); Buffer.BlockCopy(BitConverter.GetBytes(backbuf.Size), 0, backbuf.Data, 0, 4); return backbuf.Size; } 

这里需要注意的一点是,我习惯将所有内容都放在我需要通过TCP / IP套接字发送的缓冲区中。 我不想移动数据,如果我可以轻松避免它,所以我只是把我需要的所有东西都放在那里。

至于套接字本身,你可以在这里使用同步TCP套接字(我愿意),但如果这样做,你需要添加一个额外的缓冲区。

唯一剩下的就是将所有内容粘合在一起并在屏幕上显示一些统计信息:

 public void Iterate() { Stopwatch sw = Stopwatch.StartNew(); // Capture a screen: Capture(); TimeSpan timeToCapture = sw.Elapsed; // Lock both images: var locked1 = cur.LockBits(new Rectangle(0, 0, cur.Width, cur.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); var locked2 = prev.LockBits(new Rectangle(0, 0, prev.Width, prev.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); try { // Xor screen: ApplyXor(locked2, locked1); TimeSpan timeToXor = sw.Elapsed; // Compress screen: int length = Compress(); TimeSpan timeToCompress = sw.Elapsed; if ((++n) % 50 == 0) { Console.Write("Iteration: {0:0.00}s, {1:0.00}s, {2:0.00}s " + "{3} Kb => {4:0.0} FPS \r", timeToCapture.TotalSeconds, timeToXor.TotalSeconds, timeToCompress.TotalSeconds, length / 1024, 1.0 / sw.Elapsed.TotalSeconds); } // Swap buffers: var tmp = cur; cur = prev; prev = tmp; } finally { cur.UnlockBits(locked1); prev.UnlockBits(locked2); } } 

请注意,我减少了控制台输出,以确保不是瓶颈。 🙂

简单的改进

压缩所有这些0会有点浪费,对吗? 使用简单的布尔值跟踪包含数据的最小和最大y位置非常容易。

 ulong tmp = curRow[x] ^ prevRow[x]; *(dst++) = tmp; hasdata |= tmp != 0; 

如果你不需要,你也可能不想打电话给Compress

添加此function后,您将在屏幕上看到以下内容:

迭代:0.00s,0.01s,0.01s 1 Kb => 152.0 FPS

使用其他压缩算法也可能有所帮助。 我坚持使用LZ4,因为它使用简单,速度快,压缩效果非常好 - 但是,还有其他选项可能效果更好。 有关比较,请参见http://fastcompression.blogspot.nl/ 。

如果连接错误或者通过远程连接传输video,则所有这些都无法正常工作。 最好在这里减少像素值。 这很简单:在xor期间将一个简单的64位掩码应用于前一个和当前的图片......您也可以尝试使用索引颜色 - 无论如何,您可以尝试使用大量不同的东西; 我只是保持简单,因为这可能足够好了。

你也可以使用Parallel.For作为xor循环; 我个人并不在乎。

更具挑战性

如果您有1台服务器为多个客户端提供服务,那么事情将变得更具挑战性,因为它们将以不同的速率刷新。 我们希望最快速刷新客户端来确定服务器速度 - 而不是最慢。 🙂

为了实现这一点, prevcur之间的关系必须改变。 如果我们像这里一样“离开”,我们最终会在较慢的客户端看到一张完全乱码的图片。

为了解决这个问题,我们不再需要交换prev ,因为它应该保存关键帧(当压缩数据变得太大时你将刷新)并且cur将保存来自'xor'结果的增量数据。 这意味着你可以基本上抓取任意'xor'red帧并通过线路发送它 - 只要prev bitmap是最近的。

H264或Equaivalent Codec Streaming

有各种压缩流可用,几乎可以做任何事情来优化网络上的屏幕共享。 有许多开源和商业库可供流式传输。

块中的屏幕传输

H264已经这样做了,但是如果你想自己做,你必须将你的屏幕划分为100×100像素的较小块,并将这些块与以前的版本进行比较,并通过网络发送这些块。

窗口渲染信息

Microsoft RDP做得更好,它不会将屏幕作为光栅图像发送,而是分析屏幕并根据屏幕上的窗口创建屏幕块。 然后它分析屏幕的内容并仅在需要时发送图像,如果它是包含一些文本的文本框,则RDP将信息发送到具有字体信息和其他信息的文本的渲染文本框。 因此,它不是发送图像,而是发送有关渲染内容的信息。

您可以组合所有技术并制作混合协议,以使用图像和其他渲染信息发送屏幕块。

您可以将其作为整数数组处理,而不是将数据作为字节数组处理。

 int* p = (int*)((byte*)scan0.ToPointer() + y * stride); int* p2 = (int*)((byte*)scan02.ToPointer() + y * stride2); for (int x = 0; x < nWidth; x++) { //always get the complete pixel when differences are found if (*p2 != 0) *p = *p2 ++p; ++p2; }