Windows CE .NET WebCam, Part 2

 

Mike Hall
Microsoft Corporation

Steve Maillet
Entelechy Consulting

September 17, 2002

Summary: Mike Hall and Steve Maillet finish building a WebCam device using Windows CE .NET, extending their work from last month's column to deploy the image to the CEPC reference platform. (16 printed pages)

Welcome to Part 2 of the WebCam project. Last month we looked at building a WebCam using the Microsoft® Windows® CE .NET emulation environment. This showed how to configure a workspace that included the HTTPD Web server component, and how to use the shared Development PC/Emulator _FLATRELEASEDIR folder to copy images captured using the Windows XP Timershot PowerToy into the emulation environments \Release folder. This month we will extend this work to the CEPC platform, specifically adding support for a 1394 WebCam. By the way, before we get started with this month's project, I want to thank Chris Gray from the Windows CE development team for the WebCam project idea and sample code.

Okay, so here's what we will need for this month's project:

Before we get started with the steps needed to build the CEPC/1394/WebCam device, we should initially take a look at the issues/delta's we face between last month's emulation project and this month's 1394 CEPC project. Last month we used the Windows XP Timershot PowerToy to capture an image from a USB connected WebCam. The image was provided to us in JPEG format by the Timershot PowerToy application. We simply needed to drop the image into the www/wwwpub folder and our Web server could then serve up the image (with the aid of some cunning script).

Life becomes slightly more interesting when we start using the 1394 WebCam. This will provide us with an RGB stream, which we will need to convert into a JPEG before dropping it into the www/wwwpub folder. Windows CE .NET doesn't include JPEG helper libraries, so we need to make use of third-party libraries to create our JPEG file. It can then be served up by the Web server, using exactly the same script as last month's article. (The emulator is just great for trying out builds and scripts before working with a reference platform.)

There are a number of libraries available that provide the code needed to generate JPEG images. Here are a couple to choose from: JPEG libraries from the Independent JPEG Group (we're using these in this month's project), CxImage from CodeProject.com, and I'm sure there are plenty of others to choose from.

In order to expose the JPEG image, we will need to create an application that will capture data from the 1394 WebCam, convert this information into JPEG file format, and drop the file into the www/wwwpub folder. This is in fact very similar to last month's project. In last month's project, the application was simply pulling the JPEG file (created by Timershot) from the \Release folder and dropping the image into the www/wwwpub folder—no image conversion/creation was needed.

So let's get started with the basic project configuration. Though quite similar to the configuration used last month, we will need to add some additional components from that catalog to support the 1394 WebCam. I will point these out as we go.

We could create a headless platform; we are simply serving up Web pages after all, and this shouldn't require a local UI for the device. In fact, we have a headless 1394 CEPC WebCam running in the office that boots and runs from a floppy disc. (Now that's a small operating system image!) The WebCam is pointing directly at a foosball table, this makes it possible for anyone to check availability (or scores) at any time of the day or night. For the purposes of this project, however, we will mirror the steps used from last month's emulator platform, and create a device with UI. (Feel free to play around with the platform settings and create a headless device if you'd prefer.)

Here are the steps to create the core platform.

  1. Launch Platform Builder.
  2. From the File menu, click New Platform.
  3. On the New Platform Wizard, click Next.
  4. From the list of available BSPs, select CEPC: X86, and then click Next.
  5. From the list of available platform configurations, click Web Pad.
  6. In the Platform Name box, type WebCam as the name for your platform, and then click Next.
  7. From the list of Web Pad Device variants, select Web Pad, and then click Next.
  8. On the Application & Media page, only select Internet Browser, disable all other options (note that any of the features can be added, using the component catalog); click Next.
  9. On the Networking & Communications page, disable Personal Area Network (PAN) | Bluetooth (note that this can be added at any time, using the component catalog); click Next.
  10. On the Congratulations page, click Done to close the New Platform Wizard.
  11. From the Build menu, click Set Active Configuration.
  12. From the list of platform configurations, select WebCamCEPC: X86 Win32 Release, and then click OK.

At this point, the Windows CE .NET operating system could be built and downloaded to the CEPC platform. We have some additional components to add that will add support for 1394, and the Web Server.

From the Platform Builder Catalog, locate, select, and add the following components (here's where we start adding some components that were not part of the emulation image):

  • Core OS | Display Based Devices | Applications & Services Development | C Libraries and Runtimes | Standard IO ASCII (STDIOA).
  • Device Drivers | IEEE 1394 | Non-AV/C WebCamera Driver.
  • Device Drivers | IEEE 1394 | Test ToolsSample Application.
  • Core OS | Display Based Devices | Communication Services and Networking | Servers | Web Server (HTTPD).

We're all set. All of the core operating system components are now in place. We could build the image, and confirm that the Web server is running okay. From our desktop Internet browser, we would expect to see the default Windows CE HTML page. Now it's time to customize the platform by adding our custom application, and the resource files needed to support the script.

First, let's change the name of our device on the network. We looked at this last month; here's a refresher.

We need to take a look at the parameter tab of our project workspace, locate and open the project.reg file, and add the following entry:

[HKEY_LOCAL_MACHINE\Ident]
   "Name"="GetEmbed"
   "Desc"="WebCam Device"

The default Windows CE HTTPD page (\windows\www\wwwpub\default.htm) simply informs you that the Web server is running (which we may have tested before starting the platform customization). We need to replace this with our custom HTML page. We will also want our page to refresh so that our Internet browser always shows an up-to-date image.

Here's the HTML page. I'm using a one-second timeout on the page, and the image name being displayed is image.jpg. (Note that this is the name of the jpeg file we will output from our custom application.) For the purposes of this article we will call the HTML page "Frame.html".

<SCRIPT LANGUAGE="JavaScript">

var secBeforeRefresh = 1;
var timeRemaining = secBeforeRefresh;

function refreshImage()
{
    //to get the browser to reload the picture we have to somehow
    // trick it into actually reloading the image (no cache) -- we do this by
    // postfixing the date to the end of the URL.  the Web server will
    // disregard this data and send the image (but the browser will be tricked)
    timeStamp = new Date();
    timeStamp = "?"+timeStamp.getTime(); 
    newImage = document.all.webImage.src;

    //if there is a question mark, remove it
    qMark = newImage.indexOf("?", 0);
    if (qMark > 0) 
        newImage = newImage.substr(0, qMark);

    //reload the image
    document.all.webImage.src = newImage+timeStamp;
}

function startReloadTimer() {
    if (!document.layers && !document.all) 
       return;

    //decrement the remaining time by a second
    timeRemaining -= 1;
    
    //if we don't have time anymore, reset timer
    //  and refresh the image
    if (timeRemaining <= 0) {
        timeRemaining = secBeforeRefresh;
        refreshImage();
    }

    //start a 1-second timer (it will call us
    //  when time is up)
    setTimeout("startReloadTimer()", 1000);
}

</SCRIPT>
</HEAD>

<BODY onLoad="startReloadTimer()" >

<CENTER>
<img name="webImage" src="image.jpg">
</CENTER>
<HR>

</BODY>
</HTML>

So the next question is: How do we get Frame.html into our operating-system image? Simple. We follow the instructions from last month's article. First, we copy our Frame.html file to the C:\wince410\public\webcam\WINCE410\CEPC\oak\files folder. All files from this folder are copied to the _FLATRELEASEDIR folder during the build process—that's step 1. The second step is to edit our Project.BIB file. We select the parameter view within Platform Builder, and locate and open the project.bib file.

A binary image builder (.BIB) file defines which modules and files are included in an operating-system image. Makeimg.exe uses .BIB files to determine how to load modules and files into the memory of a target device. BIB files contain two types of resource—modules and files. Typically, the MODULES section contains executables and DLLs, and the FILES section contains non-executable files (bitmaps, wav files, html pages, and so on). I've edited my Project.bib (below) to include Frame.html in the FILES section.

MODULES
;  Name            Path                                 Memory Type
;  --------------  -----------------------------------  -----------

FILES
;  Name            Path                                 Memory Type
;  --------------  -----------------------------------  -----------
   frame.html       $(_FLATRELEASEDIR)\frame.html                NK  

Were we to rebuild the operating system, we would see that the Frame.html file has been included, and lives in the \Windows folder. We need to copy this file to the \Windows\www\wwwpub folder, and rename the file to overwrite the existing default.htm file. Again, this is explained in last month's article. Suffice to say that we need to edit our .DAT file to move the frame.html file from the \Windows folder to the \Windows\www\wwwpub folder. Here's the additions we need to make to the project.dat file.

root:-Directory("\Windows"):-Directory("www")
Directory("\Windows\www"):-Directory("wwwpub")
Directory("\windows\www\wwwpub"):-File("default.htm","\windows\frame.html")

The one final piece of the puzzle is to add the "Camera" application; this will get the raw RGB bits from the 1394 camera and generate an output JPEG file that we will drop into the www/wwwpub folder. There are two steps to this process. The first is to take the source files from the Independent JPEG Group, and create a .LIB file. We will call this libjpeg. The second step is to then create a simple application that captures the 1394 data and converts this to a JPEG file (using the functions exposed from libjpeg.lib).

Step 1. Creating the .LIB file, libjpeg.lib. Platform Builder contains an Application Wizard that provides the ability to create Win32 applications, DLLs, and .LIB files. (Sounds just ideal for the creating the .LIB file and the camera application!) With the WebCam project still open, on the File menu, click New Project or File. We are presented with the list of projects types. Select WCE Static Library, enter the name Libjpeg for the static library, click OK, and then click Finish to go with the default options. We will need to unpack the IJG ZIP/TAR file contents to a suitable folder. I've unpacked the contents to the following folder: C:\WINCE410\PUBLIC\WebCam\jpeg-6b. There are a bunch of header files, 'C' files, makefiles, and documentation. We will be including a number of header files into our project. To add the C:\WINCE410\PUBLIC\WebCam\jpeg-6b folder to our list of include directories, On the Tools menu, click Options, and then click the Directories tab. Note that we could set the path for includes, libraries, executables, and source files. Set the include path to contain the location of the unpacked IJG ZIP/TAR.

We need to add a number of files from the IJG folder to our library. This is extremely simple, and can be used to import files from previous workspaces or applications. Click Project | Insert | Files. Point the open file dialog to the unpacked ZIP/TAR folder and import the following files:

"jdatasrc.c" "jcomapi.c" "jcapistd.c" "jcapimin.c" "jccoefct.c" "jccolor.c" "jcdctmgr.c" "jchuff.c" "jcphuff.c" "jcprepct.c" "jcinit.c" "jcmainct.c" "jcmarker.c" "jcmaster.c" "jcparam.c" "jcsample.c" "jdapimin.c" "jdapistd.c" "jdatadst.c" "jdcoefct.c"

"jdcolor.c" "jddctmgr.c" "jdhuff.c" "jdinput.c" "jdmainct.c" "jdmarker.c" "jdmaster.c" "jdmerge.c" "jdphuff.c" "jdpostct.c" "jdsample.c" "jdtrans.c" "jerror.c" "jfdctflt.c" "jfdctfst.c" "jfdctint.c" "jidctflt.c" "jidctfst.c" "jidctint.c" "jidctred.c" "jmemmgr.c" "jmemnobs.c" "jquant1.c" "jquant2.c" "jutils.c"

The library won't build cleanly unless we define the build variable, NO_GETENV. Again, this is simple. Click Project | Settings . Click the C/C++ tab, and append NO_GETENV to the preprocessor definitions.

We can now build the library by clicking Build | Build libjpeg.lib (or by hitting F7 on the keyboard). This will generate a library file called libjpeg.lib in the folder, C:\WINCE410\PUBLIC\WebCam\libjpeg\CEPC__X86Rel.

The second step is to build the application that uses libjpeg. This is approx 400 lines of code (see below). Let's create the Camera application using the Platform Builder Application Wizard.

On the File menu, click New Project or File and select WCE Application. Give the application the name "Camera", and then select A Simple Windows CE Application. This will create a project workspace, and a skeleton application with the appropriate WinMain entry point. We simply replace the Wizard-generated code with the following code. You will notice the #define for XMD_H at the top of the source listing. If XMD_H isn't defined, then libjpeg will typedef short INT16 and long to INT32, short and long are already defined for us.

// Headers for firewire camera

#include "stdafx.h"

#define XMD_H


extern "C"
{
#include <windows.h>
#include <wdm1394.h>
#include <setupapi.h>
#include <winioctl.h>
#include <1394.h>
#include <1394dcam.h>
#include <pwinbase.h>
}

// Headers for jpg compression
extern "C"
{
#undef NEED_FAR_POINTERS   /* we presume a 32-bit flat memory model */
#undef FAR
#include "jconfig.h"
#include "jinclude.h"
#include "jpeglib.h"
#include "jerror.h"    
#include "cderror.h"
}

typedef struct tagCG_RGBTRIPLE {
    BYTE rgbtBlue;
    BYTE rgbtGreen;
    BYTE rgbtRed;
} CG_RGBTRIPLE;

// run in 640x480x24bpp mode at 15 fps
#define   RENDER_WIDTH   640
#define   RENDER_HEIGHT   480
#define PICTURE_INTERVAL   250
#define elementsof(x) (sizeof(x) / sizeof((x)[0]) )

PVOID  g_pvFrameBuffer    = NULL;
DWORD  g_cdwFrameBuffer    = (RENDER_WIDTH * RENDER_HEIGHT* 3);

BOOL LocateDevicePaths(PWCHAR path[], UINT maxpath, UINT *pathreturned,
CONST LPGUID Guid);
BOOL FindAndOpenTargets(HANDLE *dcam);
VOID FreeDevicePaths(PWCHAR *Paths, UINT num);

DWORD CreateJpg()
{
    DWORD           dwRetVal = STATUS_SUCCESS;
    INT             row_stride;
    unsigned char*  row_pointer[1];
    FILE*           fOut = NULL;
    
    struct jpeg_compress_struct cinfo;
    struct jpeg_error_mgr jerr;
    
    // Create compress struct
    cinfo.err = jpeg_std_error(&jerr);
    jpeg_create_compress(&cinfo);
    
    // Specify output stream
    fOut = fopen("\\windows\\www\\wwwpub\\temp.jpg", "w+b");
    if(fOut == NULL)
        goto exit;
    
    jpeg_stdio_dest(&cinfo, fOut);
    
    // Fill the jpeg compress struct with values for our image
    cinfo.image_width          = 640;        
    cinfo.image_height         = 480;
    cinfo.input_components     = 3;         
    cinfo.in_color_space     = JCS_RGB;    
    
    jpeg_set_defaults(&cinfo);
    
    // Start the compression
    jpeg_start_compress(&cinfo, TRUE);
    
    // Continue compression
    row_stride = cinfo.image_width * cinfo.input_components;    
    while(cinfo.next_scanline < cinfo.image_height) {
        row_pointer[0] = &(((unsigned char *)g_pvFrameBuffer)
[cinfo.next_scanline * row_stride]);
        jpeg_write_scanlines(&cinfo, row_pointer, 1);
    }    
    // Finish the compression
    jpeg_finish_compress(&cinfo);
    
    // Destroy the compressor object
    jpeg_destroy_compress(&cinfo);
    
    fclose(fOut);
    fOut = NULL;
    DeleteFile(L"\\windows\\www\\wwwpub\\image.jpg");  
    MoveFile(L"\\windows\\www\\wwwpub\\temp.jpg",
 L"\\windows\\www\\wwwpub\\image.jpg");
    
exit:
    if(fOut)
        fclose(fOut);
    return dwRetVal;
    
}

BOOL FindAndOpenTargets(HANDLE *dcam)
{
    PWCHAR       DCAMNames[5];
    UINT         returned;
    BOOL         result=FALSE;
    
    if(LocateDevicePaths(DCAMNames, elementsof(DCAMNames), &returned,
(CONST LPGUID)&GUID_DCAM))  {
        if(returned >= 1) {
            *dcam = CreateFile(DCAMNames[0], GENERIC_READ | GENERIC_WRITE,
0,  NULL, OPEN_EXISTING, 0, NULL);
            
            if(*dcam)
                result = TRUE;            
        }
        FreeDevicePaths(DCAMNames, returned);
    }    
    return(result);
}


BOOL LocateDevicePaths(PWCHAR path[], UINT maxpath, 
UINT *pathreturned, CONST LPGUID Guid)
{
    HDEVINFO        hdi;
    BOOL            result=FALSE;
    
    memset(path, 0, sizeof(path[0]) * maxpath);
    
    hdi = SetupDiGetClassDevsEx(Guid, NULL, NULL, 
DIGCF_DEVICEINTERFACE, NULL, NULL, NULL);
    if (hdi != INVALID_HANDLE_VALUE)
    {
        SP_DEVICE_INTERFACE_DATA        did;
        DWORD                           size;
        DWORD                           index=0, outindex=0;
        
        result = TRUE;
        while (outindex < maxpath) {
            if (SetupDiEnumDeviceInterfaces(hdi, NULL, Guid, index, &did)) {
                if (!SetupDiGetDeviceInterfaceDetail(hdi, &did,
 NULL, index, &size, NULL)) {
                    if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
                        PSP_DEVICE_INTERFACE_DETAIL_DATA        didd;
                        
                        didd = (PSP_DEVICE_INTERFACE_DETAIL_DATA) LocalAlloc(0, size);
                        if (didd) {
                            if (SetupDiGetDeviceInterfaceDetail(hdi, 
&did, didd, size, &size, NULL)) {
                                path[outindex] = (PWCHAR)LocalAlloc(0,
 (wcslen(didd->DevicePath)+1) * sizeof(WCHAR));
                                if (path[outindex]) {
                                    wcscpy(path[outindex], 
didd->DevicePath);
                                    outindex++;
                                }
                            }
                            LocalFree(didd);
                        }
                    }
                }
            }
            else
                break;
            
            index++;
        }
        *pathreturned = outindex;
        SetupDiDestroyDeviceInfoList(hdi);
    }
    return(result);
}

VOID FreeDevicePaths(PWCHAR *Paths, UINT num)
{  
    for (UINT x=0; x < num; x++)
        LocalFree(Paths[x]);
}

VOID CloseCamera(HANDLE hDCam)
{
    DCAM_REQUEST    Request;
    DWORD           dwRetVal = 0;

    // stop Isoch Streaming
    Request.Command = DCAM_CMD_STOP_ISOCH;
    DeviceIoControl (hDCam, IOCTL_DCAM_REQUEST, &Request, 
sizeof(Request), &Request, 
sizeof(Request), &dwRetVal, NULL);
}


HANDLE InitCamera()
{
    HANDLE hDCam = 0;
    DWORD           dwRetVal        = 0;
    
    DCAM_REQUEST    Request;
    
    // Get a handle to the digital camera device
    if(FALSE == FindAndOpenTargets(&hDCam)) {
        printf("Cant open a camera -- make sure everything is connected 
and firewire is in your image\n");
        exit(1);
    }
    
    // Set video mode
    Request.Command = DCAM_CMD_SET_VIDEO_MODE;
    Request.u.SetVideoMode.VideoMode = DCAM_640x480xRGBxFPS15;
    DeviceIoControl (hDCam, IOCTL_DCAM_REQUEST, &Request, 
sizeof(Request), &Request, 
sizeof(Request), &dwRetVal, NULL);
    
    // start Isoch Streaming
    Request.Command = DCAM_CMD_START_ISOCH;
    DeviceIoControl (hDCam, IOCTL_DCAM_REQUEST, &Request, 
sizeof(Request), &Request, 
sizeof(Request), &dwRetVal, NULL);
    Sleep(200);

    return hDCam;
}

void TakePicture(HANDLE hDCam)
{
    DWORD           dwRetVal        = 0;
    PDCAM_REQUEST   pBufferRequest    = NULL;
    CG_RGBTRIPLE        *PtrIn;
    WORD                *PtrOut;
  
    pBufferRequest = 
(PDCAM_REQUEST)new char[(sizeof(DCAM_REQUEST) + g_cdwFrameBuffer)];
    if(pBufferRequest == NULL)
        goto exit;
    
    g_pvFrameBuffer = new char[g_cdwFrameBuffer];
    if(g_pvFrameBuffer == NULL)
        goto exit;    
    
    // Fill up buffer digital camera request structure
    pBufferRequest->Command = DCAM_CMD_FILL_BUFFER;
    pBufferRequest->u.FillBuffer.BufferSize = g_cdwFrameBuffer;    
    
    // Call digital camera IOCTL to get frame buffer
    DeviceIoControl (hDCam, IOCTL_DCAM_REQUEST, 
pBufferRequest, 
(sizeof(DCAM_REQUEST) + g_cdwFrameBuffer), 
pBufferRequest, (sizeof(DCAM_REQUEST) + g_cdwFrameBuffer), 
&dwRetVal, NULL);
    
    PtrIn = (CG_RGBTRIPLE *)pBufferRequest->u.FillBuffer.Buffer;
    PtrOut = (WORD *)g_pvFrameBuffer;
    
    // Copy request buffer to FrameBuffer
    memcpy(PtrOut, PtrIn, g_cdwFrameBuffer);
    
    // Copy frame buffer to jpg buffer
    CreateJpg();

exit:    
    if(pBufferRequest)
        delete [] pBufferRequest;
    if(g_pvFrameBuffer)
        delete [] g_pvFrameBuffer;     
}


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, 
LPWSTR lpCmdLine, int nShowCmd )
{
    
    HANDLE hDCam = InitCamera();
    for(;;) {
        Sleep(1000);
        TakePicture(hDCam);
    }
    CloseCamera(hDCam);
    return 0;
}

A couple of final tweaks, and we're ready to build and test our platform. We will be linking the libjpeg.lib file with our camera application. There are two ways we can do this. We could set the library file location using the Tools | Options, directoriesLibrary files option, and point to the C:\WINCE410\PUBLIC\WebCam\libjpeg\CEPC__X86Rel folder. Alternatively, we could simply click Project | Settings | Link, and add the following: C:\WINCE410\PUBLIC\WebCam\libjpeg\CEPC__X86Rel\libjpeg.lib setupapi.lib. Setupapi.lib is required for some of the 1394 functions we're calling in our camera application.

The final step is to set the C/C++ preprocessor NO_GETENV for the camera application. Click Project | Settings and then the C/C++ tab, and enter NO_GETENV into the Preprocessor edit control.

We're done! We can now build and test the platform. We will, of course, need some way to start our Camera application. This could be achieved through Platform Builder (Target | Run Programs), on the platform by browsing to the \Windows folder and double clicking the Camera.exe application, or by modifying the HKLM\Init registry key. Thankfully, we've build an image with a shell, this makes it easy to start the application.

You can see that we only have minor deltas from last month's emulation project, and we now have a real, live 1394 WebCam, running on the CEPC platform. I guess the next step would be to expose an XML Web Service from our platform that exposes the WebCam image to C# and Visual Basic .NET applications running on the desktop (or other devices on the network; it's only SOAP and XML after all)—could be an interesting topic for a future article. If you're interested in seeing this implemented, drop us a comment on the MSDN article page.

Don't forget next month's Windows Embedded Developers Conference. For more information or to register, click here.

 

Get Embedded

Mike Hall is a Product Manager in the Microsoft Embedded and Appliance Platform Group (EAPG). Mike has been working with Windows CE since 1996—in developer support, Embedded System Engineering, and the Embedded product group. When not at the office, Mike can be found with his family, working on Skunk projects, or riding a Honda ST1100.

Steve Maillet is the Founder and Senior Consultant for Entelechy Consulting. Steve has provided training and has developed Windows CE solutions for clients since 1997, when CE was first introduced. Steve is a frequent contributor to the Microsoft Windows CE development newsgroups. When he's not at his computer burning up the keys, Steve can be found jumping out of airplanes at the nearest drop zone.