Windows远程桌面实现之二(抓屏技术之MirrorDriver镜像驱动开发)


                                                 by fanxiushu  2017-07-25  转载或引用,请注明原始作者
 
上一篇文章总体描述了三种抓屏办法,其他两种办法都是在应用层调用windows的接口函数实现,
链接地址: http://blog.csdn.net/fanxiushu/article/details/73269286
实现方法较为简单,因此也就一笔带过。
详细代码可查阅稍后提供到CSDN的抓屏部分代码。
镜像驱动牵涉到驱动,因此专门在这里阐述。
然而比起以前介绍的其他一些驱动,比如文件过滤驱动实现目录重定向,虚拟USB驱动,虚拟磁盘驱动,
摄像头驱动,NDIS驱动, TDI驱动等等,其实比起这些驱动来说,镜像驱动显得有些简单。

这个驱动跟TDI驱动一样,有些过时。
但是在WInXP,WIN7横行天下而且硬件也不如现在这么强劲的年代,这个驱动使用频率有点高。
而且在WINXP, WIN7系统中要更加高效的抓屏,除了使用这个驱动外,似乎也没有什么更好的办法。
但是现在的电脑硬件尤其是显卡已经不是10年前的蜗牛性能,因此使用BitBlt抓屏一般都能满足大部分的需求。

记得刚出来工作那会儿,印象较深刻,上篇文章说过,当时比较热衷于图形界面和这种远程控制,
因此看到当时有人实现镜像驱动来抓屏,非常羡慕,
但是当时的我对这种做法却是一窍不通,更大一点说,对windows底层驱动一窍不通。
然而现在想来,这种驱动却是各类驱动中容易实现的一个驱动。

首先我们需要简单熟悉Windows的 GDI。

在WINXP以前的年代,GDI是windows的核心显示引擎。
到了WIN7以后的年代,D3D是windows的核心显示引擎,GDI只是D3D的一个2D显示平面。
MirrorDriver(同网络传输TDI驱动一样)是WinXP年代的产物,因此它是围绕 GDI进行绘画的驱动。
MirrorDriver跟普通的WINXP显卡驱动一样,分为:
1)负责图像渲染的驱动,以dll方式运行在内核的会话空间,
2)负责处理硬件请求的小端口驱动,运行在内核全局空间。
这里的内核会话空间,我们可以这样理解:
就跟应用层空间一样,各个应用层程序是相互独立,不互相干扰。
内核会话空间也是一样,在内核中处于特殊的位置,每个会话空间相当于每个独立的应用层程序的空间一样。
内核可以有多个会话空间,一般我们登录到某个用户,内核就会建立相应的内核会话空间。

MirrorDriver的渲染驱动就运行在内核会话空间,渲染驱动负责图像绘制,硬件加速等,绘画好了之后,
通过小端口驱动最终显示到显示器上。

MirrorDriver的渲染驱动以dll方式存在,但是这个dll跟我们的应用层的动态库不一样。
首先入口函数不一样,应用层的Dll入口函数是DllMain; 由某个EXE程序负责调用应用层的DLL。
渲染驱动DLL的入口函数是DrvEnableDriver; MirrorDriver安装后,由相应的内核会话空间负责加载。
在DrvEnableDriver 入口函数中,我们需要提供一大堆的与应用层GDI函数相对应的回调函数列表。
回调函数列表伪代码如下:
static DRVFN gadrvfn[] =
{
    { INDEX_DrvDisableDriver,         (PFN)DrvDisableDriver }, //display驱动卸载
    { INDEX_DrvEnablePDEV,            (PFN)DrvEnablePDEV },    // 物理设备加载
    { INDEX_DrvCompletePDEV,          (PFN)DrvCompletePDEV }, //物理设备加载完成
    { INDEX_DrvDisablePDEV,           (PFN)DrvDisablePDEV },      //物理设备卸载
    { INDEX_DrvEnableSurface,         (PFN)DrvEnableSurface },   ///主表面加载
    { INDEX_DrvDisableSurface,        (PFN)DrvDisableSurface },  ///主表面卸载
    { INDEX_DrvAssertMode,            (PFN)DrvAssertMode },

    { INDEX_DrvEscape,                (PFN)DrvEscape }, //与应用层通讯的回调函数,对应应用层 ExtEscape函数

    { INDEX_DrvTextOut,               (PFN)DrvTextOut },   // 显示文字回调函数,与应用层GDI函数TextOut对应。
    { INDEX_DrvBitBlt,                (PFN)DrvBitBlt }, //   对应 BitBlt
    { INDEX_DrvCopyBits,              (PFN)DrvCopyBits },
    { INDEX_DrvStrokePath,            (PFN)DrvStrokePath },
    { INDEX_DrvLineTo,                (PFN)DrvLineTo },
    { INDEX_DrvFillPath,              (PFN)DrvFillPath },
    { INDEX_DrvStrokeAndFillPath,     (PFN)DrvStrokeAndFillPath },
    { INDEX_DrvStretchBlt,            (PFN)DrvStretchBlt },
    { INDEX_DrvAlphaBlend,            (PFN)DrvAlphaBlend },
    { INDEX_DrvTransparentBlt,        (PFN)DrvTransparentBlt },
    { INDEX_DrvGradientFill,          (PFN)DrvGradientFill },
    { INDEX_DrvPlgBlt,                (PFN)DrvPlgBlt },
    { INDEX_DrvStretchBltROP,         (PFN)DrvStretchBltROP },
};
当系统通过调用 DrvEnableDriver 入口函数获取到回调函数列表之后,接着调用回调函数DrvEnablePDEV,
完成之后调用DrvCompletePDEV 表示完成了物理设备的加载,
之后继续调用 DrvEnableSurface  加载主表面,完成之后,渲染驱动的初始化操作基本完成。
接着各种应用层程序调用的GDI函数都会进入我们渲染驱动对应的回调函数中。

举个例子来说:
当我们在应用层程序调用GDI函数绘图的时候,比如调用 TextOut 把某段文字显示到界面上,
某个应用层程序调用的TextOut的API函数,会进入系统的内核会话空间,
进而找到我们的渲染驱动的在DrvEnableDriver 入口函数中提供的TextOut对应的回调函数地址(比如名叫DrvTextOut),
于是在渲染驱动中,我们的DrvTextOut函数被调用。
我们在DrvTextOut回调函数中,负责这段文字的显示,如果不进行自己特殊的硬件加速处理,
直接调用 WIndows系统提供的默认的 EngTextOut 函数进行处理就可以了。
至于其他的GDI函数,比如BitBlt, LineTo,StretchBitBlt等,都是类似这样的过程,系统照样提供 Eng*前缀的默认的渲染函数。
MirrorDriver简单的工作流程就是:
所有的应用层的GDI函数调用,都会进入到渲染驱动的对应回调函数,然后在回调函数中负责画图,
这样应用层的任何界面绘图操作,MirrorDriver都能及时获取并处理。
虽然windows界面上显示出各种复杂的窗口,其实都是基本的GDI绘制而成,因此MirrorDriver截获到基本的GDI操作,
也就等于截获到整个windows屏幕的操作,因此利用MirrorDriver抓屏也就从此而来。

当然我们在应用层通过全局的HOOK所有程序的所有GDI函数,也能达到同样的目的,
但是很显然,没有在内核层直接和全面的获取GDI操作来的简洁和方便。

既然我们能在MirrorDriver的渲染驱动中实时的获取到各种GDI操作,自然也能知道这些GDI操作涉及到的范围,
也就是可以知道这些GDI会在电脑屏幕的哪个矩形框内绘制。还是以DrvTextOut为例。
DrvTextOut回调函数原型如下:
BOOL DrvTextOut(
    IN SURFOBJ *psoDst,
    IN STROBJ *pstro,
    IN FONTOBJ *pfo,
    IN CLIPOBJ *pco,
    IN RECTL *prclExtra,
    IN RECTL *prclOpaque,
    IN BRUSHOBJ *pboFore,
    IN BRUSHOBJ *pboOpaque,
    IN POINTL *pptlOrg,
    IN MIX mix )
{
     //// 首先调用系统默认函数Eng*把文字绘制出来,通过Eng*函数的绘制,
     //// 图像数据就会被绘制到我们指定的一块内存中,从而通过这块内存获取到原始RGB图像数据
    BOOL bRet = EngTextOut(psoDst, pstro, pfo, pco, prclExtra, prclOpaque, pboFore, pboOpaque, pptlOrg, mix);
    ///
    RECTL* rc = NULL;
    if (prclOpaque) rc = prclOpaque; else if (pstro) rc = &pstro->rclBkGround; ////
    RECORD_CHANGE(psoDst, rc, pco, NULL, OP_TEXTOUT); /// 报告发生变化的区域,这里 rc矩形框就是变化的区域
    /////

    return bRet;
}
我们首先通过检测上边的prclOpaque 参数,如果为空则再检测pstro->rclBkGround,
从而获取到应用层的TextOut会在频幕的哪个RECT内显示这段文字。
然后在渲染驱动中,我们通过某种通讯方式告诉给我们的应用层程序:电脑屏幕的这个RECT区域的图像即将发生变化。
这样我们就会实时获取屏幕的变化区域。
这也是MirrorDriver驱动截屏的一个特点,他不是整个屏幕的截获,而是可以实时的获取频幕变化区域。

windows提供 ExtEscape 应用层GDI函数来与渲染驱动进行通讯,在渲染驱动对应的回调函数原型是
ULONG DrvEscape(SURFOBJ *pso,
    ULONG iEsc,
    ULONG cjIn,
    PVOID pvIn,
    ULONG cjOut,
    PVOID pvOut);
我们在应用层调用CreateDC函数获取到我们的渲染驱动对应的HDC句柄,然后就可以调用 ExtEscape函数进行通讯,
渲染驱动中的 DrvEscape 回调函数就会被调用,于是我们可以在这个回调函数中把发生变化的矩形区域返回给
应用层ExtEscape函数,我们在应用层就能获取到变化的频幕区域。

但是这种通讯方式过于简单,因为各种应用程序调用GDI函数非常频繁,很可能发生的情况是:
在同一时间可能发生几百上千个变化的RECT矩形框,或者没操作电脑时候,很长时间都不会发生频幕变化的RECT矩形框。
于是我们想到要在渲染驱动层和应用层创建一个事件来进行同步,通知应用层何时接收变化的矩形区域。
大量的矩形区域同时存在时候,有必要对这些矩形区域进行合并(因为大部分情况下,这些矩形区域都是重叠的),
采用这两个措施之后,调用ExtEscape获取变化区域会提升许多性能,但是还不够,我们还应该获取更多的数据。

如果仅仅是从MirrorDriver驱动中获取变化的矩形框区域,通讯的数据量不是太大,使用ExtEscape应该就可以解决问题。
但是这样的话,我们在应用层获取到变化的区域之后,还得再次使用 BitBlt来获取这个变化区域内的图像数据。
也就是使用了MirrorDriver驱动之后,最终还是绕不开要利用 BitBlt 来截获频幕的RGB图像数据,
这显然没能发挥出MirrorDriver驱动的全部特性。
因此除了从MirrorDriver镜像驱动获取到实时变化的矩形框区域外,更应该从MirrorDriver获取到整个屏幕的实时的RGB图像数据。
整个屏幕的RGB图像数据是很庞大的,使用ExtEscape通讯不能解决这个问题,因此得另外想办法。

驱动与应用层交换庞大数据的最好办法莫过于使用共享内存。
先看看应用层使用共享内存的函数,
首先调用CreatFileMapping 创建共享文件句柄,如果CreatFileMapping第一个参数传递 INVALID_HANDLE_VALUE,
则从内存创建共享,如果第一个参数是打开的文件,则创建的是共享文件。
接着调用 MapViewOfFile 映射到程序的虚拟内存地址,这样我们就可以直接读写这个地址来操作这个共享内存块。
大致伪代码:
HANDLE hMapping = CreateFileMapping(INVALID_HANDLE_VALUE, ....);
LPVOID  addr = MapViewOfFile(hMapping, ....);
这样我们可以直接读写 addr 指向的内存块地址,这个addr指向的内存块就是共享内存。

在内核中对应的共享内存函数是
ZwCreateSection , 使用此函数创建共享内存句柄,然后使用 MmMapViewInSystemSpace 把它映射到内核地址空间,
使用 ZwMapViewOfSection 把它映射到当前的用户程序空间,
通过这种方式,驱动程序和应用层程序就能互相访问这块共享内存块。
大致伪代码如下:

status = ZwCreateSection( &SectionHandle, ....);
status = ObReferenceObjectByHandle(SectionHandle, SECTION_ALL_ACCESS,
              *MmSectionObjectType, KernelMode, (PVOID*)&SectionObject, NULL); 
status = MmMapViewInSystemSpace(SectionObject, (PVOID*)&SystemBuffer, &ViewSize); 

SystemBuffer 指向的地址就是在内核系统空间可以访问的共享内存块。
///////

status = ZwMapViewOfSection( SectionHandle, hUserProcess,
   (PVOID*)&address, .....);

 address指向的就是用户进程映射的地址,我们可以利用 ExtEscape 把 address的值传递给当前进程,
这样当前进程就可以访问address指向的共享内存块。

然而这里有个很大的问题,MirrorDriver的渲染驱动部分,也就那个以dll存在的负责绘制图像的驱动,
是运行在内核的会话空间,在这个空间运行的驱动,有很大的局限性。
最大的局限就是不能随意调用系统内核函数,
比如像上边的ZwCreateSection, MmMapViewInSystemSpace,ZwMapViewOfSection等函数就不会调用成功。
渲染驱动中能保证调用成功的函数就是以  Eng* 开头的函数,这些函数集中在 win32k.sys 内核组件中。
仔细查看 win32k.sys导出的函数,可以看到 EngMapSection函数,可惜我没找到关于这个函数的任何使用说明。
还可以找到 EngMapFile, 这个倒是有详细的说明文档,
不过它使用共享文件方式来达到共享目的,简单的说,我们要创建这个共享内存,还得创建一个临时共享文件。
使用EngMapFile虽然使事情变得简单,但是总感觉不太专业,因为多了个临时文件这么一个尾巴。
难道没有别的办法来使用ZwCreateSection 等函数了吗? 当然有!
MirrorDriver驱动分为两个部分,一个是渲染驱动,一个是小端口驱动。
小端口驱动运行在全局内核空间,可以调用任何内核函数。
我们可以在小端口驱动调用ZwCreateSection等函数来创建共享内存,
而渲染驱动和小端口驱动之间可以使用 EngDeviceIoControl 函数来通讯。
虽然这样显得有点罗嗦,但是总算是解决了问题。

大致伪代码如下。
在小端口驱动中:
VIDEO_HW_INITIALIZATION_DATA hwInitData;
...
/ / 接收 渲染驱动EngDeviceIoControl 发来的命令,这个是渲染驱动和小端口驱动通讯的唯一通道。
hwInitData.HwStartIO = MirrorStartIO;

BOOLEAN MirrorStartIO(
 PVOID HwDeviceExtension,
 PVIDEO_REQUEST_PACKET RequestPacket)
{
       switch (RequestPacket->IoControlCode)
     {
         case IOCTL_MAP_SYSTEMBUFFER: 
                // 调用 ZwCreateSection ,MmMapViewInSystemSpace 等函数  映射共享内存到内核空间
                break;
         case IOCTL_MAP_USERBUFFER:
                调用 ZwMapViewOfSection等函数映射共享内存到应用程序空间。
               break;
     }
}

再在渲染驱动中的 DrvEscape 回调函数中处理:
ULONG DrvEscape(SURFOBJ *pso,
 ULONG iEsc,
 ULONG cjIn,
 PVOID pvIn,
 ULONG cjOut,
 PVOID pvOut)
{
      switch (iEsc)
      {
          case ESCAPE_CODE_MAP_USERBUFFER:
                 ///调用EngDeviceIoControl 从小端口驱动获取到映射到用户空间的共享内存地址。
                 EngDeviceIoControl(pdev->hDriver, IOCTL_MAP_USERBUFFER, &pdev->shmem, sizeof(shmem_t), &userAddr, sizeof(PVOID), &bytes);
               。。。
      }
}

我们在应用层调用 ExtEscape(ESCAPE_CODE_MAP_USERBUFFER,.... &address);
就获取到共享内存地址,之后直接就可以操作address指向的内存。

除了这种办法让驱动和应用层共享内存之外,还有种办法,
就是通过MDL方式,也是前几篇文章介绍其他驱动时候,提到的一种办法。使用
MmMapLockedPagesSpecifyCache(。。。 , UserMode, ....); 函数把内核内存地址共享给应用层程序。
我们可以在MirrorDriver的小端口驱动中,分配足够的非分页内存作为共享内存,
应用层调用 ExtEscape 获取共享内存地址时候,渲染驱动通过EngDeviceIoControl 通知小端口驱动映射共享内存。
小端口驱动调用 MmMapLockedPagesSpecifyCache 映射这段内存到用户空间。
然后最终把共享内存用户空间地址传递给ExtEscape。这种办法也能让驱动和应用层程序共享内存块。
但是有个问题,不再使用时候,
我们必须 调用 MmUnmapLockedPages 解除映射,而且是必须在当前映射的进程环境中中解除映射。
当前进程退出时候,不解除映射的话可能会发生未知行为,甚至系统崩溃。
而MirrorDriver驱动的框架,我们是无法获取到当前进程的退出通知的。
当然我们可以在程序正常退出时候调用ExtEscape来解除映射,但是如果是程序崩溃之类的非正常退出该怎么办呢?
这会给系统带来安全隐患,我们还需写更多的代码来解决这个问题。
所以这里不打算使用MDL方式来共享内存。

共享内存的问题解决了,该如何让MirrorDriver驱动的图像数据报告给用户程序呢?
上边说过,系统调用渲染驱动的DrvEnableDriver入口函数获取到各种回调函数列表之后,接着会调用
DrvEnablePDEV 加载物理设备,然后调用DrvEnableSurface加载主表面
我们可以在DrvEnablePDEV 中创建好共享内存。
然后在DrvEnableSurface 中调用系统提供的EngModifySurface函数,把共享内存和主表面关联起来,
如此之后, 在其他的绘图回调函数中(比如DrvTextOut,DrvBitBlt等)调用系统默认的Eng*前缀的绘画函数(比如 EngTextOut等),
都会在这块共享内存里绘制图形,我们直接在应用层访问这块共享内存的数据,
就能获取到原始的RGB图像数据。
同时,共享内存还可以记录变化的矩形区域,一起提供给应用层程序访问。
比如定义如下数据结构

#define CHANGE_QUEUE_SIZE          50000
struct draw_change_t
{
 unsigned int    op_type; /// 各种GDI操作类型,其实没啥用
 ///
 RECT            rect;    /// 发生变化的区域
};
struct draw_queue_t
{
 unsigned int       next_index;                 /// 下一个将要绘制的位置, 从 0到 ( CHANGE_QUEUE_SIZE - 1 ),循环队列指示器
 draw_change_t      draw_queue[CHANGE_QUEUE_SIZE];      ///
};

共享内存的其中一部分存储 draw_queue_t 数据结构,在DrvTextOut等绘制图形的回调函数中, 计算出变化的RECT区域,
存储到draw_quque数组中,并且把next_index增加1, 当next_index超过 CHANGE_QUEUE_SIZE-1,重新设置 next_index为0,
从而形成一个循环不断的队列,驱动不断的朝这个循环队列写入变化的矩形框区域。
应用层从共享内存的这个draw_queue_t数据区获取这个队列,从而就能截取到变化的矩形区域。
详细的接口代码可查阅发布到CSDN相关的部分。

至此,MirrorDriver驱动基本就完成了。

我们在实现远程桌面时候,以40毫秒间隔(相当于每秒25帧的速率),
读取draw_queue_t共享内存数据区这段时间内所有变化的矩形框区域,
然后合并这些矩形框,并且通过矩形框和从共享内存的RGB原始图像数据区,得到40毫秒这段时间内,所有变化的图像数据。
接着把这些数据压缩,通过网络传输;再到客户端接收,解压缩,显示。
客户端第一次连接到被控制端,获取一张完整的屏幕图片,然后接下来就只需要获取发生变化区域的图像数据。
这就是远程桌面中图像数据传输采用的一种基本办法,只传输发生变化的图像数据。
而像windows自带的远程桌面(RDP协议),除了采用只传输变化的图像数据,还使用只传输属性描述信息的办法,
比如TextOut,它就只传输在那个位置,输出什么字符串,使用什么颜色和背景色等属性信息,
之后RDP客户端根据这些信息画出来,这能大量减少传输的图像数据量。

利用MirrorDriver驱动获取变化的图像数据,接着压缩,接着再通过网络传输,这应该是是实现远程桌面比较高效的解决办法。
截获到变化的图像数据之后,在图像数据压缩这部分,花了较多时间做了许多尝试。
比如先把RGBA图像数据转换成YV12格式,再使用zlib,lzma,lzo等算法做无损压缩,
或者把RGBA图像数据做JPEG压缩,或者把RGBA转成256色位图,再做无损压缩。
其中以JPEG压缩算法,网络传输量少,远程图像也较为顺畅,图像也较清晰。
但在屏幕图像有剧烈变化,比如播放视频,或者频繁和快速拖动窗口,传输图像占用的网络带宽依然很高。
这在带宽足够的局域网内没什么问题,但在带宽有限的环境中,看着一顿一卡的远程图像,将是受罪。
所以还得找更好的图像压缩算法,让占用带宽更少,图像质量更好。于是想到了H264。
使用H264编码屏幕图像前,一直持有的观点是H264是用来做视频图像压缩的,
如果让它做远程桌面图像压缩,不见得会带来什么好效果。
然而跟一直持有的顽固观点相反, 事实上图像显示效果不但比jpeg压缩清晰,更重要的是压缩后数据量大大的减少,
即使屏幕图像发生剧烈变化,H264压缩后的数据量和图像质量都能达到一个很好的效果。
在做H264压缩时候,只需要把整个屏幕图像数据输入,而不用那么麻烦的考虑当前屏幕比上一帧图像是哪些区域发生了变化。
也就是上边强调的MirrorDriver的一个特性(能够实时获取变化的矩形区域)在H264面前变得一无是处。
这的确认让人变得有点气馁。

事实上,MirrorDriver驱动实时获取变化区域的特性也不见得都比较靠谱。
至少遇到的一种情况,正是这种情况才让我觉得MirrorDriver驱动通过截获GDI函数来获取变化的矩形区域不一定正确。
 当时在测试的时候,用IE11浏览器打开v.qq.com(腾讯视频)看电视剧的时候,IE使用的是Flash插件播放视频文件。
 当播放电视剧的时候,MirrorDriver能不断的截获到播放视频的矩形框,表示这个矩形框的图像在不断变化。
非常奇怪的是,当我暂停视频播放,MirrorDriver驱动还在不断的截获到这个播放视频的矩形框,
但是我已经停止了视频播放,屏幕上几乎是没有任何变化,MirrorDriver驱动却还在不停的报告变化的矩形区域。
一开始以为是开发的MirrorDriver驱动的BUG,于是开始疯狂调试。最终发现了问题所在。
原因是渲染驱动里边的 DrvCopyBits 回调函数在不停的被调用。
是什么造成DrvCopyBits不停的被调用呢, MSDN文档解释是主表面和后台表面在不停的发生数据copy。
简单的说, 我们创建一个内存DC(memdc),先在 memdc 绘制好图像,然后使用 BitBlt 把memdc翻转到屏幕DC中。
腾讯视频那个网站flash的开发者,应该是在暂停视频后,还在后台不停的调用BitBlt翻转实际上已经是静止的视频图像到主屏幕DC,
所以才造成虽然屏幕图像没变化,但是 BitBlt还在不停运行,结果MirrorDriver驱动里边的 DrvCopyBits不停的被调用。
 这应该算是他们网站的一个bug,但是不管怎么说,这也是一种应该考虑的情况,
就是即使调用了GDI函数,也不见得图像数据实际发生了变化。
唯一能确定图像数据发生变化的,就是保存上一帧图像数据,然后做前后比较,才能完全确定哪些区域发生了变化。
因此发到CSDN上的抓屏源代码,其实都做了这样的工作,即使使用镜像驱动抓屏,最终也做了前后图像的比较。
 
 H264输入一帧图像数据不需要报告哪些区域发生了变化,实际上,H264压缩算法会自己比较和处理,他的算法很复杂。
 当然,为了尽量减少H264压缩时候CPU占用,我们应该事先粗略比较前后屏幕数据是否发生变化,
发生变化再输入整帧图像数据(哪怕是只一个像素变化)。
 
 说到这里,想到高效的图像压缩算法一些有意思的用途。
 广场这些公共场所,有些地方有液晶屏,而且一般都是非常大的,起码几百平米。
 最终这种庞然大物都需要数据输入源,才能显示出各种信息。
 大部分人的想法是就在庞然大物旁边装台电脑,然后电脑屏幕输入给这个庞然大物。
但也可能事与愿违,控制中心可能在很远的地方,
这种情况下,专用的数据线(比如HDMI,VGA,DVI等)不够长,而且距离长了信号衰减也厉害。
于是最容易想到的就是网络传输,部署网线又廉价又简单。
 在庞然大物液晶显示屏嵌入一个小设备,接收网络传输过来的压缩的视频数据,比如是H264压缩的,解压缩,输入给庞然大物显示。
 然后在控制中心,使用一台小小的电脑抓屏然后H264压缩,再通过网络传输给庞然大物。
就这样,一台电脑的小小屏幕被放大到广场上的屏幕的几百倍,想想也够酷的。
当然,如果弄张美女裸照在电脑上,然后电脑通过网络传输到广场上那个庞然大物中,会更加惊艳!
再比如现在城市中各种摄像头,拍摄的图像都需要通过网络传输到控制中心,
而视频数据都是非常大的,都需要通过高效的图像压缩算法压缩。
类似这种需求应该是非常之多。


抓屏部分代码下载地址:

http://download.csdn.net/detail/fanxiushu/9910360


最后来两张图,一张是正在开发的远程客户端程序效果图,
一张是使用WebSocket,在浏览器使用javascript脚本的开源的h264bsd解码H264 显示效果图。
因为正在开发,还不太完善。
First:
 

Second:

猜你喜欢

转载自blog.csdn.net/fanxiushu/article/details/76039801
今日推荐