1109 lines
25 KiB
C
1109 lines
25 KiB
C
/*
|
|
* Copyright (c) Microsoft Corporation, 1993. All Rights Reserved.
|
|
*/
|
|
|
|
|
|
/*
|
|
* vcuser.c
|
|
*
|
|
* 32-bit Video Capture driver
|
|
* User-mode support library
|
|
*
|
|
* functions providing access to video capture hardware, by accessing
|
|
* the kernel-mode device driver.
|
|
*
|
|
* Geraint Davies, Feb 93
|
|
*/
|
|
|
|
|
|
#include <windows.h>
|
|
#include <mmsystem.h>
|
|
#include <msvideo.h>
|
|
#include <mmddk.h>
|
|
#include <devioctl.h>
|
|
#include <vcstruct.h>
|
|
#include <ntddvidc.h>
|
|
#include <vcuser.h>
|
|
|
|
#include "vcupriv.h"
|
|
|
|
|
|
|
|
/*
|
|
* open the device and return a capture device handle that can be used
|
|
* in future calls.
|
|
* The device index is 0 for the first capture device up to N for the
|
|
* Nth installed capture device.
|
|
*
|
|
* If pDriverName is non-null, then we will open the Nth device handled
|
|
* by this driver. Current implementation supports only one device per
|
|
* drivername.
|
|
*
|
|
* This function returns NULL if it is not able to open the device.
|
|
*/
|
|
VCUSER_HANDLE
|
|
VC_OpenDevice(PWCHAR pDriverName, int DeviceIndex)
|
|
{
|
|
VCUSER_HANDLE vh;
|
|
WCHAR DeviceName[MAX_VIDCAP_NAME_LENGTH];
|
|
WCHAR CompleteName[MAX_VIDCAP_NAME_LENGTH];
|
|
PVC_PROFILE_INFO pProfile;
|
|
|
|
|
|
|
|
if (pDriverName == NULL) {
|
|
|
|
/*
|
|
* no driver name given, so we should just open the device
|
|
* numbered DeviceIndex
|
|
*/
|
|
wsprintf(DeviceName, TEXT("%s%d"),
|
|
DD_VIDCAP_DEVICE_NAME_U, DeviceIndex);
|
|
|
|
|
|
} else {
|
|
|
|
/*
|
|
* open the Nth device created by driver pDriverName.
|
|
*
|
|
* - currently we have no way of locating more than one device
|
|
* per driver (to be fixed with the multimedia device map)
|
|
*/
|
|
if (DeviceIndex != 0) {
|
|
return (NULL);
|
|
}
|
|
|
|
pProfile = VC_OpenProfileAccess(pDriverName);
|
|
DeviceName[0] = TEXT('\0');
|
|
if (!VC_ReadProfileString(pProfile, REG_DEVNAME, DeviceName, sizeof(DeviceName))) {
|
|
|
|
return(NULL);
|
|
}
|
|
if (DeviceName[0] == TEXT('\0')) {
|
|
return(NULL);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* build the complete name: we need to prepend \\.\ to the basename
|
|
* to tell Win32 that this is a device name we are opening
|
|
*/
|
|
wsprintf(CompleteName, TEXT("\\\\.\\%s"), DeviceName);
|
|
|
|
|
|
|
|
/* allocate our tracking structure (the handle returned is a
|
|
* pointer to this
|
|
*/
|
|
vh = (VCUSER_HANDLE) GlobalLock(GlobalAlloc(GPTR, sizeof(VCUSER_STRUCT)));
|
|
if (!vh) {
|
|
return(NULL);
|
|
}
|
|
|
|
|
|
/*
|
|
* open the kernel driver - we need the overlapped flag
|
|
* so that we can file async requests in the worker thread.
|
|
* all requests for which the OVERLAPPED field is NULL will
|
|
* still be synchronous though
|
|
*/
|
|
vh->hDriver = CreateFile(
|
|
CompleteName,
|
|
GENERIC_READ,
|
|
0,
|
|
NULL,
|
|
OPEN_EXISTING,
|
|
FILE_FLAG_OVERLAPPED,
|
|
NULL);
|
|
|
|
if (vh->hDriver == INVALID_HANDLE_VALUE) {
|
|
dprintf1(("failed to open device %ls, error %d", CompleteName, GetLastError()));
|
|
|
|
GlobalUnlock(GlobalHandle(vh));
|
|
GlobalFree(GlobalHandle(vh));
|
|
return(NULL);
|
|
}
|
|
|
|
/* initialise the rest of the structure */
|
|
vh->hThread = NULL;
|
|
vh->FunctionCode = InvalidFunction;
|
|
vh->hWorkEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
|
|
vh->hCompleteEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
|
|
|
|
if ((vh->hWorkEvent == NULL) || (vh->hCompleteEvent == NULL)) {
|
|
VC_CloseDevice(vh);
|
|
return NULL;
|
|
}
|
|
|
|
|
|
return(vh);
|
|
}
|
|
|
|
|
|
/*
|
|
* close a capture device. This will abort any operation in progress and
|
|
* render the device handle invalid.
|
|
*/
|
|
VOID
|
|
VC_CloseDevice(VCUSER_HANDLE vh)
|
|
{
|
|
if (vh == NULL) {
|
|
return;
|
|
}
|
|
|
|
/* make sure we don't leave a worker thread hanging */
|
|
if(vh->hThread != NULL) {
|
|
VC_StreamFini(vh);
|
|
}
|
|
|
|
if (vh->hDriver != NULL) {
|
|
CloseHandle(vh->hDriver);
|
|
}
|
|
|
|
if (vh->hWorkEvent != NULL) {
|
|
CloseHandle(vh->hWorkEvent);
|
|
}
|
|
|
|
if (vh->hCompleteEvent != NULL) {
|
|
CloseHandle(vh->hCompleteEvent);
|
|
}
|
|
|
|
GlobalUnlock(GlobalHandle(vh));
|
|
GlobalFree(GlobalHandle(vh));
|
|
}
|
|
|
|
|
|
/*
|
|
* Configuration.
|
|
*
|
|
* These functions perform device-dependent setup affecting the
|
|
* target format, the source acquisition or the display (overlay).
|
|
*
|
|
* The structures passed are not interpreted by the vcuser and vckernel
|
|
* libraries except that the first ulong of the struct must contain the
|
|
* size in bytes of the entire structure (see vcstruct.h). It is assumed
|
|
* that the structures are defined and agreed between the user-mode
|
|
* hardware-specific code and the kernel-mode hardware specific code
|
|
*/
|
|
BOOL
|
|
VC_ConfigFormat(VCUSER_HANDLE vh, PCONFIG_INFO pGeneric)
|
|
{
|
|
DWORD dwCount;
|
|
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_CONFIG_FORMAT,
|
|
pGeneric,
|
|
pGeneric->ulSize,
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL) ) {
|
|
return(FALSE);
|
|
} else {
|
|
return(TRUE);
|
|
}
|
|
}
|
|
|
|
|
|
BOOL
|
|
VC_ConfigSource(VCUSER_HANDLE vh, PCONFIG_INFO pGeneric)
|
|
{
|
|
DWORD dwCount;
|
|
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_CONFIG_SOURCE,
|
|
pGeneric,
|
|
pGeneric->ulSize,
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL) ) {
|
|
return(FALSE);
|
|
} else {
|
|
return(TRUE);
|
|
}
|
|
}
|
|
|
|
|
|
BOOL
|
|
VC_ConfigDisplay(VCUSER_HANDLE vh, PCONFIG_INFO pGeneric)
|
|
{
|
|
DWORD dwCount;
|
|
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_CONFIG_DISPLAY,
|
|
pGeneric,
|
|
pGeneric->ulSize,
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL) ) {
|
|
return(FALSE);
|
|
} else {
|
|
return(TRUE);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* overlay and keying
|
|
*
|
|
* Several different methods are used by devices to locate the overlay
|
|
* area on the screen: colour (either rgb or palette index) and/or
|
|
* either a single rectangle, or a series of rectangles defining a complex
|
|
* region. Call GetOverlayMode first to find out which type of overlay
|
|
* keying is available. If this returns 0, this hardware is not capable
|
|
* of overlay.
|
|
*/
|
|
|
|
/*
|
|
* find out the overlay keying method
|
|
*/
|
|
ULONG
|
|
VC_GetOverlayMode(VCUSER_HANDLE vh)
|
|
{
|
|
OVERLAY_MODE mode;
|
|
DWORD dwCount;
|
|
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_OVERLAY_MODE,
|
|
NULL,
|
|
0,
|
|
&mode,
|
|
sizeof(OVERLAY_MODE),
|
|
&dwCount,
|
|
NULL) ) {
|
|
return(0);
|
|
} else {
|
|
return(mode.ulMode);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* set the key colour to a specified RGB colour. This function will only
|
|
* succeed if GetOverlayMode returned VCO_KEYCOLOUR and VCO_KEYCOLOUR_RGB
|
|
* and not VCO_KEYCOLOUR_FIXED
|
|
*/
|
|
BOOL
|
|
VC_SetKeyColourRGB(VCUSER_HANDLE vh, PRGBQUAD pRGB)
|
|
{
|
|
DWORD dwCount;
|
|
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_SET_KEY_RGB,
|
|
pRGB,
|
|
sizeof(RGBQUAD),
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL) ) {
|
|
return(FALSE);
|
|
} else {
|
|
return(TRUE);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* set the key colour to a specified palette index. This function will only
|
|
* succeed if GetOverlayMode returned VCO_KEYCOLOUR and not either
|
|
* VCO_KEYCOLOUR_RGB or VCO_KEYCOLOUR_FIXED
|
|
*/
|
|
BOOL
|
|
VC_SetKeyColourPalIdx(VCUSER_HANDLE vh, WORD PalIndex)
|
|
{
|
|
DWORD dwCount;
|
|
ULONG ulPalIdx = (ULONG) PalIndex;
|
|
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_SET_KEY_PALIDX,
|
|
&ulPalIdx,
|
|
sizeof(ULONG),
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL) ) {
|
|
return(FALSE);
|
|
} else {
|
|
return(TRUE);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* get the current key colour. This 32-bit value should be interpreted
|
|
* as either a palette index or an RGB value according to the
|
|
* VCO_KEYCOLOUR_RGB flag returned from VC_GetOverlayMode.
|
|
*/
|
|
DWORD
|
|
VC_GetKeyColour(VCUSER_HANDLE vh)
|
|
{
|
|
DWORD dwCount;
|
|
DWORD dwColour;
|
|
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_GET_KEY_COLOUR,
|
|
NULL,
|
|
0,
|
|
(PULONG) &dwColour,
|
|
sizeof(ULONG),
|
|
&dwCount,
|
|
NULL) ) {
|
|
return(0);
|
|
} else {
|
|
return(dwColour);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* set the overlay rectangle(s). This rectangle marks the area in device
|
|
* co-ordinates where the overlay video will appear. The video will be
|
|
* panned so that pixel (0,0) will appear at the top-left of this rectangle,
|
|
* and the video will be cropped at the bottom and right. The video
|
|
* stream will not normally be scaled to fit this window: scaling is normally
|
|
* determined by the destination format set by VC_ConfigFormat.
|
|
*
|
|
* If VCO_KEYCOLOUR was returned, the video
|
|
* will only be shown at those pixels within the rectangle for which the
|
|
* vga display has the key colour (VC_GetKeyColour() for this).
|
|
*
|
|
* Some devices may support complex regions (VCO_COMPLEX_RECT). In that case,
|
|
* the first rectangle in the area must be the bounding rectangle for
|
|
* the overlay area, followed by one rectangle for each region within it in
|
|
* which the overlay should appear.
|
|
*/
|
|
BOOL
|
|
VC_SetOverlayRect(VCUSER_HANDLE vh, POVERLAY_RECTS pOR)
|
|
{
|
|
DWORD dwCount;
|
|
|
|
/*
|
|
* the structure OVERLAY_RECTS can include one or more rectangles.
|
|
* There must be at least one, and the structure as defined includes
|
|
* one rectangle
|
|
*/
|
|
if (pOR->ulCount < 1) {
|
|
return(FALSE);
|
|
}
|
|
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_OVERLAY_RECTS,
|
|
pOR,
|
|
sizeof(OVERLAY_RECTS) + ((pOR->ulCount - 1) * sizeof(RECT)),
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL) ) {
|
|
return(FALSE);
|
|
} else {
|
|
return(TRUE);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* set the offset of the overlay. This changes the panning - ie which
|
|
* source co-ordinate appears as the top left pixel in the overlay rectangle.
|
|
* Initially after a call to VC_SetOverlayRect, the source image will be panned
|
|
* so that the top-left of the source image is aligned with the top-left of the
|
|
* overlay rectangle. This call aligns the top-left of the source image
|
|
* with the top-left of this offset rectangle.
|
|
*/
|
|
BOOL
|
|
VC_SetOverlayOffset(VCUSER_HANDLE vh, PRECT prc)
|
|
{
|
|
DWORD dwCount;
|
|
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_OVERLAY_OFFSET,
|
|
prc,
|
|
sizeof(RECT),
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL) ) {
|
|
return(FALSE);
|
|
} else {
|
|
return(TRUE);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* enable or disable overlay. if the BOOL bOverlay is TRUE, and the overlay
|
|
* key colour and rectangle have been set, overlay will be enabled.
|
|
*/
|
|
BOOL
|
|
VC_Overlay(VCUSER_HANDLE vh, BOOL bOverlay)
|
|
{
|
|
DWORD dwCount;
|
|
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
bOverlay ? IOCTL_VIDC_OVERLAY_ON : IOCTL_VIDC_OVERLAY_OFF,
|
|
NULL,
|
|
0,
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL) ) {
|
|
return(FALSE);
|
|
} else {
|
|
return(TRUE);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* enable or disable acquisition.
|
|
* If acquisition is disabled, the overlay image will be frozen.
|
|
*
|
|
* this function will have no effect during capture since the acquisition
|
|
* flag is toggled at each frame capture.
|
|
*/
|
|
BOOL
|
|
VC_Capture(VCUSER_HANDLE vh, BOOL bAcquire)
|
|
{
|
|
DWORD dwCount;
|
|
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
bAcquire ? IOCTL_VIDC_CAPTURE_ON : IOCTL_VIDC_CAPTURE_OFF,
|
|
NULL,
|
|
0,
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL) ) {
|
|
return(FALSE);
|
|
} else {
|
|
return(TRUE);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* write a frame to the frame buffer for playback
|
|
*/
|
|
BOOL
|
|
VC_DrawFrame(VCUSER_HANDLE vh, PDRAWBUFFER pDraw)
|
|
{
|
|
DWORD dwCount;
|
|
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_DRAW_FRAME,
|
|
pDraw,
|
|
sizeof(DRAWBUFFER),
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL) ) {
|
|
return(FALSE);
|
|
} else {
|
|
return(TRUE);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* capture a single frame, synchronously. the video header must point
|
|
* to a data buffer large enough to hold one frame of the format set by
|
|
* VC_ConfigFormat.
|
|
*
|
|
* Rather than just sit and poll waiting for the vertical syncs to occur,
|
|
* we convert this into a series of calls to the interrupt-driven
|
|
* stream capture, but we only do one add buffer: we init and start
|
|
* capture, queue our one and only buffer - synchronously, and wait
|
|
* for it to be completed. Then we stop and fini the stream and all is done.
|
|
*
|
|
* This function can be called after the app has called VC_Init, but before
|
|
* VC_Start. In that case, we do not issue init/fini requests, but we do the
|
|
* Start/add/stop. The worker thread ensures that if no start has been issued,
|
|
* no buffers are queued in the kernel, so it is safe for us to
|
|
* issue start.
|
|
*
|
|
* If the add-buffer fails, then we request capture to a system buffer and
|
|
* copy from there, just as the worker thread does.
|
|
*/
|
|
BOOL
|
|
VC_Frame(VCUSER_HANDLE vh, LPVIDEOHDR lpVideoHdr)
|
|
{
|
|
ULONG ulArg;
|
|
BOOL bOK;
|
|
DWORD dwCount;
|
|
/*
|
|
* kernel drivers do not understand LPVIDEOHDR: but they do understand
|
|
* PCAPTUREBUFFER, which has the same fields.
|
|
*/
|
|
PCAPTUREBUFFER pCapture;
|
|
|
|
|
|
|
|
|
|
dprintf3(("frame"));
|
|
|
|
/*
|
|
* initialise for streaming. We need to set a frame rate, but it does
|
|
* not really matter what at this point. 30 fps means we should get the
|
|
* next frame going.
|
|
*
|
|
* Don't do this if the worker thread has already been initialised.
|
|
*/
|
|
if (!vh->hThread) {
|
|
ulArg = 30; // 30 fps
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_STREAM_INIT,
|
|
&ulArg,
|
|
sizeof(ULONG),
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL) ) {
|
|
dprintf(("frame init failed"));
|
|
return(FALSE);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* start streaming. We need to do this before issuing the add-buffer,
|
|
* so that we can do the add-buffer synchronously and just wait for
|
|
* it to finish. It doesn't matter if the frame completes and the driver
|
|
* needs a buffer before we get round to adding the buffer - we will
|
|
* just get the frame after that.
|
|
*/
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_STREAM_START,
|
|
NULL,
|
|
0,
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL)) {
|
|
dprintf1(("frame start failed"));
|
|
/* start failed. end streaming and exit */
|
|
DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_STREAM_FINI,
|
|
NULL,
|
|
0,
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL);
|
|
return(FALSE);
|
|
}
|
|
|
|
/*
|
|
* add a buffer. this request will complete only when the
|
|
* buffer has been filled with data (or an error has occured
|
|
*/
|
|
dprintf3(("adding buffer"));
|
|
pCapture = (PCAPTUREBUFFER) lpVideoHdr;
|
|
bOK = DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_ADD_BUFFER,
|
|
pCapture,
|
|
sizeof(CAPTUREBUFFER),
|
|
pCapture,
|
|
sizeof(CAPTUREBUFFER),
|
|
&dwCount,
|
|
NULL);
|
|
|
|
if (!bOK) {
|
|
/* this may have failed because of buffer size - try using the
|
|
* system buffer and then a partial copy
|
|
*/
|
|
if (DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_CAP_TO_SYSBUF,
|
|
NULL,
|
|
0,
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL) ) {
|
|
|
|
pCapture->dwWindowOffset = 0;
|
|
pCapture->dwWindowLength = pCapture->BufferLength;
|
|
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_PARTIAL_CAPTURE,
|
|
pCapture,
|
|
sizeof(CAPTUREBUFFER),
|
|
pCapture,
|
|
sizeof(CAPTUREBUFFER),
|
|
&dwCount,
|
|
NULL) ) {
|
|
dprintf(("partial capture failed"));
|
|
} else {
|
|
bOK = TRUE;
|
|
}
|
|
|
|
DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_FREE_SYSBUF,
|
|
NULL,
|
|
0,
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL);
|
|
}
|
|
}
|
|
|
|
dprintf3(("buffer completed"));
|
|
|
|
/* stop and fini the streaming */
|
|
DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_STREAM_STOP,
|
|
NULL,
|
|
0,
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL);
|
|
|
|
/*
|
|
* only issue the fini if there is no worker thread - if there is,
|
|
* it will have issued the init for us, and so we should not fini.
|
|
*/
|
|
if (!vh->hThread) {
|
|
|
|
DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_STREAM_FINI,
|
|
NULL,
|
|
0,
|
|
NULL,
|
|
0,
|
|
&dwCount,
|
|
NULL);
|
|
}
|
|
|
|
if (bOK) {
|
|
/* set the header done flag */
|
|
lpVideoHdr->dwFlags |= VHDR_DONE;
|
|
}
|
|
return(bOK);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
|
* -- data streaming ----------------------------------------------------
|
|
*
|
|
* Call VC_StreamInit to prepare for streaming.
|
|
* Call VC_StreamStart to initiate capture.
|
|
* Call VC_AddBuffer to add a capture buffer to the list. As each
|
|
* frame capture completes, the callback function specified in
|
|
* VC_StreamInit will be called with the buffer that has completed.
|
|
*
|
|
* If there is no buffer ready when it is time to capture a frame,
|
|
* a callback will occur. In addition, VC_StreamGetError will return
|
|
* a count of the frames missed this session. VC_StreamGetPos will return
|
|
* the position (in millisecs) reached so far.
|
|
*
|
|
* Call VC_StreamStop to terminate streaming. Any buffer currently in
|
|
* progress may still complete. Uncompleted buffers will remain in the
|
|
* queue. Call VC_Reset to release all buffers from the queue.
|
|
*
|
|
* Finally call VC_StreamFini to tidy up.
|
|
*/
|
|
|
|
/*
|
|
* Worker Thread Structure
|
|
* -----------------------
|
|
*
|
|
* Caller can specify a callback in the Stream Init function. We call
|
|
* this function on completion of a buffer, and on overrun (frame-skip).
|
|
* The only way we can do callbacks is to have a background thread waiting
|
|
* for completion of an i/o operation, which will call the callback function
|
|
* when the i/o completes.
|
|
*
|
|
* We create a worker thread on the stream-init call. This will queue up
|
|
* a wait-error, and wait. The wait-error request will complete whenever the
|
|
* kernel driver hits an overrun (frame-capture time with no buffer ready).
|
|
*
|
|
* each add-buffer request will be passed to the worker thread, which will
|
|
* send up to two of them to the driver (with overlapped i/o requests).
|
|
* The remainder will be queued by the worker thread - this is because
|
|
* add-buffer requests in kernel mode are queued with all memory locked down.
|
|
*
|
|
* The stream-fini call will cause the worker thread to clean up and terminate.
|
|
*
|
|
* The worker thread will wait for completion of any of 4 events, signifying:
|
|
* - completion of the outstanding wait-error request
|
|
* - completion of one of the 2 add-buffer requests
|
|
* - request (eg add-buffer, stream-fini) from the user's thread
|
|
*
|
|
* Synchronization between threads is done with two events, one to signal
|
|
* a request, and one to signal that it is complete. We assume that there
|
|
* will be only one requesting thread for each worker thread, and we have
|
|
* no shared data that requires a critical section.
|
|
*/
|
|
|
|
|
|
/*
|
|
* prepare to start capturing frames -
|
|
* create the worker thread and wait for it to signal completion of its
|
|
* setup.
|
|
*/
|
|
BOOL VC_StreamInit(
|
|
VCUSER_HANDLE vh,
|
|
PVCCALLBACK pCallback, // pointer to callback function
|
|
ULONG ulFrameRate // desired capture rate: microseconds per frame
|
|
)
|
|
{
|
|
/* no worker thread present right now */
|
|
ASSERT(vh->hThread == NULL);
|
|
|
|
/* store callback for use by worker thread */
|
|
vh->pCallback = pCallback;
|
|
|
|
/* frame rate is the argument for the init function */
|
|
vh->FunctionArg = (DWORD) ulFrameRate;
|
|
|
|
/* create the thread */
|
|
vh->hThread = CreateThread(
|
|
NULL,
|
|
0,
|
|
(LPTHREAD_START_ROUTINE) VC_ThreadInit,
|
|
vh,
|
|
0,
|
|
&vh->ThreadId);
|
|
|
|
if (vh->hThread == NULL) {
|
|
dprintf(("thread creation failed"));
|
|
return(FALSE);
|
|
}
|
|
|
|
dprintf2(("waiting for completion event"));
|
|
|
|
/* wait for completion of the init operation */
|
|
WaitForSingleObject(vh->hCompleteEvent, INFINITE);
|
|
|
|
dprintf2(("completion signalled"));
|
|
|
|
/* check that the init succeeded */
|
|
if ( (BOOL) vh->FunctionResult == TRUE) {
|
|
return(TRUE);
|
|
}
|
|
|
|
/* init failed - wait for the thread to exit and clean up */
|
|
dprintf(("init failed"));
|
|
WaitForSingleObject(vh->hThread, INFINITE);
|
|
|
|
dprintf2(("worker exited"));
|
|
|
|
CloseHandle(vh->hThread);
|
|
vh->hThread = NULL;
|
|
|
|
return(FALSE);
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
* clean up after capturing. You must have stopped capturing first.
|
|
*/
|
|
BOOL
|
|
VC_StreamFini(VCUSER_HANDLE vh)
|
|
{
|
|
|
|
/* there must be a worker thread to talk to */
|
|
if (vh->hThread == NULL) {
|
|
return(FALSE);
|
|
}
|
|
|
|
/* can't call stream-fini from a callback. not nice */
|
|
if (GetCurrentThreadId() == vh->ThreadId) {
|
|
return(FALSE);
|
|
}
|
|
|
|
|
|
/* there must be no currently-running operation */
|
|
ASSERT(vh->FunctionCode == InvalidFunction);
|
|
|
|
/* set the function code and alert the thread */
|
|
vh->FunctionCode = StreamFini;
|
|
SetEvent(vh->hWorkEvent);
|
|
|
|
/* on this function, we don't wait for the completion event -
|
|
* the thread just cleans up and exits. We wait for the thread to
|
|
* terminate
|
|
*/
|
|
WaitForSingleObject(vh->hThread, INFINITE);
|
|
|
|
CloseHandle(vh->hThread);
|
|
vh->hThread = NULL;
|
|
|
|
return(TRUE);
|
|
}
|
|
|
|
|
|
/*
|
|
* initiate capturing of frames. Must have called VC_StreamInit first.
|
|
*
|
|
* We need the worker thread in place to do streaming properly, but this
|
|
* function does not affect it. We would gain nothing by passing this
|
|
* call via the worker thread.
|
|
*/
|
|
BOOL
|
|
VC_StreamStart(VCUSER_HANDLE vh)
|
|
{
|
|
BOOL bOK;
|
|
|
|
/* check we have a worker thread- ie we have correctly done
|
|
* a stream-init.
|
|
*/
|
|
if (vh->hThread == NULL) {
|
|
return(FALSE);
|
|
}
|
|
|
|
/* check whether we are already on the worker thread */
|
|
if (GetCurrentThreadId() == vh->ThreadId) {
|
|
/*
|
|
* we are on the worker thread already - so call directly
|
|
*/
|
|
VC_ProcessFunction(vh, StreamStart, 0, (LPDWORD) &bOK);
|
|
} else {
|
|
/* if another operation is in progress we are in a mess */
|
|
ASSERT(vh->FunctionCode == InvalidFunction);
|
|
|
|
/* set function code and alert thread */
|
|
vh->FunctionCode = StreamStart;
|
|
SetEvent(vh->hWorkEvent);
|
|
|
|
/* wait for completion of the operation */
|
|
WaitForSingleObject(vh->hCompleteEvent, INFINITE);
|
|
|
|
bOK = (BOOL) vh->FunctionResult;
|
|
}
|
|
|
|
/* return the result */
|
|
return(bOK);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* stop capturing frames. Current frame may still complete. All other buffers
|
|
* will remain in the queue until capture is re-started, or they are released
|
|
* by VC_StreamReset.
|
|
*
|
|
* again we do not need to go via the worker thread for this.
|
|
*/
|
|
BOOL
|
|
VC_StreamStop(VCUSER_HANDLE vh)
|
|
{
|
|
BOOL bOK;
|
|
|
|
/*
|
|
* check that there is a worker thread in place - otherwise
|
|
* functions are being called out of sequence
|
|
*/
|
|
if (vh->hThread == NULL) {
|
|
return(FALSE);
|
|
}
|
|
|
|
/* check whether we are already on the worker thread */
|
|
if (GetCurrentThreadId() == vh->ThreadId) {
|
|
/*
|
|
* we are on the worker thread already - so call directly
|
|
*/
|
|
VC_ProcessFunction(vh, StreamStop, 0, (LPDWORD) &bOK);
|
|
} else {
|
|
/* if another operation is in progress we are in a mess */
|
|
ASSERT(vh->FunctionCode == InvalidFunction);
|
|
|
|
/* set function code and alert thread */
|
|
vh->FunctionCode = StreamStop;
|
|
SetEvent(vh->hWorkEvent);
|
|
|
|
/* wait for completion of the operation */
|
|
WaitForSingleObject(vh->hCompleteEvent, INFINITE);
|
|
|
|
bOK = (BOOL) vh->FunctionResult;
|
|
}
|
|
|
|
/* return the result */
|
|
return(bOK);
|
|
|
|
}
|
|
|
|
|
|
/*
|
|
* cancel all buffers that have been 'add-buffered' but have not
|
|
* completed. This will also force VC_StreamStop if it hasn't already been
|
|
* called.
|
|
*
|
|
* This request needs to go to the worker thread, since all but a few (2)
|
|
* of the buffers will be queued up in user-space by the worker thread.
|
|
*/
|
|
BOOL
|
|
VC_StreamReset(VCUSER_HANDLE vh)
|
|
{
|
|
BOOL bOK;
|
|
|
|
/* make sure that there really is a worker thread */
|
|
if (vh->hThread == NULL) {
|
|
return(FALSE);
|
|
}
|
|
|
|
/* call the function directly if we are in a worker thread
|
|
* (eg in a callback func)
|
|
*/
|
|
if (GetCurrentThreadId() == vh->ThreadId) {
|
|
|
|
VC_ProcessFunction(vh, StreamReset, 0, (LPDWORD) &bOK);
|
|
} else {
|
|
|
|
/* if another operation is in progress we are in a mess */
|
|
ASSERT(vh->FunctionCode == InvalidFunction);
|
|
|
|
|
|
/* set function code and alert thread */
|
|
vh->FunctionCode = StreamReset;
|
|
SetEvent(vh->hWorkEvent);
|
|
|
|
/* wait for completion of the operation */
|
|
WaitForSingleObject(vh->hCompleteEvent, INFINITE);
|
|
|
|
bOK = (BOOL) vh->FunctionResult;
|
|
}
|
|
|
|
/* return the result */
|
|
return(bOK);
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* get the count of frames that have been skipped since the last call
|
|
* to VC_StreamInit.
|
|
*
|
|
* The worker thread keeps count of the number of skips. It is easier
|
|
* to synchronize access to the count by treating this as a function,
|
|
* though it might be more efficient just to grab the count
|
|
* ourselves from shared data.
|
|
*/
|
|
ULONG
|
|
VC_GetStreamError(VCUSER_HANDLE vh)
|
|
{
|
|
ULONG ulCount;
|
|
|
|
/* check that there is a worker thread */
|
|
if (vh->hThread == NULL) {
|
|
return(FALSE);
|
|
}
|
|
|
|
|
|
if (GetCurrentThreadId() == vh->ThreadId) {
|
|
/* we are on the worker thread already (eg from callback).
|
|
* we must call directly or we will never see the event!
|
|
*/
|
|
VC_ProcessFunction(vh, GetError, 0, (LPDWORD) &ulCount);
|
|
|
|
} else {
|
|
/* if another operation is in progress we are in a mess */
|
|
ASSERT(vh->FunctionCode == InvalidFunction);
|
|
|
|
/* set function code and alert thread */
|
|
vh->FunctionCode = GetError;
|
|
SetEvent(vh->hWorkEvent);
|
|
|
|
/* wait for completion of the operation */
|
|
WaitForSingleObject(vh->hCompleteEvent, INFINITE);
|
|
|
|
/* return the result */
|
|
ulCount = (ULONG) vh->FunctionResult;
|
|
}
|
|
/* return the result */
|
|
return(ulCount);
|
|
}
|
|
|
|
|
|
/*
|
|
* add a buffer to the queue. The buffer should be large enough
|
|
* to hold one frame of the format specified by VC_ConfigFormat.
|
|
*
|
|
* this function goes to the worker thread, who will queue buffers in user
|
|
* space and pass a limited number on to the kernel driver
|
|
*/
|
|
BOOL
|
|
VC_StreamAddBuffer(VCUSER_HANDLE vh, LPVIDEOHDR lpVideoHdr)
|
|
{
|
|
BOOL bOK;
|
|
|
|
/* check that there is a worker thread */
|
|
if (vh->hThread == NULL) {
|
|
return(FALSE);
|
|
}
|
|
|
|
/* check whether we are already on the worker thread */
|
|
if (GetCurrentThreadId() == vh->ThreadId) {
|
|
/*
|
|
* we are on the worker thread already - so call directly
|
|
*/
|
|
VC_ProcessFunction(vh, AddBuffer, (DWORD) lpVideoHdr, (LPDWORD) &bOK);
|
|
} else {
|
|
/* if another operation is in progress we are in a mess */
|
|
ASSERT(vh->FunctionCode == InvalidFunction);
|
|
|
|
/* set function code and alert thread */
|
|
vh->FunctionCode = AddBuffer;
|
|
vh->FunctionArg = (DWORD) lpVideoHdr;
|
|
SetEvent(vh->hWorkEvent);
|
|
|
|
/* wait for completion of the operation */
|
|
WaitForSingleObject(vh->hCompleteEvent, INFINITE);
|
|
|
|
bOK = (BOOL) vh->FunctionResult;
|
|
}
|
|
|
|
/* return the result */
|
|
return(bOK);
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* get the current position within the capture stream (ie time
|
|
* in millisecs since capture began).
|
|
*
|
|
* This has no interaction with the worker thread and can go straight to
|
|
* the device.
|
|
*/
|
|
BOOL
|
|
VC_GetStreamPos(VCUSER_HANDLE vh, LPMMTIME pTime)
|
|
{
|
|
ULONG ulPosition;
|
|
DWORD dwCount;
|
|
|
|
/* check we have a worker thread- ie we have correctly done
|
|
* a stream-init.
|
|
*/
|
|
if (vh->hThread == NULL) {
|
|
return(FALSE);
|
|
}
|
|
|
|
if (!DeviceIoControl(vh->hDriver,
|
|
IOCTL_VIDC_GET_POSITION,
|
|
NULL,
|
|
0,
|
|
&ulPosition,
|
|
sizeof(ULONG),
|
|
&dwCount,
|
|
NULL)) {
|
|
return(FALSE);
|
|
} else {
|
|
pTime->wType = TIME_MS;
|
|
pTime->u.ms = (DWORD) ulPosition;
|
|
return(TRUE);
|
|
}
|
|
}
|
|
|
|
|
|
|