Monitor Your Web Cam from a Remote Computer : Page 2

We've offered a few solutions for working with web cams within .NET to create fun and intriguing monitoring applications. In this article, we extend those ideas so that web cam images can be shared with multiple clients over the Web.

by Wei-Meng Lee

Jan 31, 2007

Page 2 of 3

Saving the Video as Images
You will now modify the server so that it can act as a video server, accepting connections from clients and sending them images captured by the web cam.

The first step is to recognize that the video captured by the web cam can be saved as individual images. By displaying a series of continuous images on the client, it is similar to watching a video stream. To capture an image, I have defined the following subroutine:

'---save the video data into the Image global variable---
Public Sub CaptureImage()
Dim data As IDataObject
Dim bmap As Image
Dim ms As New IO.MemoryStream()
'---copy the image to the clipboard---
SendMessage(hWnd, WM_CAP_EDIT_COPY, 0, 0)
'---retrieve the image from clipboard and convert it
' to the bitmap format
data = Clipboard.GetDataObject()
If data Is Nothing Then Exit Sub
If data.GetDataPresent(GetType(System.Drawing.Bitmap)) Then
'---convert the data into a Bitmap---
bmap = CType(data.GetData(GetType( _
System.Drawing.Bitmap)), Image)
'---save the Bitmap into a memory stream---
bmap.Save(ms, System.Drawing.Imaging.ImageFormat.Bmp)
'---write the Bitmap from stream into a byte array---
Image = ms.GetBuffer
End If
End Sub

Here, I first copy the image displayed in the PictureBox control to the clipboard. I then convert it to an Image object and save it to a memory stream. Finally, I use the memory stream to write out the image as an array of bytes. The array of bytes is saved into a global variable, Image, which is defined in Module1.vb (right-click on project name in Solution Explorer and select Add | New Item…. Then select Module):

Module Module1
Public Image As Byte()
End Module

Saving the image as a byte array allows me to easily transmit the image over a socket connection.

To ensure that the Image variable is always containing the latest image, add a Timer control to Form1 and set its properties as follows:

EnabledTrue

Interval100

Double-click on the Timer control (located underneath Form1) to reveal its Tick event handler. Code the Tick event handler as follows:

Essentially, you are invoking the CaptureImage() subroutine every 100 milliseconds (10 times per second) so that the Image variable is always containing the latest video image.

Communication between the Clients and Server
The final step is to write the code for communicating with clients over a socket connection. Before I show you the code to do so, you should understand how the client will communicate with the server.

Figure 3. Communication between a client and the server involves a Send message that receives a video capture as a reply.

As shown in Figure 3, upon connecting to the server, the client first sends a "Send" message to the server. When the server receives a "Send" message, it sends back to the client an image captured by the web cam (specifically, the data contained within the Image global variable). The transfer takes place synchronously and the client will only send back another "Send" message when it is ready to accept another image from the server. This technique prevents the server from overwhelming the client, especially if the client is connected to the server over a slow connection.

Now, to enable this communication, add a new class to the project and name it WebCamClient.vb. Import the following namespace:

Imports System.Net.Sockets

Declare the following constant and variables:

'---class to contain information of each client---
Public Class WebCamClient
'--constant for LineFeed character---
Private Const LF As Integer = 10
'---contains a list of all the clients---
Public Shared AllClients As New Hashtable
'---information about the client---
Private _client As TcpClient
Private _clientIP As String
'---used for sending/receiving data---
Private data() As Byte
'---used to store partially received data---
Private partialStr As String

The ReceiveMessage() subroutine reads the data sent from the client. All messages sent from the client will end with a LineFeed (LF) character. Because a single message may be broken up into a few blocks during transmission, it is thus important that you detect for a LF character to ensure that you have received the entire message. Once a message is received and it contains the word "Send," the web cam image is sent to the client using the SendData() subroutine (defined next):

'---receiving a message from the client---
Public Sub ReceiveMessage(ByVal ar As IAsyncResult)
'---read from client---
Dim bytesRead As Integer
Try
SyncLock _client.GetStream
bytesRead = _client.GetStream.EndRead(ar)
End SyncLock
'---client has disconnected---
If bytesRead < 1 Then
AllClients.Remove(_clientIP)
Exit Sub
Else
Dim messageReceived As String
Dim i As Integer = 0
Dim start As Integer = 0
'---loop until no more chars---
While data(i) <> 0
'---do not scan more than what is read---
If i + 1 > bytesRead Then Exit While
'---if LF is detected---
If data(i) = LF Then
messageReceived = partialStr & _
System.Text.Encoding.ASCII.GetString( _
data, start, i - start)
If messageReceived.StartsWith("Send") Then
SendData(Image)
End If
start = i + 1
End If
i += 1
End While
'---partial string---
If start <> i Then
partialStr = _
System.Text.Encoding.ASCII.GetString( _
data, start, i - start)
End If
End If
'---continue reading from client---
SyncLock _client.GetStream
_client.GetStream.BeginRead(data, 0, _
CInt(_client.ReceiveBufferSize), _
AddressOf ReceiveMessage, Nothing)
End SyncLock
Catch ex As Exception
'---remove the client from the HashTable---
AllClients.Remove(_clientIP)
Console.WriteLine(ex.ToString)
End Try
End Sub

The SendData() subroutine sends the data contained in the Image global variable over to the client:

Back in Form1, you can now wire up the rest of the code to make the server functional. Add the following constants and variable:

Public Class Form1
'---port no for listening and sending data---
Const IP_Address As String = "127.0.0.1"
Const portNo As Integer = 500
'---use to spin off a thread to listen for incoming connections---
Dim t As System.Threading.Thread

Define the Listen() subroutine to listen for incoming socket connections: