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

时间:2015-07-21 15:52:59

标签: c# image sockets

这就是我如何编写你的漂亮代码(为了便于理解我的一些简单改动)

     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

3 个答案:

答案 0 :(得分:5)

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

你不想压缩视频流(所以MPEG,AVI等是不可能的 - 这些不是必须是实时的)你不想要压缩单个图片(因为那只是愚蠢的)。

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

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

如果你想要性能,可能最好用C ++做到这一点:目前的C#实现并没有对你的代码进行矢量化,这对你有很大帮助。

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

for (int y=0; y<height; ++y) // change to PFor if you like
{
    ulong* row1 = (ulong*)(image1BasePtr + image1Stride * y);
    ulong* row2 = (ulong*)(image2BasePtr + image2Stride * y);
    for (int x=0; x<width; x += 2)
        row2[x] ^= row1[x];
}

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

总而言之,如果您尝试压缩白噪声,它只会失败,您的帧速率会下降。解决这个问题的一种方法是,如果你有太多的随机性,就要减少颜色。在一个框架中。随机性的度量是熵,并且有几种方法来获得图片的熵的度量(https://en.wikipedia.org/wiki/Entropy_(information_theory))。我坚持用一个非常简单的方法:检查压缩图片的大小 - 如果它超过一定的限制,减少位数;如果在下面,增加位数。

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

for (int y=0; y<height; ++y) // change to PFor if you like
{
    ulong* row1 = (ulong*)(image1BasePtr + image1Stride * y);
    ulong* row2 = (ulong*)(image2BasePtr + image2Stride * y);
    ulong mask = 0xFFFCFCFCFFFCFCFC;
    for (int x=0; x<width; x += 2)
        row2[x] = (row2[x] ^ row1[x]) & mask;
}

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

祝你好运!

答案很长

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

此代码通常运行超过130 FPS,笔记本电脑上的内存压力很大,所以瓶颈不应该再存在了。请注意,您需要LZ4来实现此功能,并且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
{

接下来,如果我正在运行高性能代码,我会养成先预先分配所有缓冲区的习惯。这样可以节省您在实际算法中的时间。 4个1080p的缓冲区大约是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);
    }
}

正如我所说,我不想压缩&#39; raw&#39;像素。相反,我更喜欢压缩先前和当前图像的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

添加此功能后,您将在屏幕上显示以下内容:

  

迭代:0.00s,0.01s,0.01s 1 Kb =&gt; 152.0 FPS

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

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

您还可以将Parallel.For用于xor循环;我个人并不十分关心这一点。

更具挑战性

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

要实现这一点,prevcur之间的关系必须改变。如果我们只是&#39; xor&#39;就像在这里一样,我们会在较慢的客户端看到完全乱码的图片。

要解决这个问题,我们不想再交换prev,因为它应该保存关键帧(当压缩数据变得太大时你将刷新)和{{1} }将保存来自“xor”的增量数据。结果。这意味着只要cur位图是最近的,您就可以基本上抓取任意的“红色”帧并将其发送到线上。

答案 1 :(得分:1)

H264或Equaivalent Codec Streaming

有各种压缩流可用,几乎可以完成通过网络优化屏幕共享所能做的一切。有许多开源和商业图书馆可以流式传输。

块中的屏幕传输

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

窗口渲染信息

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

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

答案 2 :(得分:0)

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

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;
}