diff --git a/CMakeLists.txt b/CMakeLists.txt index 6550962..d632ba3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,26 +21,46 @@ include_directories(include) IF (WIN32) -# add files for WIN32 -set (SOURCE common/libmain.cpp - common/context.cpp - common/logging.cpp - common/stream.cpp - win/platformcontext.cpp - win/platformstream.cpp) + # add files for WIN32 + set (SOURCE common/libmain.cpp + common/context.cpp + common/logging.cpp + common/stream.cpp + win/platformcontext.cpp + win/platformstream.cpp) -# add windows-specific test application -add_subdirectory(win/tests) + # create the library + add_library(openpnp-capture SHARED ${SOURCE}) + + # add windows-specific test application + add_subdirectory(win/tests) ELSEIF(APPLE) -set (SOURCE common/libmain.cpp - common/context.cpp - common/logging.cpp - common/stream.cpp) - + + set (SOURCE common/libmain.cpp + common/context.cpp + common/logging.cpp + common/stream.cpp) + + # create the library + add_library(openpnp-capture SHARED ${SOURCE}) + ELSEIF(UNIX) -# TODO: add files for UNIX/LINUX - message( FATAL_ERROR "Support for UNIX systems is pending." ) + + set (SOURCE common/libmain.cpp + common/context.cpp + common/logging.cpp + common/stream.cpp + linux/platformcontext.cpp + linux/platformstream.cpp) + + # need pthreads for std::thread + set(THREADS_PREFER_PTHREAD_FLAG ON) + find_package(Threads REQUIRED) + add_library(openpnp-capture SHARED ${SOURCE}) + target_link_libraries(openpnp-capture Threads::Threads) + + # add linux-specific test application + add_subdirectory(linux/tests) ENDIF() -add_library(openpnp-capture SHARED ${SOURCE}) diff --git a/bootstrap_linux.sh b/bootstrap_linux.sh new file mode 100755 index 0000000..deb4b03 --- /dev/null +++ b/bootstrap_linux.sh @@ -0,0 +1,4 @@ +#!/bin/sh +mkdir build +cd build +cmake .. diff --git a/common/logging.cpp b/common/logging.cpp index a6265c5..3da53ba 100644 --- a/common/logging.cpp +++ b/common/logging.cpp @@ -15,16 +15,16 @@ void LOG(uint32_t logLevel, const char *format, ...) switch(logLevel) { case LOG_CRIT: - printf("[CRIT] "); + fprintf(stderr,"[CRIT] "); break; case LOG_ERR: - printf("[ERR ] "); + fprintf(stderr,"[ERR ] "); break; case LOG_INFO: - printf("[INFO] "); + fprintf(stderr,"[INFO] "); break; case LOG_DEBUG: - printf("[DBG ] "); + fprintf(stderr,"[DBG ] "); break; default: break; diff --git a/common/stream.cpp b/common/stream.cpp index ec86c59..296a554 100644 --- a/common/stream.cpp +++ b/common/stream.cpp @@ -9,6 +9,7 @@ */ +#include // for memcpy #include "stream.h" #include "context.h" diff --git a/include/openpnp-capture.h b/include/openpnp-capture.h index 2ff7f80..5517e64 100644 --- a/include/openpnp-capture.h +++ b/include/openpnp-capture.h @@ -12,14 +12,14 @@ #include -// +// // #if defined(__clang__) #define SO_IMPORT #define SO_EXPORT #elif defined(__GNUC__) || defined(__GNUG__) - #define SO_IMPORT __attribute__ ((dllexport)) - #define SO_EXPORT __attribute__ ((dllexport)) + #define SO_IMPORT + #define SO_EXPORT #elif defined(_MSC_VER) #define SO_IMPORT __declspec(dllimport) #define SO_EXPORT __declspec(dllexport) diff --git a/linux/platformcontext.cpp b/linux/platformcontext.cpp new file mode 100644 index 0000000..a42f56c --- /dev/null +++ b/linux/platformcontext.cpp @@ -0,0 +1,143 @@ +/* + + OpenPnp-Capture: a video capture subsystem. + + Windows platform code + + Created by Niels Moseley on 7/6/17. + Copyright © 2017 Niels Moseley. All rights reserved. + + Platform/implementation specific structures + and typedefs. + +*/ + +#include +#include +#include +#include +#include +#include + +#include "../common/logging.h" +#include "platformstream.h" +#include "platformcontext.h" + +// a platform factory function needed by +// libmain.cpp +Context* createPlatformContext() +{ + return new PlatformContext(); +} + +PlatformContext::PlatformContext() : + Context() +{ + LOG(LOG_DEBUG, "Context created\n"); + enumerateDevices(); +} + +PlatformContext::~PlatformContext() +{ +} + +bool PlatformContext::enumerateDevices() +{ + int fd; + v4l2_capability video_cap; + //video_window video_win; + //video_picture video_pic; + + LOG(LOG_INFO,"Enumerating devices\n"); + + const uint32_t maxDevices = 64; // FIXME: is this a sane number for linux? + + uint32_t dcount = 0; + while(dcount < maxDevices) + { + char fname[100]; + snprintf(fname, sizeof(fname), "/dev/video%d", dcount++); + + if ((fd = ::open(fname, O_RDWR /* required */ | O_NONBLOCK)) == -1) + { + //LOG(LOG_ERR, "enumerateDevices: Can't open device %s\n", fname); + continue; + } + + if (ioctl(fd, VIDIOC_QUERYCAP, &video_cap) == -1) + { + ::close(fd); + LOG(LOG_ERR, "enumerateDevices: Can't get capabilities\n"); + continue; + } + else + { + LOG(LOG_INFO,"Name: '%s'\n", video_cap.card); + LOG(LOG_INFO,"Path: '%s'\n", fname); + LOG(LOG_INFO,"capflags = %08X\n", video_cap.capabilities); + LOG(LOG_INFO,"devflags = %08X\n", video_cap.device_caps); + + if ((video_cap.device_caps & V4L2_CAP_READWRITE) != 0) + { + LOG(LOG_INFO,"read/write supported\n"); + } + else + { + LOG(LOG_INFO,"read/write NOT supported\n"); + } + + if ((video_cap.device_caps & V4L2_CAP_STREAMING) != 0) + { + LOG(LOG_INFO,"streaming I/O supported\n"); + } + else + { + LOG(LOG_INFO,"streaming I/O NOT supported\n"); + } + + if ((video_cap.device_caps & V4L2_CAP_ASYNCIO) != 0) + { + LOG(LOG_INFO,"async I/O supported\n"); + } + else + { + LOG(LOG_INFO,"async I/O NOT supported\n"); + } + + //printf("Minimum size:\t%d x %d\n", video_cap.minwidth, video_cap.minheight); + //printf("Maximum size:\t%d x %d\n", video_cap.maxwidth, video_cap.maxheight); + } + + if ((video_cap.device_caps & V4L2_CAP_VIDEO_CAPTURE) != 0) + { + platformDeviceInfo* dinfo = new platformDeviceInfo(); + dinfo->m_name = std::string((const char*)video_cap.card); + dinfo->m_devicePath = std::string(fname); + m_devices.push_back(dinfo); + } + + +#if 0 + if (ioctl(fd, VIDIOCGWIN, &video_win) == -1) + { + LOG(LOG_ERR, "enumerateDevices: Can't get window information\n"); + } + else + { + LOG(LOG_INFO, "Current size:\t%d x %d\n", video_win.width, video_win.height); + } + + if (ioctl(fd, VIDIOCGPICT, &video_pic) == -1) + { + LOG(LOG_ERR, "enumerateDevices: Can't get picture information"); + } + else + { + printf("Current depth:\t%d\n", video_pic.depth); + } +#endif + + close(fd); + } + return true; +} diff --git a/linux/platformcontext.h b/linux/platformcontext.h new file mode 100644 index 0000000..9983f04 --- /dev/null +++ b/linux/platformcontext.h @@ -0,0 +1,57 @@ +/* + + OpenPnp-Capture: a video capture subsystem. + + Created by Niels Moseley on 7/11/17. + Copyright © 2017 Niels Moseley. All rights reserved. + + Platform independent context class to keep track + of the global state. + +*/ + +#ifndef openpnp_win32platformcontext_h +#define openpnp_win32platformcontext_h + +#define _CRT_SECURE_NO_WARNINGS + + +#include +#include +#include +#include + +#include "openpnp-capture.h" + +#pragma comment(lib, "strmiids") +#include "platformdeviceinfo.h" +#include "../common/context.h" + +/** context base class keeps track of all the platform independent + objects and information */ + +class PlatformContext : public Context +{ +public: + /** Create a context for the library. + Device enumeration is perform in the constructor, + so all devices must be present in the system when + the Context is created or devices will not be found. + + Re-enumeration support is pending. + */ + PlatformContext(); + virtual ~PlatformContext(); + +protected: + /** Enumerate V4L capture devices and put their + information into the m_devices array + + Implement this function in a platform-dependent + derived class. + */ + virtual bool enumerateDevices(); + +}; + +#endif \ No newline at end of file diff --git a/linux/platformdeviceinfo.h b/linux/platformdeviceinfo.h new file mode 100644 index 0000000..dcdbaad --- /dev/null +++ b/linux/platformdeviceinfo.h @@ -0,0 +1,26 @@ +#ifndef platformdeviceinfo_h +#define platformdeviceinfo_h + +#include +#include + +#include "../common/deviceinfo.h" + +/** device information struct/object */ +class platformDeviceInfo : public deviceInfo +{ +public: + platformDeviceInfo() : deviceInfo() + { + + } + + virtual ~platformDeviceInfo() + { + + } + + std::string m_devicePath; ///< unique device path +}; + +#endif \ No newline at end of file diff --git a/linux/platformstream.cpp b/linux/platformstream.cpp new file mode 100644 index 0000000..0d28274 --- /dev/null +++ b/linux/platformstream.cpp @@ -0,0 +1,364 @@ +/* + + OpenPnp-Capture: a video capture subsystem. + + Windows platform code + + Created by Niels Moseley on 7/6/17. + Copyright © 2017 Niels Moseley. All rights reserved. + + Stream class + +*/ + +#include +#include +#include +#include +#include +#include + +#include "platformdeviceinfo.h" +#include "platformstream.h" +#include "platformcontext.h" + +#define CLEAR(x) memset(&(x), 0, sizeof(x)) + +Stream* createPlatformStream() +{ + return new PlatformStream(); +} + +int xioctl(int fh, int request, void *arg) +{ + int r; + + do + { + r = ioctl(fh, request, arg); + } while ((r == -1) && (errno == EINTR)); + + return r; +} + +// ********************************************************************** +// Capture thread/function +// ********************************************************************** + +void captureThreadFunction(PlatformStream *stream, int fd, size_t bufferSizeBytes) +{ + if (stream == nullptr) + { + return; + } + + LOG(LOG_DEBUG, "capture thread running (deviceHandle = %08X) ...\n", fd); + + // create local frame buffer + std::vector buffer(bufferSizeBytes); + + // FIXME: For now, weĺl just rely on the read to fail + // when the PlatformStream closes the file + // descriptor. This doesn't feel very professional, + // but it should work :) + while(!stream->getThreadQuitState()) + { + ssize_t actualBytesRead = ::read(fd, &buffer[0], bufferSizeBytes); + if (actualBytesRead < 0) + { + LOG(LOG_DEBUG, "capture thread exited (errno %d).\n", errno); + return; //exit thread + } + + // read will only return complete buffers + stream->threadSubmitBuffer(&buffer[0], actualBytesRead); + LOG(LOG_INFO, "yay\n"); + } +} + +void captureThreadFunctionAsync(PlatformStream *stream, int fd, size_t bufferSizeBytes) +{ + //https://linuxtv.org/downloads/v4l-dvb-apis/uapi/v4l/capture.c.html + const uint32_t nBuffers = 5; + + struct CaptureBufferInfo + { + uint8_t *start; + size_t length; + }; + + CaptureBufferInfo buffers[nBuffers]; + + if (stream == nullptr) + { + return; + } + + // **************************************** + // create queue buffers + // **************************************** + + v4l2_buf_type bufferType; + for (uint32_t i = 0; i < nBuffers; ++i) + { + v4l2_buffer buf; + + CLEAR(buf); + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = i; + + if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) + { + LOG(LOG_ERR,"VIDIOC_QBUF failed (errno=%d)\n", errno); + } + } + + bufferType = V4L2_BUF_TYPE_VIDEO_CAPTURE; + + if (xioctl(fd, VIDIOC_STREAMON, &bufferType) == -1) + { + LOG(LOG_ERR,"VIDIOC_STREAMON failed (errno=%d)\n", errno); + } + + while(!stream->getThreadQuitState()) + { + fd_set fds; + struct timeval tv; + int result; + + FD_ZERO(&fds); + FD_SET(fd, &fds); + + /* Timeout. */ + tv.tv_sec = 2; + tv.tv_usec = 0; + + result = select(fd + 1, &fds, NULL, NULL, &tv); + if (result == -1) + { + if (errno == EINTR) + { + continue; + } + LOG(LOG_ERR,"Select failed (errno=%d)\n", errno); + return; + } + else if (result == 0) + { + LOG(LOG_ERR,"Select timeout\n"); + return; + } + + // **************************************** + // read the frame + // **************************************** + v4l2_buffer buf; + CLEAR(buf); + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + + if (xioctl(fd, VIDIOC_DQBUF, &buf) == -1) + { + switch (errno) + { + case EAGAIN: + return; + + case EIO: + /* Could ignore EIO, see spec. */ + + /* fall through */ + + default: + LOG(LOG_ERR, "VIDIOC_DQBUF error\n"); + return; + } + } + + //assert(buf.index < nBuffers); + stream->threadSubmitBuffer(buffers[buf.index].start, buf.bytesused); + + // re-queue the buffer + if (xioctl(fd, VIDIOC_QBUF, &buf) == -1) + { + LOG(LOG_ERR, "VIDIOC_DQBUF error\n"); + return; + } + } // while + + bufferType = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (xioctl(fd, VIDIOC_STREAMOFF, &bufferType) == -1) + { + LOG(LOG_ERR, "VIDIOC_STREAMOFF error\n"); + return; + } +} + +// ********************************************************************** +// PlatformStream +// ********************************************************************** + +PlatformStream::PlatformStream() : + Stream(), + m_quitThread(false), + m_helperThread(nullptr) +{ + +} + +PlatformStream::~PlatformStream() +{ + close(); +} + +void PlatformStream::close() +{ + LOG(LOG_INFO, "closing stream\n"); + + m_owner = nullptr; + m_width = 0; + m_height = 0; + m_frameBuffer.resize(0); + m_isOpen = false; + m_quitThread = true; + + ::close(m_deviceHandle); + + if (m_helperThread != nullptr) + { + m_helperThread->join(); + + delete m_helperThread; + + m_helperThread = nullptr; + } + + m_deviceHandle = -1; +} + +void test(size_t bufferSizeBytes) +{ + +} + +bool PlatformStream::open(Context *owner, deviceInfo *device, uint32_t width, uint32_t height, uint32_t fourCC) +{ + if (m_isOpen) + { + LOG(LOG_INFO,"open() was called on an active stream.\n"); + close(); + } + + if (owner == nullptr) + { + LOG(LOG_ERR,"open() was with owner=NULL!\n"); + return false; + } + + if (device == nullptr) + { + LOG(LOG_ERR,"open() was with device=NULL!\n"); + return false; + } + + platformDeviceInfo *dinfo = dynamic_cast(device); + if (dinfo == NULL) + { + LOG(LOG_CRIT, "Could not cast deviceInfo* to platfromDeviceInfo*!"); + return false; + } + + m_owner = owner; + m_frames = 0; + m_width = 0; + m_height = 0; + + //m_deviceHandle = ::open(dinfo->m_devicePath.c_str(), O_RDWR /* required */ | O_NONBLOCK); + m_deviceHandle = ::open(dinfo->m_devicePath.c_str(), O_RDWR /* required */); + if (m_deviceHandle < 0) + { + LOG(LOG_CRIT, "Could not open device %s (errno = %d)\n", dinfo->m_devicePath.c_str(), errno); + close(); + return false; + } + + m_fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (xioctl(m_deviceHandle, VIDIOC_G_FMT, &m_fmt) == -1) + { + LOG(LOG_CRIT, "Could not query default format (errno = %d)\n", errno); + close(); + return false; + } + + LOG(LOG_INFO, "Format buffer type: %d\n", m_fmt.type); + if (m_fmt.type != V4L2_BUF_TYPE_VIDEO_CAPTURE) + { + LOG(LOG_ERR, "Buffer type (%d) not supported!\n", m_fmt.type); + close(); + return false; + } + + m_width = m_fmt.fmt.pix.width; + m_height = m_fmt.fmt.pix.height; + + LOG(LOG_INFO, "Width = %d pixels\n", m_fmt.fmt.pix.width); + LOG(LOG_INFO, "Height = %d pixels\n", m_fmt.fmt.pix.height); + LOG(LOG_INFO, "FOURCC = %s\n", genFOURCCstring(m_fmt.fmt.pix.pixelformat).c_str()); + + m_isOpen = true; + + // create the helper thread to read from the device + m_quitThread = false; + +#if 0 + m_helperThread = new std::thread(&captureThreadFunction, this, + m_deviceHandle, m_width*m_height*4); +#else + m_helperThread = new std::thread(&captureThreadFunctionAsync, this, + m_deviceHandle, m_width*m_height*4); +#endif + return true; +} + +uint32_t PlatformStream::getFOURCC() +{ + if (m_isOpen) + { + return m_fmt.fmt.pix.pixelformat; + } + else + { + return 0; + } +} + + +bool PlatformStream::setExposure(int32_t value) +{ + return false; +} + + +bool PlatformStream::setAutoExposure(bool enabled) +{ + return false; +} + + +bool PlatformStream::getExposureLimits(int32_t *emin, int32_t *emax) +{ + return false; +} + +std::string PlatformStream::genFOURCCstring(uint32_t v) +{ + std::string result; + for(uint32_t i=0; i<4; i++) + { + result += static_cast(v & 0xFF); + v >>= 8; + } + return result; +} + diff --git a/linux/platformstream.h b/linux/platformstream.h new file mode 100644 index 0000000..3a2c733 --- /dev/null +++ b/linux/platformstream.h @@ -0,0 +1,77 @@ +/* + + OpenPnp-Capture: a video capture subsystem. + + Windows platform code + + Created by Niels Moseley on 7/6/17. + Copyright © 2017 Niels Moseley. All rights reserved. + + Stream class + +*/ + +#ifndef win32platform_stream_h +#define win32platform_stream_h + +#include +#include +#include +#include +#include +#include "../common/logging.h" +#include "../common/stream.h" + + +class Context; // pre-declaration +class PlatformStream; // pre-declaration + + +class PlatformStream : public Stream +{ +public: + PlatformStream(); + virtual ~PlatformStream(); + + /** Open a capture stream to a device and request a specific (internal) stream format. + When succesfully opened, capturing starts immediately. + */ + virtual bool open(Context *owner, deviceInfo *device, uint32_t width, uint32_t height, uint32_t fourCC) override; + + /** Close a capture stream */ + virtual void close() override; + + /** Return the FOURCC media type of the stream */ + virtual uint32_t getFOURCC() override; + + virtual bool setExposure(int32_t value) override; + + virtual bool setAutoExposure(bool enabled) override; + + virtual bool getExposureLimits(int32_t *min, int32_t *max) override; + + /** called by the capture thread/function to query if it + should quit */ + bool getThreadQuitState() const + { + return m_quitThread; + } + + /** public submit buffer so the capture thread/function + can access it */ + void threadSubmitBuffer(uint8_t *ptr, size_t bytes) + { + submitBuffer(ptr, bytes); + } + +protected: + /** generate FOURCC string from a uint32 */ + std::string genFOURCCstring(uint32_t v); + + int m_deviceHandle; ///< V4L2 device handle + v4l2_format m_fmt; ///< V4L2 frame format + bool m_quitThread; ///< if true, captureThreadFunction should return + std::thread *m_helperThread; ///< helper object threading control +}; + +#endif \ No newline at end of file diff --git a/linux/tests/CMakeLists.txt b/linux/tests/CMakeLists.txt new file mode 100644 index 0000000..f77b7c4 --- /dev/null +++ b/linux/tests/CMakeLists.txt @@ -0,0 +1,23 @@ +# +# CMAKE build file for OpenPnP Capture library +# +# This generates make files for several build systems, +# such as GNU Make, Ninja and visual studio +# +# When invoking on Windows systems, make sure the +# compiler (Visual Studio) is in the search path. +# +# Author: Niels A. Moseley +# + +cmake_minimum_required(VERSION 3.0) +project (openpnp-capture-test) + +# add include directory +include_directories(../include ..) + +set (SOURCE main.cpp ../../common/logging.cpp) + +add_executable(openpnp-capture-test ${SOURCE}) + +target_link_libraries(openpnp-capture-test openpnp-capture) \ No newline at end of file diff --git a/linux/tests/main.cpp b/linux/tests/main.cpp new file mode 100644 index 0000000..c927ea7 --- /dev/null +++ b/linux/tests/main.cpp @@ -0,0 +1,143 @@ +/* + + openpnp test application + + Niels Moseley + +*/ +#include +#include +#include +#include + +#include "openpnp-capture.h" +#include "../common/context.h" + +int main(int argc, char*argv[]) +{ + printf("OpenPNP Capture Test Program\n"); + Cap_setLogLevel(7); + + CapContext ctx = Cap_createContext(); + + uint32_t deviceCount = Cap_getDeviceCount(ctx); + printf("Number of devices: %d\n", deviceCount); + for(uint32_t i=0; i %s\n", i, Cap_getDeviceName(ctx,i)); + } + + int32_t streamID = Cap_openStream(ctx, 0, 0, 0, 0); + printf("Stream ID = %d\n", streamID); + + if (Cap_isOpenStream(ctx, streamID) == 1) + { + printf("Stream is open\n"); + } + else + { + printf("Stream is closed (?)\n"); + } + + printf("Press Q to exit..\n"); + + std::vector m_buffer; + m_buffer.resize(640*480*3); + + Cap_setAutoExposure(ctx, streamID, 1); + +#if 1 + uint32_t counter = 0; + uint32_t tries = 0; + while(counter < 30) + { + usleep(50000); + printf("%d", Cap_getStreamFrameCount(ctx, streamID)); + if (Cap_hasNewFrame(ctx, streamID) == 1) + { + Cap_captureFrame(ctx, streamID, &m_buffer[0], m_buffer.size()); + counter++; + } + tries++; + if (tries == 1000) + { + break; + } + }; +#endif + + Cap_setAutoExposure(ctx, streamID, 0); + + // wait for a new frame .. + //while (Cap_hasNewFrame(ctx, streamID) == 0) {}; + + if (Cap_captureFrame(ctx, streamID, &m_buffer[0], m_buffer.size()) == CAPRESULT_OK) + { + printf("Buffer captured!\n"); + + FILE *fout = fopen("image.ppm", "wb"); + fprintf(fout, "P6 640 480 255\n"); // PGM header + + const uint32_t height = 480; + const uint32_t width = 640; + + // exchange BGR to RGB + uint32_t idx = 0; + for(uint32_t i=0; i