/* * v4l2 backend to the VFW Capture filter * * Copyright 2005 Maarten Lankhorst * Copyright 2019 Zebediah Figura * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA */ #define BIONIC_IOCTL_NO_SIGNEDNESS_OVERLOAD /* work around ioctl breakage on Android */ #include "config.h" #include "wine/port.h" #define COBJMACROS #include #include #include #ifdef HAVE_SYS_IOCTL_H #include #endif #ifdef HAVE_SYS_MMAN_H #include #endif #include #ifdef HAVE_SYS_TIME_H #include #endif #ifdef HAVE_ASM_TYPES_H #include #endif #ifdef HAVE_LINUX_VIDEODEV2_H #include #endif #ifdef HAVE_UNISTD_H #include #endif #include "windef.h" #include "winbase.h" #include "wtypes.h" #include "wingdi.h" #include "winuser.h" #include "dshow.h" #include "vfwmsgs.h" #include "amvideo.h" #include "wine/debug.h" #include "wine/library.h" #include "qcap_main.h" #include "capture.h" WINE_DEFAULT_DEBUG_CHANNEL(qcap); #ifdef HAVE_LINUX_VIDEODEV2_H WINE_DECLARE_DEBUG_CHANNEL(winediag); static typeof(open) *video_open = open; static typeof(close) *video_close = close; static typeof(ioctl) *video_ioctl = ioctl; static typeof(read) *video_read = read; static BOOL video_init(void) { #ifdef SONAME_LIBV4L2 static void *video_lib; if (video_lib) return TRUE; if (!(video_lib = wine_dlopen(SONAME_LIBV4L2, RTLD_NOW, NULL, 0))) return FALSE; video_open = wine_dlsym(video_lib, "v4l2_open", NULL, 0); video_close = wine_dlsym(video_lib, "v4l2_close", NULL, 0); video_ioctl = wine_dlsym(video_lib, "v4l2_ioctl", NULL, 0); video_read = wine_dlsym(video_lib, "v4l2_read", NULL, 0); return TRUE; #else return FALSE; #endif } struct _Capture { UINT width, height, bitDepth, fps, outputwidth, outputheight; BOOL swresize; struct strmbase_source *pin; int fd, mmap; FILTER_STATE state; HANDLE thread, run_event; }; static int xioctl(int fd, int request, void * arg) { int r; do { r = video_ioctl (fd, request, arg); } while (-1 == r && EINTR == errno); return r; } HRESULT qcap_driver_destroy(Capture *capBox) { TRACE("%p\n", capBox); if( capBox->fd != -1 ) video_close(capBox->fd); CoTaskMemFree(capBox); return S_OK; } HRESULT qcap_driver_check_format(Capture *device, const AM_MEDIA_TYPE *mt) { HRESULT hr; TRACE("device %p, mt %p.\n", device, mt); if (!mt) return E_POINTER; if (!IsEqualGUID(&mt->majortype, &MEDIATYPE_Video)) return S_FALSE; if (IsEqualGUID(&mt->formattype, &FORMAT_VideoInfo) && mt->pbFormat && mt->cbFormat >= sizeof(VIDEOINFOHEADER)) { VIDEOINFOHEADER *vih = (VIDEOINFOHEADER *)mt->pbFormat; if (vih->bmiHeader.biBitCount == 24 && vih->bmiHeader.biCompression == BI_RGB) hr = S_OK; else { FIXME("Unsupported compression %#x, bpp %u.\n", vih->bmiHeader.biCompression, vih->bmiHeader.biBitCount); hr = S_FALSE; } } else hr = VFW_E_INVALIDMEDIATYPE; return hr; } HRESULT qcap_driver_set_format(Capture *device, AM_MEDIA_TYPE *mt) { struct v4l2_format format = {0}; int newheight, newwidth; VIDEOINFOHEADER *vih; int fd = device->fd; HRESULT hr; if (FAILED(hr = qcap_driver_check_format(device, mt))) return hr; vih = (VIDEOINFOHEADER *)mt->pbFormat; newwidth = vih->bmiHeader.biWidth; newheight = vih->bmiHeader.biHeight; if (device->height == newheight && device->width == newwidth) return S_OK; format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (xioctl(fd, VIDIOC_G_FMT, &format) == -1) { ERR("Failed to get current format: %s\n", strerror(errno)); return VFW_E_TYPE_NOT_ACCEPTED; } format.fmt.pix.width = newwidth; format.fmt.pix.height = newheight; if (!xioctl(fd, VIDIOC_S_FMT, &format) && format.fmt.pix.width == newwidth && format.fmt.pix.height == newheight) { device->width = newwidth; device->height = newheight; device->swresize = FALSE; } else { TRACE("Using software resize: %dx%d -> %dx%d.\n", format.fmt.pix.width, format.fmt.pix.height, device->width, device->height); device->swresize = TRUE; } device->outputwidth = format.fmt.pix.width; device->outputheight = format.fmt.pix.height; return S_OK; } HRESULT qcap_driver_get_format(const Capture *capBox, AM_MEDIA_TYPE ** mT) { VIDEOINFOHEADER *vi; mT[0] = CoTaskMemAlloc(sizeof(AM_MEDIA_TYPE)); if (!mT[0]) return E_OUTOFMEMORY; vi = CoTaskMemAlloc(sizeof(VIDEOINFOHEADER)); mT[0]->cbFormat = sizeof(VIDEOINFOHEADER); if (!vi) { CoTaskMemFree(mT[0]); mT[0] = NULL; return E_OUTOFMEMORY; } mT[0]->majortype = MEDIATYPE_Video; mT[0]->subtype = MEDIASUBTYPE_RGB24; mT[0]->formattype = FORMAT_VideoInfo; mT[0]->bFixedSizeSamples = TRUE; mT[0]->bTemporalCompression = FALSE; mT[0]->pUnk = NULL; mT[0]->lSampleSize = capBox->outputwidth * capBox->outputheight * capBox->bitDepth / 8; TRACE("Output format: %dx%d - %d bits = %u KB\n", capBox->outputwidth, capBox->outputheight, capBox->bitDepth, mT[0]->lSampleSize/1024); vi->rcSource.left = 0; vi->rcSource.top = 0; vi->rcTarget.left = 0; vi->rcTarget.top = 0; vi->rcSource.right = capBox->width; vi->rcSource.bottom = capBox->height; vi->rcTarget.right = capBox->outputwidth; vi->rcTarget.bottom = capBox->outputheight; vi->dwBitRate = capBox->fps * mT[0]->lSampleSize; vi->dwBitErrorRate = 0; vi->AvgTimePerFrame = (LONGLONG)10000000.0 / (LONGLONG)capBox->fps; vi->bmiHeader.biSize = 40; vi->bmiHeader.biWidth = capBox->outputwidth; vi->bmiHeader.biHeight = capBox->outputheight; vi->bmiHeader.biPlanes = 1; vi->bmiHeader.biBitCount = 24; vi->bmiHeader.biCompression = BI_RGB; vi->bmiHeader.biSizeImage = mT[0]->lSampleSize; vi->bmiHeader.biClrUsed = vi->bmiHeader.biClrImportant = 0; vi->bmiHeader.biXPelsPerMeter = 100; vi->bmiHeader.biYPelsPerMeter = 100; mT[0]->pbFormat = (void *)vi; return S_OK; } static __u32 v4l2_cid_from_qcap_property(VideoProcAmpProperty property) { switch (property) { case VideoProcAmp_Brightness: return V4L2_CID_BRIGHTNESS; case VideoProcAmp_Contrast: return V4L2_CID_CONTRAST; case VideoProcAmp_Hue: return V4L2_CID_HUE; case VideoProcAmp_Saturation: return V4L2_CID_SATURATION; default: FIXME("Unhandled property %d.\n", property); return 0; } } HRESULT qcap_driver_get_prop_range(Capture *device, VideoProcAmpProperty property, LONG *min, LONG *max, LONG *step, LONG *default_value, LONG *flags) { struct v4l2_queryctrl ctrl; ctrl.id = v4l2_cid_from_qcap_property(property); if (xioctl(device->fd, VIDIOC_QUERYCTRL, &ctrl) == -1) { WARN("Failed to query control: %s\n", strerror(errno)); return E_PROP_ID_UNSUPPORTED; } *min = ctrl.minimum; *max = ctrl.maximum; *step = ctrl.step; *default_value = ctrl.default_value; *flags = VideoProcAmp_Flags_Manual; return S_OK; } HRESULT qcap_driver_get_prop(Capture *device, VideoProcAmpProperty property, LONG *value, LONG *flags) { struct v4l2_control ctrl; ctrl.id = v4l2_cid_from_qcap_property(property); if (xioctl(device->fd, VIDIOC_G_CTRL, &ctrl) == -1) { WARN("Failed to get property: %s\n", strerror(errno)); return E_FAIL; } *value = ctrl.value; *flags = VideoProcAmp_Flags_Manual; return S_OK; } HRESULT qcap_driver_set_prop(Capture *device, VideoProcAmpProperty property, LONG value, LONG flags) { struct v4l2_control ctrl; ctrl.id = v4l2_cid_from_qcap_property(property); ctrl.value = value; if (xioctl(device->fd, VIDIOC_S_CTRL, &ctrl) == -1) { WARN("Failed to set property: %s\n", strerror(errno)); return E_FAIL; } return S_OK; } static void Resize(const Capture * capBox, LPBYTE output, const BYTE *input) { /* the whole image needs to be reversed, because the dibs are messed up in windows */ if (!capBox->swresize) { int depth = capBox->bitDepth / 8; int inoffset = 0, outoffset = capBox->height * capBox->width * depth; int ow = capBox->width * depth; while (outoffset > 0) { int x; outoffset -= ow; for (x = 0; x < ow; x++) output[outoffset + x] = input[inoffset + x]; inoffset += ow; } } else { HDC dc_s, dc_d; HBITMAP bmp_s, bmp_d; int depth = capBox->bitDepth / 8; int inoffset = 0, outoffset = (capBox->outputheight) * capBox->outputwidth * depth; int ow = capBox->outputwidth * depth; LPBYTE myarray; /* FIXME: Improve software resizing: add error checks and optimize */ myarray = CoTaskMemAlloc(capBox->outputwidth * capBox->outputheight * depth); dc_s = CreateCompatibleDC(NULL); dc_d = CreateCompatibleDC(NULL); bmp_s = CreateBitmap(capBox->width, capBox->height, 1, capBox->bitDepth, input); bmp_d = CreateBitmap(capBox->outputwidth, capBox->outputheight, 1, capBox->bitDepth, NULL); SelectObject(dc_s, bmp_s); SelectObject(dc_d, bmp_d); StretchBlt(dc_d, 0, 0, capBox->outputwidth, capBox->outputheight, dc_s, 0, 0, capBox->width, capBox->height, SRCCOPY); GetBitmapBits(bmp_d, capBox->outputwidth * capBox->outputheight * depth, myarray); while (outoffset > 0) { int i; outoffset -= ow; for (i = 0; i < ow; i++) output[outoffset + i] = myarray[inoffset + i]; inoffset += ow; } CoTaskMemFree(myarray); DeleteObject(dc_s); DeleteObject(dc_d); DeleteObject(bmp_s); DeleteObject(bmp_d); } } static DWORD WINAPI ReadThread(LPVOID lParam) { Capture * capBox = lParam; HRESULT hr; IMediaSample *pSample = NULL; ULONG framecount = 0; unsigned char *pTarget, *image_data; unsigned int image_size; image_size = capBox->height * capBox->width * 3; if (!(image_data = heap_alloc(image_size))) { ERR("Failed to allocate memory.\n"); return 0; } while (capBox->state != State_Stopped) { if (capBox->state == State_Paused) WaitForSingleObject(capBox->run_event, INFINITE); hr = BaseOutputPinImpl_GetDeliveryBuffer(capBox->pin, &pSample, NULL, NULL, 0); if (SUCCEEDED(hr)) { int len; if (!capBox->swresize) len = capBox->height * capBox->width * capBox->bitDepth / 8; else len = capBox->outputheight * capBox->outputwidth * capBox->bitDepth / 8; IMediaSample_SetActualDataLength(pSample, len); len = IMediaSample_GetActualDataLength(pSample); TRACE("Data length: %d KB\n", len / 1024); IMediaSample_GetPointer(pSample, &pTarget); while (video_read(capBox->fd, image_data, image_size) == -1) { if (errno != EAGAIN) { ERR("Failed to read frame: %s\n", strerror(errno)); break; } } Resize(capBox, pTarget, image_data); hr = IMemInputPin_Receive(capBox->pin->pMemInputPin, pSample); TRACE("%p -> Frame %u: %x\n", capBox, ++framecount, hr); IMediaSample_Release(pSample); } if (FAILED(hr) && hr != VFW_E_NOT_CONNECTED) { TRACE("Return %x, stop IFilterGraph\n", hr); break; } } heap_free(image_data); return 0; } void qcap_driver_init_stream(Capture *device) { ALLOCATOR_PROPERTIES req_props, ret_props; HRESULT hr; req_props.cBuffers = 3; if (!device->swresize) req_props.cbBuffer = device->width * device->height; else req_props.cbBuffer = device->outputwidth * device->outputheight; req_props.cbBuffer = (req_props.cbBuffer * device->bitDepth) / 8; req_props.cbAlign = 1; req_props.cbPrefix = 0; hr = IMemAllocator_SetProperties(device->pin->pAllocator, &req_props, &ret_props); if (FAILED(hr)) ERR("Failed to set allocator properties (buffer size %u), hr %#x.\n", req_props.cbBuffer, hr); if (SUCCEEDED(hr)) { if (FAILED(hr = IMemAllocator_Commit(device->pin->pAllocator))) ERR("Failed to commit allocator, hr %#x.\n", hr); } device->state = State_Paused; device->thread = CreateThread(NULL, 0, ReadThread, device, 0, NULL); } void qcap_driver_start_stream(Capture *device) { device->state = State_Running; SetEvent(device->run_event); } void qcap_driver_stop_stream(Capture *device) { device->state = State_Paused; ResetEvent(device->run_event); } void qcap_driver_cleanup_stream(Capture *device) { HRESULT hr; device->state = State_Stopped; WaitForSingleObject(device->thread, INFINITE); CloseHandle(device->thread); device->thread = NULL; hr = IMemAllocator_Decommit(device->pin->pAllocator); if (hr != S_OK && hr != VFW_E_NOT_COMMITTED) ERR("Failed to decommit allocator, hr %#x.\n", hr); } Capture *qcap_driver_init(struct strmbase_source *pin, USHORT card) { struct v4l2_capability caps = {{0}}; struct v4l2_format format = {0}; Capture *device = NULL; BOOL have_libv4l2; char path[20]; int fd; have_libv4l2 = video_init(); if (!(device = CoTaskMemAlloc(sizeof(*device)))) return NULL; sprintf(path, "/dev/video%i", card); TRACE("Opening device %s.\n", path); #ifdef O_CLOEXEC if ((fd = video_open(path, O_RDWR | O_NONBLOCK | O_CLOEXEC)) == -1 && errno == EINVAL) #endif fd = video_open(path, O_RDWR | O_NONBLOCK); if (fd == -1) { WARN("Failed to open video device: %s\n", strerror(errno)); goto error; } fcntl(fd, F_SETFD, FD_CLOEXEC); /* in case O_CLOEXEC isn't supported */ device->fd = fd; if (xioctl(fd, VIDIOC_QUERYCAP, &caps) == -1) { WARN("Failed to query device capabilities: %s\n", strerror(errno)); goto error; } #ifdef V4L2_CAP_DEVICE_CAPS if (caps.capabilities & V4L2_CAP_DEVICE_CAPS) caps.capabilities = caps.device_caps; #endif if (!(caps.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { WARN("Device does not support single-planar video capture.\n"); goto error; } if (!(caps.capabilities & V4L2_CAP_READWRITE)) { WARN("Device does not support read().\n"); if (!have_libv4l2) #ifdef SONAME_LIBV4L2 ERR_(winediag)("Reading from %s requires libv4l2, but it could not be loaded.\n", path); #else ERR_(winediag)("Reading from %s requires libv4l2, but Wine was compiled without libv4l2 support.\n", path); #endif goto error; } format.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (xioctl(fd, VIDIOC_G_FMT, &format) == -1) { ERR("Failed to get device format: %s\n", strerror(errno)); goto error; } format.fmt.pix.pixelformat = V4L2_PIX_FMT_BGR24; if (xioctl(fd, VIDIOC_S_FMT, &format) == -1 || format.fmt.pix.pixelformat != V4L2_PIX_FMT_BGR24) { ERR("Failed to set pixel format: %s\n", strerror(errno)); if (!have_libv4l2) ERR_(winediag)("You may need libv4l2 to use this device.\n"); goto error; } device->outputwidth = device->width = format.fmt.pix.width; device->outputheight = device->height = format.fmt.pix.height; device->swresize = FALSE; device->bitDepth = 24; device->pin = pin; device->fps = 3; device->state = State_Stopped; device->run_event = CreateEventW(NULL, TRUE, FALSE, NULL); TRACE("Format: %d bpp - %dx%d.\n", device->bitDepth, device->width, device->height); return device; error: qcap_driver_destroy(device); return NULL; } #else Capture *qcap_driver_init(struct strmbase_source *pin, USHORT card) { static const char msg[] = "The v4l headers were not available at compile time,\n" "so video capture support is not available.\n"; MESSAGE(msg); return NULL; } #define FAIL_WITH_ERR \ ERR("v4l absent: shouldn't be called\n"); \ return E_NOTIMPL HRESULT qcap_driver_destroy(Capture *capBox) { FAIL_WITH_ERR; } HRESULT qcap_driver_check_format(Capture *device, const AM_MEDIA_TYPE *mt) { FAIL_WITH_ERR; } HRESULT qcap_driver_set_format(Capture *capBox, AM_MEDIA_TYPE * mT) { FAIL_WITH_ERR; } HRESULT qcap_driver_get_format(const Capture *capBox, AM_MEDIA_TYPE ** mT) { FAIL_WITH_ERR; } HRESULT qcap_driver_get_prop_range( Capture *capBox, VideoProcAmpProperty Property, LONG *pMin, LONG *pMax, LONG *pSteppingDelta, LONG *pDefault, LONG *pCapsFlags ) { FAIL_WITH_ERR; } HRESULT qcap_driver_get_prop(Capture *capBox, VideoProcAmpProperty Property, LONG *lValue, LONG *Flags) { FAIL_WITH_ERR; } HRESULT qcap_driver_set_prop(Capture *capBox, VideoProcAmpProperty Property, LONG lValue, LONG Flags) { FAIL_WITH_ERR; } void qcap_driver_init_stream(Capture *device) { ERR("v4l absent: shouldn't be called\n"); } void qcap_driver_start_stream(Capture *device) { ERR("v4l absent: shouldn't be called\n"); } void qcap_driver_stop_stream(Capture *device) { ERR("v4l absent: shouldn't be called\n"); } void qcap_driver_cleanup_stream(Capture *device) { ERR("v4l absent: shouldn't be called\n"); } #endif /* defined(VIDIOCMCAPTURE) */