commit ce321a6e335b0d9579c880762f2c3a667aa711c6 Author: uvos Date: Sat Nov 18 14:07:27 2023 +0100 intial commit diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6561ac4 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 2.4) +project(videosender) + +set(CMAKE_CXX_STANDARD 17) +add_executable(videosender main.cpp webcam.cpp jpeg_img.cpp) + +target_link_libraries( videosender -ljpeg ) +add_definitions("-s -Wall") + +install(TARGETS videosender RUNTIME DESTINATION bin) diff --git a/image.h b/image.h new file mode 100644 index 0000000..05a8612 --- /dev/null +++ b/image.h @@ -0,0 +1,43 @@ +#pragma once +#include +#include +#include +#include + +class Image +{ +public: + std::shared_ptr data = nullptr; + uint32_t width; + uint32_t height; + uint8_t channels; + const uint32_t size() + { + return width*height*channels; + } + void save(const char* filename) + { + std::ofstream outfile(filename); + outfile.write((char *) data.get(), size()); + outfile.close(); + } + bool open(const char* filename, const uint32_t widthI, const uint32_t heightI, const uint32_t channelsI) + { + height = heightI; + width = widthI; + channels = channelsI; + std::ifstream infile( filename, std::ios::binary); + if(!infile.is_open() || infile.bad()) return false; + + infile.seekg (0, infile.end); + uint32_t filelength = infile.tellg(); + infile.seekg (0, infile.beg); + if(filelength != size()) return false; + + data = std::shared_ptr(new unsigned char[filelength]); + infile.read((char*)data.get(), filelength); + + infile.close(); + return true; + } +}; diff --git a/jpeg_img.cpp b/jpeg_img.cpp new file mode 100644 index 0000000..c159558 --- /dev/null +++ b/jpeg_img.cpp @@ -0,0 +1,104 @@ +#include "jpeg_img.h" + +#include +#include +#include + +Image decompressJpegImage(const unsigned char* buffer, size_t size) +{ + struct jpeg_decompress_struct info; //for our jpeg info + struct jpeg_error_mgr err; //the error handler + + info.err = jpeg_std_error( &err ); + jpeg_create_decompress( &info ); //fills info structure + + jpeg_mem_src(&info, buffer, size); + jpeg_read_header( &info, true ); + + jpeg_start_decompress( &info ); + + Image image; + + image.width = info.output_width; + image.height = info.output_height; + image.channels = info.num_components; // 3 = RGB, 4 = RGBA + + // read RGB(A) scanlines one at a time into jdata[] + image.data = std::shared_ptr(new unsigned char[image.size()]); + unsigned char* rowptr; + while(info.output_scanline < image.height) + { + rowptr = image.data.get() + info.output_scanline * image.width * image.channels; + jpeg_read_scanlines( &info, &rowptr, 1 ); + } + + jpeg_finish_decompress( &info ); + + return image; +} + +Image decompressJpegImage(FILE *file) +{ + struct jpeg_decompress_struct info; //for our jpeg info + struct jpeg_error_mgr err; //the error handler + + info.err = jpeg_std_error( &err ); + jpeg_create_decompress( &info ); //fills info structure + + jpeg_stdio_src( &info, file ); + jpeg_read_header( &info, true ); + + jpeg_start_decompress( &info ); + + Image image; + + image.width = info.output_width; + image.height = info.output_height; + image.channels = info.num_components; // 3 = RGB, 4 = RGBA + + // read RGB(A) scanlines one at a time into jdata[] + image.data = std::shared_ptr(new unsigned char[image.size()]); + unsigned char* rowptr; + while ( info.output_scanline < image.height ) + { + rowptr = image.data.get() + info.output_scanline * image.width * image.channels; + jpeg_read_scanlines( &info, &rowptr, 1 ); + } + + jpeg_finish_decompress( &info ); + + fclose( file ); + + return image; +} + +void compressJpegImage(FILE *file, Image *image) +{ + struct jpeg_compress_struct info; + struct jpeg_error_mgr err; + + info.err = jpeg_std_error( &err ); + + jpeg_create_compress(&info); + jpeg_stdio_dest(&info, file); + + /* Setting the parameters of the output file here */ + info.image_width = image->width; + info.image_height= image->height; + info.input_components = image->channels; + info.in_color_space = JCS_RGB; + + jpeg_set_defaults( &info ); + /* Now do the compression .. */ + jpeg_start_compress( &info, TRUE ); + unsigned char* rowptr; + while ( info.next_scanline < image->height ) + { + rowptr = image->data.get() + info.next_scanline * image->width * image->channels; + jpeg_write_scanlines( &info, &rowptr, 1 ); + } + + jpeg_finish_compress( &info ); + + jpeg_destroy_compress( &info ); +} diff --git a/jpeg_img.h b/jpeg_img.h new file mode 100644 index 0000000..08e0227 --- /dev/null +++ b/jpeg_img.h @@ -0,0 +1,9 @@ +#pragma once + +#include "image.h" + +Image decompressJpegImage(const unsigned char* buffer, size_t size); + +Image decompressJpegImage(FILE *file); + +void compressJpegImage(FILE *file, Image *image); diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..3ee0d92 --- /dev/null +++ b/main.cpp @@ -0,0 +1,71 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "image.h" +#include "jpeg_img.h" +#include "webcam.h" +#include "options.h" + + +void overlayImage(Image& target, Image& tool) +{ + if( target.size() != tool.size() || target.width != tool.width || target.channels != 3 ) + throw std::runtime_error("Overlay Image needs to be same size as target"); + else + { + for(uint32_t i = 0; i < target.size(); i+=3) + { + if(tool.data.get()[i] != 0xFF || tool.data.get()[i+1] != 0x00 || tool.data.get()[i+2] != 0xFF) + { + target.data.get()[i] = tool.data.get()[i]; + target.data.get()[i+1] = tool.data.get()[i+1]; + target.data.get()[i+2] = tool.data.get()[i+2]; + } + } + } +} + +void printUsage() +{ + std::cout<<"useage:\nvideosender [video device] [ip] [port] [overlay]\n"; +} + +int main(int argc, char* argv[]) +{ + Config config; + argp_parse(&argp, argc, argv, 0, 0, &config); + + std::cout<<"UVOS webcam sender v0.1\n"; + + Webcam webcam(config.device, config.Xres, config.Yres); + + std::stringstream ss; + ss<<"nc "< +#include +#include +#include +#include + +const inline char *argp_program_version = "VideoSender"; +const inline char *argp_program_bug_address = ""; +static char doc[] = "Application that sends video4linux streams over the network with minimal latency"; +static char args_doc[] = ""; + +static struct argp_option options[] = +{ + {"device", 'd', "[PATH]", 0, "device to use" }, + {"width", 'x', "[NUMBER]", 0, "Width at witch to capture frames from the camera"}, + {"height", 'y', "[NUMBER]", 0, "Heigt at witch to capture frames from the camera"}, + {"overlay", 'o', "[PATH]", 0, "Overlay file to use"}, + {"mjpeg", 'j', 0, 0, "Use mjpeg as the capture format for the camera"}, + {"addr", 'a', "[IP_ADDRESS]",0, "Ip address of the remote machine"}, + {"port", 'p', "[PORT]", 0, "Port of the remote machine"}, + { 0 } +}; + +struct Config +{ + std::string url = "10.0.0.1"; + int port = 9874; + std::filesystem::path device = "/dev/video0"; + int Xres = 640; + int Yres = 480; + std::filesystem::path overlay; + bool mjpeg = false; +}; + +static error_t parse_opt (int key, char *arg, struct argp_state *state) +{ + Config *config = reinterpret_cast(state->input); + + try + { + switch (key) + { + case 'd': + config->device = arg; + break; + case 'x': + config->Xres = std::stoi(arg); + break; + case 'y': + config->Yres = std::stoi(arg); + case 'o': + config->overlay = arg; + break; + case 'j': + config->mjpeg = true; + break; + case 'a': + config->url = arg; + break; + case 'p': + config->port = std::stoi(arg); + break; + default: + return ARGP_ERR_UNKNOWN; + } + } + catch(const std::invalid_argument& ex) + { + std::cout<(key)<<" is not a valid number.\n"; + return ARGP_KEY_ERROR; + } + return 0; +} + +static struct argp argp = {options, parse_opt, args_doc, doc}; diff --git a/overlay.data b/overlay.data new file mode 100644 index 0000000..198b310 Binary files /dev/null and b/overlay.data differ diff --git a/webcam.cpp b/webcam.cpp new file mode 100644 index 0000000..85e238e --- /dev/null +++ b/webcam.cpp @@ -0,0 +1,400 @@ +/* + + (c) 2016-2023 Carl Philipp Klemm + (c) 2014 Séverin Lemaignan + (c) 2008 Hans de Goede for yuyv_to_rgb24 + + This program 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 program 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 program; if not, write to the Free Software Foundation, + Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335 USA + + */ + +#include +#include +#include /* low-level i/o */ +#include +#include +#include // strerrno +#include +#include +#include +#include +#include + +#include + +#include + +#include "webcam.h" +#include "jpeg_img.h" + +#define CLEAR(x) memset(&(x), 0, sizeof(x)) + +using namespace std; + +static int xioctl(int fh, unsigned long int request, void *arg) +{ + int r; + + do { + r = ioctl(fh, request, arg); + } while (-1 == r && EINTR == errno); + + return r; +} + +#define CLIP(color) (unsigned char)(((color) > 0xFF) ? 0xff : (((color) < 0) ? 0 : (color))) + +static void v4lconvert_yuyv_to_rgb24(const unsigned char *src, + unsigned char *dest, + int width, int height, + int stride) +{ + int j; + + while (--height >= 0) { + for (j = 0; j + 1 < width; j += 2) { + int u = src[1]; + int v = src[3]; + int u1 = (((u - 128) << 7) + (u - 128)) >> 6; + int rg = (((u - 128) << 1) + (u - 128) + + ((v - 128) << 2) + ((v - 128) << 1)) >> 3; + int v1 = (((v - 128) << 1) + (v - 128)) >> 1; + + *dest++ = CLIP(src[0] + v1); + *dest++ = CLIP(src[0] - rg); + *dest++ = CLIP(src[0] + u1); + + *dest++ = CLIP(src[2] + v1); + *dest++ = CLIP(src[2] - rg); + *dest++ = CLIP(src[2] + u1); + src += 4; + } + src += stride - (width * 2); + } +} + +Webcam::Webcam(const string& device, int width, int height, bool mjpeg) : + device(device), + xres(width), + yres(height), + format(mjpeg ? V4L2_PIX_FMT_JPEG : V4L2_PIX_FMT_YUYV) +{ + open_device(); + init_device(); + // xres and yres are set to the actual resolution provided by the cam + + // frame stored as RGB888 (ie, RGB24) + rgb_frame.width = xres; + rgb_frame.height = yres; + rgb_frame.channels = 3; + rgb_frame.data = std::shared_ptr(new unsigned char[rgb_frame.size()]); + + start_capturing(); +} + +Webcam::~Webcam() +{ + stop_capturing(); + uninit_device(); + close_device(); +} + +const Image& Webcam::frame(int timeout) +{ + for (;;) { + fd_set fds; + struct timeval tv; + int r; + + FD_ZERO(&fds); + FD_SET(fd, &fds); + + /* Timeout. */ + tv.tv_sec = timeout; + tv.tv_usec = 0; + + r = select(fd + 1, &fds, NULL, NULL, &tv); + + if (-1 == r) { + if (EINTR == errno) + continue; + throw runtime_error("select"); + } + + if (0 == r) { + throw runtime_error(device + ": select timeout"); + } + int idx = read_frame(); + if (idx != -1) { + if (format == V4L2_PIX_FMT_YUYV) + { + v4lconvert_yuyv_to_rgb24((unsigned char *) buffers[idx].data, + rgb_frame.data.get(), + xres, + yres, + stride); + } + else if(format == V4L2_PIX_FMT_JPEG) + { + rgb_frame = decompressJpegImage((unsigned char *) buffers[idx].data, buffers[idx].size); + } + return rgb_frame; + } + /* EAGAIN - continue select loop. */ + } + +} + +bool Webcam::read_frame() +{ + + struct v4l2_buffer buf; + + CLEAR(buf); + + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + + if (-1 == xioctl(fd, VIDIOC_DQBUF, &buf)) { + switch (errno) { + case EAGAIN: + return -1; + + case EIO: + /* Could ignore EIO, see spec. */ + + /* fall through */ + + default: + throw runtime_error("VIDIOC_DQBUF"); + } + } + + assert(buf.index < n_buffers); + + if (-1 == xioctl(fd, VIDIOC_QBUF, &buf)) + throw runtime_error("VIDIOC_QBUF"); + + return buf.index; +} + +void Webcam::open_device(void) +{ + struct stat st; + + if (-1 == stat(device.c_str(), &st)) { + throw runtime_error(device + ": cannot identify! " + to_string(errno) + ": " + strerror(errno)); + } + + if (!S_ISCHR(st.st_mode)) { + throw runtime_error(device + " is no device"); + } + + fd = open(device.c_str(), O_RDWR /* required */ | O_NONBLOCK, 0); + + if (-1 == fd) { + throw runtime_error(device + ": cannot open! " + to_string(errno) + ": " + strerror(errno)); + } +} + + +void Webcam::init_mmap(void) +{ + struct v4l2_requestbuffers req; + + CLEAR(req); + + req.count = 1; + req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + req.memory = V4L2_MEMORY_MMAP; + + if (-1 == xioctl(fd, VIDIOC_REQBUFS, &req)) { + if (EINVAL == errno) { + throw runtime_error(device + " does not support memory mapping"); + } else { + throw runtime_error("VIDIOC_REQBUFS"); + } + } + + /*if (req.count < 2) { + throw runtime_error(string("Insufficient buffer memory on ") + device); + }*/ + + buffers = (buffer*) calloc(req.count, sizeof(*buffers)); + + if (!buffers) { + throw runtime_error("Out of memory"); + } + + for (n_buffers = 0; n_buffers < req.count; ++n_buffers) { + struct v4l2_buffer buf; + + CLEAR(buf); + + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = n_buffers; + + if (-1 == xioctl(fd, VIDIOC_QUERYBUF, &buf)) + throw runtime_error("VIDIOC_QUERYBUF"); + + buffers[n_buffers].size = buf.length; + buffers[n_buffers].data = + mmap(NULL /* start anywhere */, + buf.length, + PROT_READ | PROT_WRITE /* required */, + MAP_SHARED /* recommended */, + fd, buf.m.offset); + + if (MAP_FAILED == buffers[n_buffers].data) + throw runtime_error("mmap"); + } +} + +void Webcam::close_device(void) +{ + if (-1 == close(fd)) + throw runtime_error("close"); + + fd = -1; +} + +void Webcam::init_device(void) +{ + struct v4l2_capability cap; + struct v4l2_cropcap cropcap; + struct v4l2_crop crop; + struct v4l2_format fmt; + + if (-1 == xioctl(fd, VIDIOC_QUERYCAP, &cap)) { + if (EINVAL == errno) { + throw runtime_error(device + " is no V4L2 device"); + } else { + throw runtime_error("VIDIOC_QUERYCAP"); + } + } + + if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { + throw runtime_error(device + " is no video capture device"); + } + + if (!(cap.capabilities & V4L2_CAP_STREAMING)) { + throw runtime_error(device + " does not support streaming i/o"); + } + + /* Select video input, video standard and tune here. */ + + + CLEAR(cropcap); + + cropcap.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + + if (0 == xioctl(fd, VIDIOC_CROPCAP, &cropcap)) { + crop.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + crop.c = cropcap.defrect; /* reset to default */ + + if (-1 == xioctl(fd, VIDIOC_S_CROP, &crop)) { + switch (errno) { + case EINVAL: + /* Cropping not supported. */ + break; + default: + /* Errors ignored. */ + break; + } + } + } else { + /* Errors ignored. */ + } + + + CLEAR(fmt); + + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (force_format) { + fmt.fmt.pix.width = xres; + fmt.fmt.pix.height = yres; + fmt.fmt.pix.pixelformat = format; + fmt.fmt.pix.field = V4L2_FIELD_ANY; + + if (-1 == xioctl(fd, VIDIOC_S_FMT, &fmt)) + throw runtime_error("VIDIOC_S_FMT"); + + if (fmt.fmt.pix.pixelformat != format) + { + // note that libv4l2 (look for 'v4l-utils') provides helpers + // to manage conversions + throw runtime_error("Webcam does not support the requested format. Support for more formats need to be added!"); + } + + /* Note VIDIOC_S_FMT may change width and height. */ + xres = fmt.fmt.pix.width; + yres = fmt.fmt.pix.height; + + stride = fmt.fmt.pix.bytesperline; + + + } else { + /* Preserve original settings as set by v4l2-ctl for example */ + if (-1 == xioctl(fd, VIDIOC_G_FMT, &fmt)) + throw runtime_error("VIDIOC_G_FMT"); + } + + init_mmap(); +} + + +void Webcam::uninit_device(void) +{ + unsigned int i; + + for (i = 0; i < n_buffers; ++i) + if (-1 == munmap(buffers[i].data, buffers[i].size)) + throw runtime_error("munmap"); + + free(buffers); +} + +void Webcam::start_capturing(void) +{ + unsigned int i; + enum v4l2_buf_type type; + + for (i = 0; i < n_buffers; ++i) { + struct v4l2_buffer buf; + + CLEAR(buf); + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = i; + + if (-1 == xioctl(fd, VIDIOC_QBUF, &buf)) + throw runtime_error("VIDIOC_QBUF"); + } + type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (-1 == xioctl(fd, VIDIOC_STREAMON, &type)) + throw runtime_error("VIDIOC_STREAMON"); +} + +void Webcam::stop_capturing(void) +{ + enum v4l2_buf_type type; + + type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + if (-1 == xioctl(fd, VIDIOC_STREAMOFF, &type)) + throw runtime_error("VIDIOC_STREAMOFF"); +} + + diff --git a/webcam.h b/webcam.h new file mode 100644 index 0000000..09f2fdb --- /dev/null +++ b/webcam.h @@ -0,0 +1,68 @@ +/** Small C++ wrapper around V4L example code to access the webcam +**/ + +#include +#include // unique_ptr +#include + +#include "image.h" + +struct buffer { + void *data; + size_t size; +}; + + +class Webcam { + +public: + Webcam(const std::string& device = "/dev/video0", + int width = 640, + int height = 480, bool mjpeg = false); + + ~Webcam(); + + /** Captures and returns a frame from the webcam. + * + * The returned object contains a field 'data' with the image data in RGB888 + * format (ie, RGB24), as well as 'width', 'height' and 'size' (equal to + * width * height * 3) + * + * This call blocks until a frame is available or until the provided + * timeout (in seconds). + * + * Throws a runtime_error if the timeout is reached. + */ + const Image& frame(int timeout = 2); + +private: + void init_mmap(); + + void open_device(); + void close_device(); + + void init_device(); + void uninit_device(); + + void start_capturing(); + void stop_capturing(); + + bool read_frame(); + + std::string device; + int fd; + + Image rgb_frame; + struct buffer *buffers; + unsigned int n_buffers; + + size_t xres, yres; + size_t stride; + + bool force_format = true; + uint32_t format; +}; + + + +