[原创教程]LabWindows/CVI入门之第六章:综合实例:远程监控系统
导航目录
6.1 任务描述
在本节中,我们将完成:
Ø 一个可以提供视频源的服务器程序
Ø 一个可以链接到指定视频源的网站
通过本节的程序,我们可以在任意终端(含移动终端)上查看任意视频源传回的实时监控视频。
在本节中,我们最终完成如图 6‑1和图 6‑2所示的程序。
其中,服务器端视频源提供程序在CVI开发环境下使用C语言完成,网站使用jQuery+Ajax+JSON的方案在Apache下完成。
6.2 系统整体设计
摄像头驱动设计
市面上常见的USB摄像头均为免驱设计。“免驱”的意思并不是说这种摄像头不需要驱动即可运行,而是因为目前市面上主流的操作系统中已经包含了该摄像头所需要的驱动程序,当摄像头连接计算机后无需额外安装驱动程序。
因为摄像头连接至计算机后便已经加载了驱动程序,所以我们不必使用CVI去开发驱动了。况且NI-VISA中 USB只支持控制传输、批量传输、中断传输三种传输类型,不支持USB视频设备所采用的同步传输类型,所以使用NI-VISA开发摄像头的驱动程序也存在诸多不便。
视频与图像处理
目前在Windows下常用的开发摄像头的方法有DirectShow与AVICAP32两种。DirectShow相比于AVICAP32更新,性能更高一些。但是考虑到普通摄像头加载驱动时均加载avicap32.dll,而且使用AVICAP32开发复杂度较低,因此本节中采用AVICAP32进行开发进行示范。建议大家在实际应用中更多采用DirectShow等新技术进行开发。
客户端/服务器通信
基于Internet的设备之间的通信可以分为“请求-响应”以及“广播”等模式。“请求-响应”模式是指当客户端向服务器请求资源时服务器将资源信息返回的模式,而“广播”模式是指服务器不断的发送广播消息而客户端选择消息进行处理的模式。
在“请求-响应”模式下,浏览器发送的请求被服务器接收后,向浏览器返回当前的摄像头的一帧数据,浏览器接收后显示在浏览器中。
在“广播”模式下,服务器每隔一段时间更新存放在服务器中的一帧数据。浏览器定时去访问该帧数据即可。
以上两种模式中,在没有客户端访问的情况下,“广播”模式仍然在进行大量数据的处理,而且没有考虑到复杂的网络延时、文件占用等情况,采用此类方法的客户端会出现各种异常情况,所以在此采用“请求-响应”模式。
通信协议选择
由于客户端采用网络浏览器(IE、Chrome、Opera、Safari、FireFox等)进行访问,因此客户端发送的消息基于HTTP协议。服务器端软件采用CVI进行开发,而CVI不包含HTTP协议库,所以我们采用几种思路:
Ø 使用第三方HTTP协议库
Ø 使用.Net开发HTTP协议需要的函数生成DLL供CVI调用
Ø 直接使用TCP库
以上三种思路各有优缺点。使用第三方HTTP协议库最简单,但非官方的库设计的程序可能存在缺陷;使用.Net开发DLL供CVI调用也可,但需要用户对.Net开发比较了解,而且编写DLL后调试相对麻烦;HTTP协议基于TCP协议,所以使用TCP库也可以读出HTTP报文的信息,但直接使用TCP库需要用户对HTTP报文比较了解。
考虑到本项任务中,服务器端接收的请求相对比较简单,接收到合理的请求之后只需要服务器返回摄像头中的数据即可,所以我们采用使用TCP库的方式进行开发。
HTTP服务器选择
架设网站时,我们需要选择一个合适的HTTP服务器以方便我们开发的网站运行。目前市面上常见的HTTP服务器有以下几种:
IIS服务器
Apache服务器
Nginx服务器
其中IIS是微软开发的服务器,集成于Windows操作系统中,不过相对于Apache较不稳定。Apache是免费的服务器,可以引用于Windows、UNIX、Linux等操作系统中,并且具有较强的安全性与开放性。而Nginx是一个高性能的HTTP和反向代理服务器,但Nginx并不支持cgi方式运行,适合处理静态文件。
我们的网站基于HTTP、JavaScript、JSON以及CSS语言,此类语言均有国际标准,而我们的网站并不存在动态页面,因此采用以上任意服务器均可。
在此我们以跨平台、免费的Apache服务器为例。
传输数据格式选择
浏览器发送加载一帧图像的请求,或者服务器传送一帧图像被浏览器接收的过程中都涉及到数据的传输。此时会经常遇到多个不同类型的数据一同传输的问题。常常用来进行Web之间数据传输的方式有XML与JSON。
XML(Extensible Markup Language),是一种允许用户对自己的标记语言进行定义的源语言,可以方便的以文本的方式存储各种类型的数据。而JSON(JavaScript Object Notation)是一种轻量级的数据交换方式,可以直接被JavaScript识别并且转换成JavaScript对象。
考虑到JSON与JavaScript转换方便性以及JSON更加简洁的特性,在此我们选择JSON作为传输格式。
HTTP请求方式选择
Ajax(Asynchronous JavaScript and XML,异步的JavaScript和XML),是一种用于客户端与服务器间进行异步的数据交换的方法。“异步”,是与“同步”相对而言的。之所以叫做“异步”是因为在使用Ajax进行数据请求与数据接收时无需伴随网页的刷新(Refresh)与加载(Load)。
伴随着网页刷新的数据加载方式严重影响了用户体验,因此我们在此使用可以让网页局部刷新的Ajax技术。
每帧数据传输方式选择
既然我们选定了使用JSON所谓数据传输的格式,那么当用户请求一帧数据时,那么回传的JSON字符串中应该包含一帧数据的全部内容呢,还是一帧数据的URL呢?
考虑到用户体验,我们应该在成功加载完一帧数据之后再加载第二帧数据。而在JavaScript中,当JSON传输结束后可以设定回调函数,而给图片等资源文件加载结束设置回调函数较复杂。所以我们需要尽可能的将图片的内容放在JSON中进行传输。
而JSON并不能完美的支持二进制文件的传输, 但使用JSON传输字符串比较方便。因此我们可以采取将图片文件编码Base64编码的方式,将二进制的文件转换成字符串。
Base64编码是一种基于64个可打印字符来表示二进制数据的表示方法[来自维基百科],是一种将3个字节转换成4个可以打印出来的字符来表示的一种方法。Base64常用于在通常处理文本数据的场合,表示、传输、存储一些二进制数据。目前一般的浏览器均支持直接显示Base64编码的图片。
因此,虽然转换成Base64编码之后文件体积增大了约33%,但是由于图片本身已经过JPEG算法压缩,对于640×480的图片而言只有几十KB,因此仍然是一种较为理想的传输方式。
6.3 服务器端图像采集设计
6.3.1 VFW库
VFW(Video for Windows)是Windows专门提供的用来进行视频处理的库,可以被市面上主流的摄像头所支持,并且可以将视频流存储在硬盘或其他介质上。使用VFW进行开发所需的avicap32.dll已经在操作系统中提供,而CVI安装路径下的sdk/lib文件夹中也有avicap32.lib导入库文件。
我们可以使用DEPENDS工具查看avicap32.dll中所包含的函数。
图 6‑3 Depends工具查看avicap32.dll中包含的函数
在DEPENDS工具中,我们注意到avicap32.dll只提供了6个函数。因为avicap32大部分的操作基于Windows Message机制,所以绝大多数操作通过SendMessage()函数经由Windows的消息缓冲区队列进行,而非用户主动调用函数。这种设计具有一些优势:因为视频流是采用USB同步传输的方式,用户对库中函数的调用会影响同步传输的过程。而采用消息机制则允许avicap只在不影响同步传输的时候处理用户的操作,虽然用户的操作优先级被降低了,但是保证了USB同步传输,提高了视频流的处理能力。
例如,开启摄像头并实时捕捉视频则可以采用以下几行代码实现:
其中上述代码中,WM_CAP_DRIVER_CONNECT消息与WM_CAP_SEQUENCE消息分别是(WM_USER+ 10) 与(WM_USER+ 62)消息的宏定义。而WM_USER是为了防止用户定义的消息ID与系统定义的ID冲突而制定的宏定义,一般值为0x400,小于WM_USER的消息ID被系统使用,用户可以自己定义大于WM_USER的消息ID并使用。
我们向摄像头句柄发送指定的消息即可控制摄像头采取对应的动作。在avicap32中,常用的消息如下所示。
6.3.2 UI设计
为了方便的在服务器端进行控制与消息显示,我们新建一个uir文件,并且在上面放置一个Canvas控件用于图像显示,四个Button控件分别用于实现“开始预览视频”、“停止预览视频”、“截图”以及“关闭程序”的功能。
图 6‑4 服务器端软件UI
uir文件编辑完成后,我们点击菜单“Code-Generate-All Code”自动生成全部代码。
6.3.3 程序设计
添加“开始”按钮响应代码
我们给“开始”按钮添加响应代码。响应代码如下:
在上面的代码中,我们首先停用“开始”按钮,防止用户重复点击本按钮,并启用“停止”、“截图”按钮。因为打开摄像头时需要知道当前程序的Windows句柄,所以我们使用GetPanelAttribute()函数来获取当前程序的ATTR_SYSTEM_WINDOW_HANDLE属性。通过capCreateCaptureWindowA函数打开摄像头后,我们就获取了摄像头的句柄。
有了摄像头的操作句柄之后,我们就可以可以很方便的向摄像头发送控制命令了。在上面的代码中,我们设置了预览比例,设置更新频率等消息,并且启动了视频预览。
添加“停止”按钮响应代码
同“开始”按钮一样,我们给“停止”按钮添加响应代码。我们给“停止”按钮添加的响应代码如下。(此处省去了消息函数体,下同)
在上面的代码中,我们首先向摄像头发送了断开连接的命令,然后启用或禁用了相应的按钮,防止误操作。
添加“截图”按钮响应代码
在AVICAP32中,截图也只需要发送一个命令即可。我们给“截图”按钮添加如下的响应代码。
在上面的代码中,当用户点击“截图”按钮后,首先弹出文件位置选择对话框,如果用户选择完毕则将图片保存在用户的指定位置上。
添加“退出”按钮响应代码
退出按钮比较简单,我们只需要添加以下一行代码即可:
依次添加了以上四个按钮的回调函数之后,我们的程序就已经具备了预览视频并且截取视频中的一帧图像保存到指定的位置上的功能。
添加“自动截图”代码
但是仅完成上面的演示功能还不够,因为我们需要在接收到用户请求之后自动截取一帧的数据并回传给客户端。默认截取一帧的图像的保存格式为.bmp,但bmp格式的文件未经压缩占用空间较大,所以我们需要将bmp文件另存为jpg文件。
CVI并不带JPEG压缩算法库,但是提供了一个函数SaveBitmapToJPEGFile()可以将bmp文件另存为jpg文件。于是我们在每次截图并保存为bmp文件后,再将bmp文件转换成jpg文件。
自动截图的代码如下:
其中wParam变量为函数的形参,为一随机数。这样每次截取的图都会保存为一个[随机数].jpg的文件。
需要注意的是,bitmap本质上是一个指针,GetBitmapFromFile()函数调用时指针指向了分配的一些空间,因此使用完bitmap后需要将bitmap空间释放。
6.4 服务器端网络服务设计
6.4.1 HTTP报头分析与回发数据格式分析
HTTP报头分析
由于我们选择TCP库进行数据的收发与处理,而要进行数据的收发与处理,我们首先要知道当浏览器向服务器发起请求的时候,浏览器向服务器发送了什么消息。
我们直接运行“3.1.2 网络通信:TCP库”中TCP服务端的示例代码,看浏览器发起HTTP请求的时候,TCP服务器收到了什么数据。
由于示例程序中设置的端口号码是26845号端口,因此当示例程序运行时,我们在浏览器地址栏中输入http://localhost:26845 并且按回车键。
图 6‑5 在浏览器中输入http://localhost:26845/
则我们可在服务端的程序中看到,TCP服务端收到如下消息:
图 6‑6 在服务器端收到的数据
我们来简单分析一下收到的数据分别代表什么含义。
第一行中,GET表示浏览器发送的是GET请求。/表示访问的是根目录路径,不带任何参数。HTTP/1.1表明采用的HTTP协议版本号。
第二行HOST表示发起请求的地址和端口。
第三行Connection是连接类型,表示是否需要持久连接。如果值为keep-alive则表示使用持久连接,当页面包含多个元素时持久连接可以有效的减少多次连接所造成的请求数量的锐增。
第四行Cache-Control中的max-age=0表示每次都会重复访问而不会从缓存中读取。
剩余的其他行的HTTP报头表明了浏览器可以接收的数据类型、编码方式、语言等信息。由于我们返回JSON代码,所有返回文字全部是英文与字符,所以可以不必关心这部分信息。
回发数据格式分析
收到HTTP请求后,我们首先获取浏览器发送的数据,然后分析请求的内容里面callback参数,并且将抓到的图片数据以及服务器状态一并返回。
为什么要先获取请求里面的callback参数呢?
原来,我们要架设的网站使用的端口号是80,而我们提供视频源的服务器使用的端口号是26845。当网站上的JavaScript试图去访问视频源时,由于两个服务所在的端口号不一致,所以JavaScript的安全机制会限制“跨域”的访问,导致访问失败。而JSONP技术可以解决此类问题。
JSONP是一种非官方跨域数据交互协议,用来解决js无法跨域访问的问题。该协议的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据。
一个典型的回发给浏览器的JSON字符串如下所示:
其中代码中的“callbackdata”就是指的浏览器发送的callback参数。
errorCode表明服务器状态,我们在此约定,errorCode为0表示视频数据采集成功。
eclipsTime表示从服务器收到请求到服务器发出数据之间所经历的时间,单位为毫秒。
Image表示该帧的图像数据。其中“base64,”后面跟的是生成的jpg图片经过Base64编码之后的数据。需要注意的是,image中的省略号表示该部分省略了一部分数据。
6.4.2 UI设计
在上一节UI的基础上,我们再添加四个控件:两个Button控件,一个定时器控件,一个静态文本控件。两个按钮分别控制开启网络服务与关闭网络服务,静态文本用来显示当前需要显示的提示信息,而定时器用来设定显示的静态信息过3秒后自动消失。
添加四个控件后,UI如下图所示。
图 6‑7 服务器端UI 设计
6.4.3 程序设计
添加定时器的回调函数
程序在运行过程中需要显示一些状态或提示信息,而静态文本控件中的文字一旦显示之后“永不消失”。
为了让消息只显示一段时间,我们设定了一个定时器,定时时间为3秒。当3秒时间过后,静态文本中的数据自动清空。
定时器的回调函数内容如下:
从上面的代码可以看出,每当3秒钟定时时间到时,静态文本控件内容被清空,定时器被禁用。
而与定时器相对应的是ShowState宏定义。该宏的定义如下:
从上面的代码可以看出,执行ShowState(“Hello,world”)后,“Hello,world”字样将会在静态文本控件中显示出来,3秒钟后,该字样消失。
添加“WebCam!!!”按钮的回调函数
在“WebCam!!!”的回调函数中,我们只需要注册指定端口的TCP服务器即可。
回调函数全部代码如下:
当注册成功时,“WebCam!!!”按钮将变为不可用以防止重复注册,“NoWebCam”按钮将可用,以允许服务器管理员随时关闭WebCam服务。
添加TCP消息的回调函数
上面的代码中,ServerCallback是TCP服务器收到消息之后的回调函数。如同“3.1.2 网络通信:TCP库”中的示例程序一样,几乎所有接收数据的消息都是在回调函数里面处理的。
服务器端TCP消息只有三个:TCP_CONNECT、TCP_DISCONNECT以及TCP_DATAREADY。我们只需要关注TCP_DATAREADY消息。
在TCP_DATAREADY消息处理时,我们首先读取服务器发来的HTTP报头,获取callback参数之后,获取当前帧的数据,并且将当前帧的jpg文件转成Base64编码,加入到JSON字符串中回传给客户端。
我们添加TCP_DATAREADY消息的回调函数代码如下:
添加“NoWebCam”按钮的回调函数
在“NoWebCam”按钮的回调函数中,我们只需要关闭指定端口的TCP服务即可。
回调函数全部代码如下:
当指定端口的服务关闭成功时,“NoWebCam”按钮将变为不可用以防止用户再次关闭该端口的服务,“WebCam!!!”按钮将可用,以允许服务器管理员再次开启WebCam服务。
至此,我们的服务器端的软件已经全部设计完成。我们运行服务器端软件,运行效果如下图所示。
图 6‑8 服务器端软件运行效果
6.5 网页前端设计
由于本次实验所需要的网站不需要访问数据库,所以我们的网站全部采用静态页面。所有的数据请求与数据处理都采用JavaScript来实现。网站前端采用jQuery+Ajax+JSON的架构。
由于网站架设的端口与视频源服务器架设的端口不相同,所以我们在请求数据的时候需要采用可以跨域的getJSON()函数来实现。
getJSON()函数的声明如下:
jQuery.getJSON(url, [data], [callback])
其中,url表示发送请求的地址,data表示发送get请求时所附带的参数,callback表示载入成功时的回调函数。
因此,当我们的HTML文件内容如下时,
在上面的HTML代码中,我们引用了jQuery的函数库文件,并且引用了我们自定义的tr.js文件。并且在HTML文件的body中定义了一个ID为“WebCam”的img图像标签。
下面在我们自定义的js文件“tr.js”中写自定义的函数内容。在下面的js文件中,我们即将通过调用getJSON()函数来获取视频源传回的每一帧的视频信息。
在上面的例子中,我们首先写了一个GetCap()函数。在函数中,我们调用了getJSON函数。需要注意的是,我们在访问视频源服务器时还附带了两个参数。一个是随机数,一个是callback参数。附带随机数是为了防止服务器返回304错误,而callback参数如前所述是使用JSONP的需要。304错误是指当服务器检测到用户的请求参数不变并且服务器端数据也没有变化时服务器返回的错误值,以告诉用户服务器的内容并未发生变化。
当获取数据成功后,程序将执行getJSON()的回调函数,即上述代码第7行。在回调函数中,我们把HTML文件中ID为“WebCam”的图片src设置为刚刚返回的JSON字符串的image变量值,并且再次调用GetCap()函数以继续进行下一帧的访问。因此,只要调用GetCap()函数,程序即开始不断的访问视频源服务器,不断的获取实时监控视频。
$(document).ready()表示“页面载入成功”事件。在ready()函数中添加回调函数,则回调函数中的代码即页面载入成功后立即执行的代码。因此,只要我们在ready的回调函数中调用GetCap()函数后,GetCap()函数即开始执行。
上述代码实际运行效果如下图所示。
图 6‑9 网络摄像机Demo运行效果
上面的代码只是为了完成该功能而写的最简单的Demo程序。为了保证程序的健壮性以及良好的用户体验,我们实际运行的代码做了一番改动。实际的HTML代码如下:
在上面的HTML代码中,我们添加了一个用于输入视频源的文本输入框以及按钮。并且添加了一个写有“Powered By:Asvzeg”的页脚。
实际的js代码支持用户添加自己的视频源,并且允许最多添加10个视频源。实际的js代码如下:
从上面的代码我们可以看出,实际的js代码中,用户可以添加自定义的视频源,并且支持最多添加10个视频源。此外,实际的js代码还支持分辨率检测、支持关闭视频源、支持网络延时服务器延时时间提示、支持超时提示以及加载失败之后的重试等等。
实际的网络摄像机的运行效果如下图所示。
图 6‑10 实际网络摄像机运行效果
6.6 结束语
至此,通过本次实验,大家应该对第一章中我们所说过的一句话“C语言不仅能够做C++、C#、Java等可以做的事,有时还可以做的更快更好,甚至还可以做这些编程语言所不能做的事。”有了更加深刻的理解与体会。使用C语言,一样也可以紧随时代潮流开发一些新鲜的玩意儿。
通过本次综合实验,大家应该从底层了解到了HTTP协议的工作原理。如果认真的理解了本次实验的设计思路与过程,从TCP协议层了解了互联网中通信的基本概念与基本过程之后,大家应该无论对Servlet还是Socket,都应该有恍然大悟的感觉:“一个帮助我们开发TCP协议的工具而已”。
当然了,本次课的综合示例仍然有许多需要完善的地方。如:
Ø 采用AVICAP32开发出来的服务端程序的视频预览窗口不能被其他窗口遮挡,也不能最小化,否则看到的视频永远是遮挡前的最后一帧。
Ø 采用单张图片传输的方式终究不如视频传输的方式压缩率更高,看起来更加流畅。
Ø 当多个浏览器通过访问该视频源服务器时,便有可能出现连接不到服务器的情况。
而只要不懈的去努力改进,以上三个问题也是可以解决的。
6.7 探索与实验
6.7.1 实验
实现本节课中的“远程监控系统”。
6.7.2 探索
尝试解决目前“远程监控系统”存在的三个问题之一。推荐解决问题三。
提示:
第一个问题的出现是因为AVICAP32本身的运行机制的问题。AVICAP32只有在Frame被刷新的时候才会更新图片。而Windows系统的机制导致视频预览窗口被遮挡或被最小化时停止了刷新,也便停止了更新图片。采用DirectShow等其他视频库便不会存在该问题。
第二个问题的出现是因为,视频压缩时不仅将当帧图片压缩,而且一般相邻几帧的图片内容比较相近所以也可以被压缩。若采用视频传输的方式则可以大大的提高视频的连贯性。
第三个问题的出现是因为在处理视频请求的时候CPU一直在等待摄像头的图像数据的返回。因为程序是单线程的,所以等待时,CPU并不会去运行本程序的其他部分的代码。所以,采用多线程的方式即可大大的提高该服务器并发请求的处理能力。
1 条回复
[…] “网络摄像机”分为服务器软件与视频查看网站两部分。服务器端负责采集连接到服务器的摄像头的监控画面,即提供“视频源”。而视频查看网站则负责连接视频源并且将视频呈现给用户。具体的开发过程参见《[原创教程]LabWindows/CVI入门之第六章:综合实例:远程监控系统》。 […]