From 6d8d431cbafc4884c8c32d27122df1588962b52d Mon Sep 17 00:00:00 2001 From: aler9 <46489434+aler9@users.noreply.github.com> Date: Wed, 14 Aug 2024 21:30:17 +0200 Subject: [PATCH] initial commit --- .dockerignore | 4 + .github/workflows/build.yml | 24 ++ .github/workflows/release.yml | 72 +++++ .gitignore | 4 + LICENSE | 21 ++ Makefile | 68 +++++ README.md | 46 ++++ base64.c | 87 ++++++ base64.h | 6 + camera.cpp | 499 ++++++++++++++++++++++++++++++++++ camera.h | 31 +++ encoder.c | 337 +++++++++++++++++++++++ encoder.h | 15 + main.c | 116 ++++++++ parameters.c | 210 ++++++++++++++ parameters.h | 64 +++++ pipe.c | 44 +++ pipe.h | 12 + sensor_mode.c | 27 ++ sensor_mode.h | 15 + text.c | 171 ++++++++++++ text.h | 15 + utils.mk | 61 +++++ window.c | 30 ++ window.h | 15 + 25 files changed, 1994 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 base64.c create mode 100644 base64.h create mode 100644 camera.cpp create mode 100644 camera.h create mode 100644 encoder.c create mode 100644 encoder.h create mode 100644 main.c create mode 100644 parameters.c create mode 100644 parameters.h create mode 100644 pipe.c create mode 100644 pipe.h create mode 100644 sensor_mode.c create mode 100644 sensor_mode.h create mode 100644 text.c create mode 100644 text.h create mode 100644 utils.mk create mode 100644 window.c create mode 100644 window.h diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..569b66b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +*.o +text_font.* +rpicc +rpicc_* diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..2321d3b --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,24 @@ +name: build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build_32_bit: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + + - run: make -f utils.mk build32 + + build_64_bit: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + + - run: make -f utils.mk build64 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d45df8c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,72 @@ +name: release + +on: + push: + tags: + - 'v*' + +jobs: + build_32_bit: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + + - run: make -f utils.mk build32 + + - uses: actions/upload-artifact@v3 + with: + name: rpicc + path: binaries/rpicc_32 + + build_64_bit: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v3 + + - run: make -f utils.mk build64 + + - uses: actions/upload-artifact@v3 + with: + name: rpicc + path: binaries/rpicc_64 + + github_release: + needs: + - build_32_bit + - build_64_bit + runs-on: ubuntu-22.04 + + steps: + - uses: actions/download-artifact@v3 + with: + name: binaries + path: binaries + + - uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs').promises; + const { repo: { owner, repo } } = context; + + const currentRelease = context.ref.split('/')[2]; + + const res = await github.rest.repos.createRelease({ + owner, + repo, + tag_name: currentRelease, + name: currentRelease, + }); + const release_id = res.data.id; + + for (const name of await fs.readdir('./binaries/')) { + await github.rest.repos.uploadReleaseAsset({ + owner, + repo, + release_id, + name, + data: await fs.readFile(`./binaries/${name}`), + }); + } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..569b66b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.o +text_font.* +rpicc +rpicc_* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2beaddb --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 bluenviron + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..94dc16f --- /dev/null +++ b/Makefile @@ -0,0 +1,68 @@ +all: rpicc + +################################################# +# text font + +TEXT_FONT_URL = https://github.com/IBM/plex/raw/v6.4.2/IBM-Plex-Mono/fonts/complete/ttf/IBMPlexMono-Medium.ttf +TEXT_FONT_SHA256 = 0bede3debdea8488bbb927f8f0650d915073209734a67fe8cd5a3320b572511c + +TEXT_FONT_TARGET = text_font.h + +text_font.ttf: + wget -O $@.tmp $(TEXT_FONT_URL) + H=$$(sha256sum $@.tmp | awk '{ print $$1 }'); [ "$$H" = "$(TEXT_FONT_SHA256)" ] || { echo "hash mismatch; got $$H, expected $(TEXT_FONT_SHA256)"; exit 1; } + mv $@.tmp $@ + +$(TEXT_FONT_TARGET): text_font.ttf + xxd --include $< > $@ + +################################################# +# rpicc + +CFLAGS = \ + -Ofast \ + -Werror \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -Wno-unused-result \ + $$(pkg-config --cflags freetype2) + +CXXFLAGS = \ + -Ofast \ + -Werror \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -Wno-unused-result \ + -std=c++17 \ + $$(pkg-config --cflags libcamera) + +LDFLAGS = \ + -s \ + -pthread \ + $$(pkg-config --libs freetype2) \ + $$(pkg-config --libs libcamera) + +OBJS = \ + base64.o \ + camera.o \ + encoder.o \ + main.o \ + parameters.o \ + pipe.o \ + sensor_mode.o \ + text.o \ + window.o + +DEPENDENCIES = \ + $(TEXT_FONT_TARGET) + +%.o: %.c $(DEPENDENCIES) + $(CC) $(CFLAGS) -c $< -o $@ + +%.o: %.cpp $(DEPENDENCIES) + $(CXX) $(CXXFLAGS) -c $< -o $@ + +rpicc: $(OBJS) + $(CXX) $^ $(LDFLAGS) -o $@ diff --git a/README.md b/README.md new file mode 100644 index 0000000..20b4383 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# mediamtx-rpicamera + +Raspberry Pi Camera component for [MediaMTX](https://github.com/bluenviron/mediamtx). + +This is embed into all MediaMTX releases and shouldn't normally be downloaded unless you want to recompile it. + +This is a C-based executable that pulls the Raspberry Camera video feed, encodes it and makes the compressed video available to the server, while listening for incoming commands. + +## Compile + +1. You must be on a Raspberry Pi, running Raspberry Pi OS Bullseye + +2. Install the build dependencies: + + ```sh + sudo apt install -y \ + g++ \ + pkg-config \ + make \ + libcamera-dev \ + libfreetype-dev \ + xxd \ + wget + ``` + +3. Build: + + ```sh + make -j$(nproc) + ``` + +4. The resulting executable will be available in `rpicc` + +## Cross-compile + +1. You can be on any machine you like + +2. Install Docker and `make` + +3. Build for every supported architecture: + + ``` + make -f utils.mk build + ``` + +4. The resulting executables will be available in `rpicc_32` and `rpicc_64` diff --git a/base64.c b/base64.c new file mode 100644 index 0000000..5079bb2 --- /dev/null +++ b/base64.c @@ -0,0 +1,87 @@ +#include +#include +#include +#include + +#include "base64.h" + +static const unsigned char decoding_table[256] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x3e, 0x00, 0x00, 0x00, 0x3f, + 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, + 0x3c, 0x3d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, + 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, + 0x17, 0x18, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, + 0x31, 0x32, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +char* base64_decode(const char *data) { + size_t input_length = strlen(data); + if (input_length % 4 != 0) { + return NULL; + } + + size_t output_length = input_length / 4 * 3; + + if (data[input_length - 1] == '=') { + (output_length)--; + } + if (data[input_length - 2] == '=') { + (output_length)--; + } + + unsigned char* output = (unsigned char *)malloc(output_length + 1); + if (output == NULL) { + return NULL; + } + + for (int i = 0, j = 0; i < (int)input_length;) { + uint32_t sextet_a = (data[i] == '=') ? (0 & i++) : decoding_table[(unsigned char)(data[i++])]; + uint32_t sextet_b = (data[i] == '=') ? (0 & i++) : decoding_table[(unsigned char)(data[i++])]; + uint32_t sextet_c = (data[i] == '=') ? (0 & i++) : decoding_table[(unsigned char)(data[i++])]; + uint32_t sextet_d = (data[i] == '=') ? (0 & i++) : decoding_table[(unsigned char)(data[i++])]; + + uint32_t triple = (sextet_a << 3 * 6) + + (sextet_b << 2 * 6) + + (sextet_c << 1 * 6) + + (sextet_d << 0 * 6); + + if (j < (int)output_length) { + output[j++] = (triple >> 2 * 8) & 0xFF; + } + if (j < (int)output_length) { + output[j++] = (triple >> 1 * 8) & 0xFF; + } + if (j < (int)output_length) { + output[j++] = (triple >> 0 * 8) & 0xFF; + } + } + + output[output_length] = 0x00; + return (char *)output; +}; diff --git a/base64.h b/base64.h new file mode 100644 index 0000000..5a656ae --- /dev/null +++ b/base64.h @@ -0,0 +1,6 @@ +#ifndef __BASE64_H__ +#define __BASE64_H__ + +char* base64_decode(const char *data); + +#endif diff --git a/camera.cpp b/camera.cpp new file mode 100644 index 0000000..73701a6 --- /dev/null +++ b/camera.cpp @@ -0,0 +1,499 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "camera.h" + +using libcamera::CameraManager; +using libcamera::CameraConfiguration; +using libcamera::Camera; +using libcamera::ColorSpace; +using libcamera::ControlList; +using libcamera::FrameBufferAllocator; +using libcamera::FrameBuffer; +using libcamera::PixelFormat; +using libcamera::Rectangle; +using libcamera::Request; +using libcamera::Size; +using libcamera::Span; +using libcamera::Stream; +using libcamera::StreamRole; +using libcamera::StreamConfiguration; +using libcamera::Transform; + +namespace controls = libcamera::controls; +namespace formats = libcamera::formats; +namespace properties = libcamera::properties; + +static char errbuf[256]; + +static void set_error(const char *format, ...) { + va_list args; + va_start(args, format); + vsnprintf(errbuf, 256, format, args); +} + +const char *camera_get_error() { + return errbuf; +} + +// https://github.com/raspberrypi/libcamera-apps/blob/dd97618a25523c2c4aa58f87af5f23e49aa6069c/core/libcamera_app.cpp#L42 +static PixelFormat mode_to_pixel_format(sensor_mode_t *mode) { + static std::vector, PixelFormat>> table = { + { {8, false}, formats::SBGGR8 }, + { {8, true}, formats::SBGGR8 }, + { {10, false}, formats::SBGGR10 }, + { {10, true}, formats::SBGGR10_CSI2P }, + { {12, false}, formats::SBGGR12 }, + { {12, true}, formats::SBGGR12_CSI2P }, + }; + + auto it = std::find_if(table.begin(), table.end(), [&mode] (auto &m) { + return mode->bit_depth == m.first.first && mode->packed == m.first.second; }); + if (it != table.end()) { + return it->second; + } + + return formats::SBGGR12_CSI2P; +} + +struct CameraPriv { + const parameters_t *params; + camera_frame_cb frame_cb; + std::unique_ptr camera_manager; + std::shared_ptr camera; + Stream *video_stream; + std::unique_ptr allocator; + std::vector> requests; + std::mutex ctrls_mutex; + std::unique_ptr ctrls; + std::map mapped_buffers; +}; + +static int get_v4l2_colorspace(std::optional const &cs) { + if (cs == ColorSpace::Rec709) { + return V4L2_COLORSPACE_REC709; + } + return V4L2_COLORSPACE_SMPTE170M; +} + +// https://github.com/raspberrypi/libcamera-apps/blob/a5b5506a132056ac48ba22bc581cc394456da339/core/libcamera_app.cpp#L824 +static uint8_t *map_buffer(FrameBuffer *buffer) { + size_t buffer_size = 0; + + for (unsigned i = 0; i < buffer->planes().size(); i++) { + const FrameBuffer::Plane &plane = buffer->planes()[i]; + buffer_size += plane.length; + + if (i == buffer->planes().size() - 1 || plane.fd.get() != buffer->planes()[i + 1].fd.get()) { + return (uint8_t *)mmap(NULL, buffer_size, PROT_READ | PROT_WRITE, MAP_SHARED, plane.fd.get(), 0); + } + } + + return NULL; +} + +// https://github.com/raspberrypi/libcamera-apps/blob/a6267d51949d0602eedf60f3ddf8c6685f652812/core/options.cpp#L101 +static void set_hdr(bool hdr) { + bool ok = false; + for (int i = 0; i < 4 && !ok; i++) + { + std::string dev("/dev/v4l-subdev"); + dev += (char)('0' + i); + int fd = open(dev.c_str(), O_RDWR, 0); + if (fd < 0) + continue; + + v4l2_control ctrl { V4L2_CID_WIDE_DYNAMIC_RANGE, hdr }; + ok = !ioctl(fd, VIDIOC_S_CTRL, &ctrl); + close(fd); + } +} + +bool camera_create(const parameters_t *params, camera_frame_cb frame_cb, camera_t **cam) { + std::unique_ptr camp = std::make_unique(); + + set_hdr(params->hdr); + + if (strcmp(params->log_level, "debug") == 0) { + setenv("LIBCAMERA_LOG_LEVELS", "*:DEBUG", 1); + } else if (strcmp(params->log_level, "info") == 0) { + setenv("LIBCAMERA_LOG_LEVELS", "*:INFO", 1); + } else if (strcmp(params->log_level, "warn") == 0) { + setenv("LIBCAMERA_LOG_LEVELS", "*:WARN", 1); + } else { // error + setenv("LIBCAMERA_LOG_LEVELS", "*:ERROR", 1); + } + + // We make sure to set the environment variable before libcamera init + setenv("LIBCAMERA_RPI_TUNING_FILE", params->tuning_file, 1); + + camp->camera_manager = std::make_unique(); + int ret = camp->camera_manager->start(); + if (ret != 0) { + set_error("CameraManager.start() failed"); + return false; + } + + std::vector> cameras = camp->camera_manager->cameras(); + auto rem = std::remove_if(cameras.begin(), cameras.end(), + [](auto &cam) { return cam->id().find("/usb") != std::string::npos; }); + cameras.erase(rem, cameras.end()); + if (params->camera_id >= cameras.size()){ + set_error("selected camera is not available"); + return false; + } + + camp->camera = camp->camera_manager->get(cameras[params->camera_id]->id()); + if (camp->camera == NULL) { + set_error("CameraManager.get() failed"); + return false; + } + + ret = camp->camera->acquire(); + if (ret != 0) { + set_error("Camera.acquire() failed"); + return false; + } + + std::vector stream_roles = { StreamRole::VideoRecording }; + if (params->mode != NULL) { + stream_roles.push_back(StreamRole::Raw); + } + + std::unique_ptr conf = camp->camera->generateConfiguration(stream_roles); + if (conf == NULL) { + set_error("Camera.generateConfiguration() failed"); + return false; + } + + StreamConfiguration &video_stream_conf = conf->at(0); + video_stream_conf.size = libcamera::Size(params->width, params->height); + video_stream_conf.pixelFormat = formats::YUV420; + video_stream_conf.bufferCount = params->buffer_count; + if (params->width >= 1280 || params->height >= 720) { + video_stream_conf.colorSpace = ColorSpace::Rec709; + } else { + video_stream_conf.colorSpace = ColorSpace::Smpte170m; + } + + if (params->mode != NULL) { + StreamConfiguration &raw_stream_conf = conf->at(1); + raw_stream_conf.size = Size(params->mode->width, params->mode->height); + raw_stream_conf.pixelFormat = mode_to_pixel_format(params->mode); + raw_stream_conf.bufferCount = video_stream_conf.bufferCount; + } + + conf->transform = Transform::Identity; + if (params->h_flip) { + conf->transform = Transform::HFlip * conf->transform; + } + if (params->v_flip) { + conf->transform = Transform::VFlip * conf->transform; + } + + CameraConfiguration::Status vstatus = conf->validate(); + if (vstatus == CameraConfiguration::Invalid) { + set_error("StreamConfiguration.validate() failed"); + return false; + } + + int res = camp->camera->configure(conf.get()); + if (res != 0) { + set_error("Camera.configure() failed"); + return false; + } + + camp->video_stream = video_stream_conf.stream(); + + for (unsigned int i = 0; i < params->buffer_count; i++) { + std::unique_ptr request = camp->camera->createRequest((uint64_t)camp.get()); + if (request == NULL) { + set_error("createRequest() failed"); + return false; + } + camp->requests.push_back(std::move(request)); + } + + camp->allocator = std::make_unique(camp->camera); + for (StreamConfiguration &stream_conf : *conf) { + Stream *stream = stream_conf.stream(); + + res = camp->allocator->allocate(stream); + if (res < 0) { + set_error("allocate() failed"); + return false; + } + + int i = 0; + for (const std::unique_ptr &buffer : camp->allocator->buffers(stream)) { + // map buffer of the video stream only + if (stream == video_stream_conf.stream()) { + camp->mapped_buffers[buffer.get()] = map_buffer(buffer.get()); + } + + res = camp->requests.at(i++)->addBuffer(stream, buffer.get()); + if (res != 0) { + set_error("addBuffer() failed"); + return false; + } + } + } + + camp->params = params; + camp->frame_cb = frame_cb; + *cam = camp.release(); + + return true; +} + +static int buffer_size(const std::vector &planes) { + int size = 0; + for (const FrameBuffer::Plane &plane : planes) { + size += plane.length; + } + return size; +} + +static void on_request_complete(Request *request) { + if (request->status() == Request::RequestCancelled) { + return; + } + + CameraPriv *camp = (CameraPriv *)request->cookie(); + + FrameBuffer *buffer = request->buffers().at(camp->video_stream); + + camp->frame_cb( + camp->mapped_buffers.at(buffer), + camp->video_stream->configuration().stride, + camp->video_stream->configuration().size.height, + buffer->planes()[0].fd.get(), + buffer_size(buffer->planes()), + buffer->metadata().timestamp / 1000); + + request->reuse(Request::ReuseFlag::ReuseBuffers); + + { + std::lock_guard lock(camp->ctrls_mutex); + request->controls() = *camp->ctrls; + camp->ctrls->clear(); + } + + camp->camera->queueRequest(request); +} + +int camera_get_mode_stride(camera_t *cam) { + CameraPriv *camp = (CameraPriv *)cam; + return camp->video_stream->configuration().stride; +} + +int camera_get_mode_colorspace(camera_t *cam) { + CameraPriv *camp = (CameraPriv *)cam; + return get_v4l2_colorspace(camp->video_stream->configuration().colorSpace); +} + +static void fill_dynamic_controls(ControlList *ctrls, const parameters_t *params) { + ctrls->set(controls::Brightness, params->brightness); + ctrls->set(controls::Contrast, params->contrast); + ctrls->set(controls::Saturation, params->saturation); + ctrls->set(controls::Sharpness, params->sharpness); + + int exposure_mode; + if (strcmp(params->exposure, "short") == 0) { + exposure_mode = controls::ExposureShort; + } else if (strcmp(params->exposure, "long") == 0) { + exposure_mode = controls::ExposureLong; + } else if (strcmp(params->exposure, "custom") == 0) { + exposure_mode = controls::ExposureCustom; + } else { + exposure_mode = controls::ExposureNormal; + } + ctrls->set(controls::AeExposureMode, exposure_mode); + + int awb_mode; + if (strcmp(params->awb, "incandescent") == 0) { + awb_mode = controls::AwbIncandescent; + } else if (strcmp(params->awb, "tungsten") == 0) { + awb_mode = controls::AwbTungsten; + } else if (strcmp(params->awb, "fluorescent") == 0) { + awb_mode = controls::AwbFluorescent; + } else if (strcmp(params->awb, "indoor") == 0) { + awb_mode = controls::AwbIndoor; + } else if (strcmp(params->awb, "daylight") == 0) { + awb_mode = controls::AwbDaylight; + } else if (strcmp(params->awb, "cloudy") == 0) { + awb_mode = controls::AwbCloudy; + } else if (strcmp(params->awb, "custom") == 0) { + awb_mode = controls::AwbCustom; + } else { + awb_mode = controls::AwbAuto; + } + ctrls->set(controls::AwbMode, awb_mode); + + if (params->awb_gain_red > 0 && params->awb_gain_blue > 0) { + ctrls->set(controls::ColourGains, + Span({params->awb_gain_red, params->awb_gain_blue})); + } + + int denoise_mode; + if (strcmp(params->denoise, "cdn_off") == 0) { + denoise_mode = controls::draft::NoiseReductionModeMinimal; + } else if (strcmp(params->denoise, "cdn_hq") == 0) { + denoise_mode = controls::draft::NoiseReductionModeHighQuality; + } else if (strcmp(params->denoise, "cdn_fast") == 0) { + denoise_mode = controls::draft::NoiseReductionModeFast; + } else { + denoise_mode = controls::draft::NoiseReductionModeOff; + } + ctrls->set(controls::draft::NoiseReductionMode, denoise_mode); + + ctrls->set(controls::ExposureTime, params->shutter); + + int metering_mode; + if (strcmp(params->metering, "spot") == 0) { + metering_mode = controls::MeteringSpot; + } else if (strcmp(params->metering, "matrix") == 0) { + metering_mode = controls::MeteringMatrix; + } else if (strcmp(params->metering, "custom") == 0) { + metering_mode = controls::MeteringCustom; + } else { + metering_mode = controls::MeteringCentreWeighted; + } + ctrls->set(controls::AeMeteringMode, metering_mode); + + ctrls->set(controls::AnalogueGain, params->gain); + + ctrls->set(controls::ExposureValue, params->ev); + + int64_t frame_time = (int64_t)(((float)1000000) / params->fps); + ctrls->set(controls::FrameDurationLimits, Span({ frame_time, frame_time })); +} + +bool camera_start(camera_t *cam) { + CameraPriv *camp = (CameraPriv *)cam; + + camp->ctrls = std::make_unique(controls::controls); + + fill_dynamic_controls(camp->ctrls.get(), camp->params); + + if (camp->camera->controls().count(&controls::AfMode) > 0) { + if (camp->params->af_window != NULL) { + std::optional opt = camp->camera->properties().get(properties::ScalerCropMaximum); + Rectangle sensor_area; + try { + sensor_area = opt.value(); + } catch(const std::bad_optional_access& exc) { + set_error("get(ScalerCropMaximum) failed"); + return false; + } + + Rectangle afwindows_rectangle[1]; + + afwindows_rectangle[0] = Rectangle( + camp->params->af_window->x * sensor_area.width, + camp->params->af_window->y * sensor_area.height, + camp->params->af_window->width * sensor_area.width, + camp->params->af_window->height * sensor_area.height); + + afwindows_rectangle[0].translateBy(sensor_area.topLeft()); + camp->ctrls->set(controls::AfMetering, controls::AfMeteringWindows); + camp->ctrls->set(controls::AfWindows, afwindows_rectangle); + } + + int af_mode; + if (strcmp(camp->params->af_mode, "manual") == 0) { + af_mode = controls::AfModeManual; + } else if (strcmp(camp->params->af_mode, "continuous") == 0) { + af_mode = controls::AfModeContinuous; + } else { + af_mode = controls::AfModeAuto; + } + camp->ctrls->set(controls::AfMode, af_mode); + + int af_range; + if (strcmp(camp->params->af_range, "macro") == 0) { + af_range = controls::AfRangeMacro; + } else if (strcmp(camp->params->af_range, "full") == 0) { + af_range = controls::AfRangeFull; + } else { + af_range = controls::AfRangeNormal; + } + camp->ctrls->set(controls::AfRange, af_range); + + int af_speed; + if (strcmp(camp->params->af_range, "fast") == 0) { + af_speed = controls::AfSpeedFast; + } else { + af_speed = controls::AfSpeedNormal; + } + camp->ctrls->set(controls::AfSpeed, af_speed); + + if (strcmp(camp->params->af_mode, "auto") == 0) { + camp->ctrls->set(controls::AfTrigger, controls::AfTriggerStart); + } else if (strcmp(camp->params->af_mode, "manual") == 0) { + camp->ctrls->set(controls::LensPosition, camp->params->lens_position); + } + } + + if (camp->params->roi != NULL) { + std::optional opt = camp->camera->properties().get(properties::ScalerCropMaximum); + Rectangle sensor_area; + try { + sensor_area = opt.value(); + } catch(const std::bad_optional_access& exc) { + set_error("get(ScalerCropMaximum) failed"); + return false; + } + + Rectangle crop( + camp->params->roi->x * sensor_area.width, + camp->params->roi->y * sensor_area.height, + camp->params->roi->width * sensor_area.width, + camp->params->roi->height * sensor_area.height); + crop.translateBy(sensor_area.topLeft()); + camp->ctrls->set(controls::ScalerCrop, crop); + } + + int res = camp->camera->start(camp->ctrls.get()); + if (res != 0) { + set_error("Camera.start() failed"); + return false; + } + + camp->ctrls->clear(); + + camp->camera->requestCompleted.connect(on_request_complete); + + for (std::unique_ptr &request : camp->requests) { + int res = camp->camera->queueRequest(request.get()); + if (res != 0) { + set_error("Camera.queueRequest() failed"); + return false; + } + } + + return true; +} + +void camera_reload_params(camera_t *cam, const parameters_t *params) { + CameraPriv *camp = (CameraPriv *)cam; + + std::lock_guard lock(camp->ctrls_mutex); + fill_dynamic_controls(camp->ctrls.get(), params); +} diff --git a/camera.h b/camera.h new file mode 100644 index 0000000..467a7cc --- /dev/null +++ b/camera.h @@ -0,0 +1,31 @@ +#ifndef __CAMERA_H__ +#define __CAMERA_H__ + +#include "parameters.h" + +typedef void camera_t; + +typedef void (*camera_frame_cb)( + uint8_t *mapped_buffer, + int stride, + int height, + int buffer_fd, + uint64_t size, + uint64_t timestamp); + +#ifdef __cplusplus +extern "C" { +#endif + +const char *camera_get_error(); +bool camera_create(const parameters_t *params, camera_frame_cb frame_cb, camera_t **cam); +int camera_get_mode_stride(camera_t *cam); +int camera_get_mode_colorspace(camera_t *cam); +bool camera_start(camera_t *cam); +void camera_reload_params(camera_t *cam, const parameters_t *params); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/encoder.c b/encoder.c new file mode 100644 index 0000000..0bfeb7b --- /dev/null +++ b/encoder.c @@ -0,0 +1,337 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "encoder.h" + +#define DEVICE "/dev/video11" +#define POLL_TIMEOUT_MS 200 + +static char errbuf[256]; + +static void set_error(const char *format, ...) { + va_list args; + va_start(args, format); + vsnprintf(errbuf, 256, format, args); +} + +const char *encoder_get_error() { + return errbuf; +} + +typedef struct { + const parameters_t *params; + int fd; + void **capture_buffers; + int cur_buffer; + encoder_output_cb output_cb; + pthread_t output_thread; + bool ts_initialized; + uint64_t start_ts; +} encoder_priv_t; + +static void *output_thread(void *userdata) { + encoder_priv_t *encp = (encoder_priv_t *)userdata; + + while (true) { + struct pollfd p = { encp->fd, POLLIN, 0 }; + int res = poll(&p, 1, POLL_TIMEOUT_MS); + if (res == -1) { + fprintf(stderr, "output_thread(): poll() failed\n"); + exit(1); + } + + if (p.revents & POLLIN) { + struct v4l2_buffer buf = {0}; + struct v4l2_plane planes[VIDEO_MAX_PLANES] = {0}; + buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + buf.memory = V4L2_MEMORY_DMABUF; + buf.length = 1; + buf.m.planes = planes; + int res = ioctl(encp->fd, VIDIOC_DQBUF, &buf); + if (res != 0) { + fprintf(stderr, "output_thread(): ioctl(VIDIOC_DQBUF) failed\n"); + exit(1); + } + + memset(&buf, 0, sizeof(buf)); + memset(planes, 0, sizeof(planes)); + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + buf.memory = V4L2_MEMORY_MMAP; + buf.length = 1; + buf.m.planes = planes; + res = ioctl(encp->fd, VIDIOC_DQBUF, &buf); + if (res == 0) { + uint64_t ts = ((uint64_t)buf.timestamp.tv_sec * (uint64_t)1000000) + (uint64_t)buf.timestamp.tv_usec; + + if (!encp->ts_initialized) { + encp->ts_initialized = true; + encp->start_ts = ts; + } + + ts -= encp->start_ts; + + const uint8_t *bufmem = (const uint8_t *)encp->capture_buffers[buf.index]; + int bufsize = buf.m.planes[0].bytesused; + encp->output_cb(ts, bufmem, bufsize); + + int index = buf.index; + int length = buf.m.planes[0].length; + + struct v4l2_buffer buf = {0}; + struct v4l2_plane planes[VIDEO_MAX_PLANES] = {0}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = index; + buf.length = 1; + buf.m.planes = planes; + buf.m.planes[0].bytesused = 0; + buf.m.planes[0].length = length; + int res = ioctl(encp->fd, VIDIOC_QBUF, &buf); + if (res < 0) { + fprintf(stderr, "output_thread(): ioctl(VIDIOC_QBUF) failed\n"); + exit(1); + } + } + } + } + + return NULL; +} + +static bool fill_dynamic_params(int fd, const parameters_t *params) { + struct v4l2_control ctrl = {0}; + ctrl.id = V4L2_CID_MPEG_VIDEO_H264_I_PERIOD; + ctrl.value = params->idr_period; + int res = ioctl(fd, VIDIOC_S_CTRL, &ctrl); + if (res != 0) { + set_error("unable to set IDR period"); + return false; + } + + ctrl.id = V4L2_CID_MPEG_VIDEO_BITRATE; + ctrl.value = params->bitrate; + res = ioctl(fd, VIDIOC_S_CTRL, &ctrl); + if (res != 0) { + set_error("unable to set bitrate"); + return false; + } + + return true; +} + +bool encoder_create(const parameters_t *params, int stride, int colorspace, encoder_output_cb output_cb, encoder_t **enc) { + *enc = malloc(sizeof(encoder_priv_t)); + encoder_priv_t *encp = (encoder_priv_t *)(*enc); + memset(encp, 0, sizeof(encoder_priv_t)); + + encp->fd = open(DEVICE, O_RDWR, 0); + if (encp->fd < 0) { + set_error("unable to open device"); + goto failed; + } + + bool res2 = fill_dynamic_params(encp->fd, params); + if (!res2) { + goto failed; + } + + struct v4l2_control ctrl = {0}; + ctrl.id = V4L2_CID_MPEG_VIDEO_H264_PROFILE; + ctrl.value = params->profile; + int res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl); + if (res != 0) { + set_error("unable to set profile"); + goto failed; + } + + ctrl.id = V4L2_CID_MPEG_VIDEO_H264_LEVEL; + ctrl.value = params->level; + res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl); + if (res != 0) { + set_error("unable to set level"); + goto failed; + } + + ctrl.id = V4L2_CID_MPEG_VIDEO_REPEAT_SEQ_HEADER; + ctrl.value = 0; + res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl); + if (res != 0) { + set_error("unable to set REPEAT_SEQ_HEADER"); + goto failed; + } + + struct v4l2_format fmt = {0}; + fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + fmt.fmt.pix_mp.width = params->width; + fmt.fmt.pix_mp.height = params->height; + fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_YUV420; + fmt.fmt.pix_mp.plane_fmt[0].bytesperline = stride; + fmt.fmt.pix_mp.field = V4L2_FIELD_ANY; + fmt.fmt.pix_mp.colorspace = colorspace; + fmt.fmt.pix_mp.num_planes = 1; + res = ioctl(encp->fd, VIDIOC_S_FMT, &fmt); + if (res != 0) { + set_error("unable to set output format"); + goto failed; + } + + memset(&fmt, 0, sizeof(fmt)); + fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + fmt.fmt.pix_mp.width = params->width; + fmt.fmt.pix_mp.height = params->height; + fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_H264; + fmt.fmt.pix_mp.field = V4L2_FIELD_ANY; + fmt.fmt.pix_mp.colorspace = V4L2_COLORSPACE_DEFAULT; + fmt.fmt.pix_mp.num_planes = 1; + fmt.fmt.pix_mp.plane_fmt[0].bytesperline = 0; + fmt.fmt.pix_mp.plane_fmt[0].sizeimage = 512 << 10; + res = ioctl(encp->fd, VIDIOC_S_FMT, &fmt); + if (res != 0) { + set_error("unable to set capture format"); + goto failed; + } + + struct v4l2_streamparm parm = {0}; + parm.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + parm.parm.output.timeperframe.numerator = 1; + parm.parm.output.timeperframe.denominator = params->fps; + res = ioctl(encp->fd, VIDIOC_S_PARM, &parm); + if (res != 0) { + set_error("unable to set fps"); + goto failed; + } + + struct v4l2_requestbuffers reqbufs = {0}; + reqbufs.count = params->buffer_count; + reqbufs.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + reqbufs.memory = V4L2_MEMORY_DMABUF; + res = ioctl(encp->fd, VIDIOC_REQBUFS, &reqbufs); + if (res != 0) { + set_error("unable to set output buffers"); + goto failed; + } + + memset(&reqbufs, 0, sizeof(reqbufs)); + reqbufs.count = params->capture_buffer_count; + reqbufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + reqbufs.memory = V4L2_MEMORY_MMAP; + res = ioctl(encp->fd, VIDIOC_REQBUFS, &reqbufs); + if (res != 0) { + set_error("unable to set capture buffers"); + goto failed; + } + + encp->capture_buffers = malloc(sizeof(void *) * reqbufs.count); + + for (unsigned int i = 0; i < reqbufs.count; i++) { + struct v4l2_plane planes[VIDEO_MAX_PLANES]; + + struct v4l2_buffer buffer = {0}; + buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + buffer.memory = V4L2_MEMORY_MMAP; + buffer.index = i; + buffer.length = 1; + buffer.m.planes = planes; + int res = ioctl(encp->fd, VIDIOC_QUERYBUF, &buffer); + if (res != 0) { + set_error("unable to query buffer"); + goto failed; + } + + encp->capture_buffers[i] = mmap( + 0, + buffer.m.planes[0].length, + PROT_READ | PROT_WRITE, MAP_SHARED, + encp->fd, + buffer.m.planes[0].m.mem_offset); + if (encp->capture_buffers[i] == MAP_FAILED) { + set_error("mmap() failed"); + goto failed; + } + + res = ioctl(encp->fd, VIDIOC_QBUF, &buffer); + if (res != 0) { + set_error("ioctl(VIDIOC_QBUF) failed"); + goto failed; + } + } + + enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + res = ioctl(encp->fd, VIDIOC_STREAMON, &type); + if (res != 0) { + set_error("unable to activate output stream"); + goto failed; + } + + type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + res = ioctl(encp->fd, VIDIOC_STREAMON, &type); + if (res != 0) { + set_error("unable to activate capture stream"); + } + + encp->params = params; + encp->cur_buffer = 0; + encp->output_cb = output_cb; + encp->ts_initialized = false; + + pthread_create(&encp->output_thread, NULL, output_thread, encp); + + return true; + +failed: + if (encp->capture_buffers != NULL) { + free(encp->capture_buffers); + } + if (encp->fd >= 0) { + close(encp->fd); + } + + free(encp); + + return false; +} + +void encoder_encode(encoder_t *enc, int buffer_fd, size_t size, int64_t timestamp_us) { + encoder_priv_t *encp = (encoder_priv_t *)enc; + + int index = encp->cur_buffer++; + encp->cur_buffer %= encp->params->buffer_count; + + struct v4l2_buffer buf = {0}; + struct v4l2_plane planes[VIDEO_MAX_PLANES] = {0}; + buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + buf.index = index; + buf.field = V4L2_FIELD_NONE; + buf.memory = V4L2_MEMORY_DMABUF; + buf.length = 1; + buf.timestamp.tv_sec = timestamp_us / 1000000; + buf.timestamp.tv_usec = timestamp_us % 1000000; + buf.m.planes = planes; + buf.m.planes[0].m.fd = buffer_fd; + buf.m.planes[0].bytesused = size; + buf.m.planes[0].length = size; + int res = ioctl(encp->fd, VIDIOC_QBUF, &buf); + if (res != 0) { + fprintf(stderr, "encoder_encode(): ioctl(VIDIOC_QBUF) failed\n"); + // it happens when the raspberry is under pressure. do not exit. + } +} + +void encoder_reload_params(encoder_t *enc, const parameters_t *params) { + encoder_priv_t *encp = (encoder_priv_t *)enc; + + fill_dynamic_params(encp->fd, params); +} diff --git a/encoder.h b/encoder.h new file mode 100644 index 0000000..eb1e792 --- /dev/null +++ b/encoder.h @@ -0,0 +1,15 @@ +#ifndef __ENCODER_H__ +#define __ENCODER_H__ + +#include "parameters.h" + +typedef void encoder_t; + +typedef void (*encoder_output_cb)(uint64_t ts, const uint8_t *buf, uint64_t size); + +const char *encoder_get_error(); +bool encoder_create(const parameters_t *params, int stride, int colorspace, encoder_output_cb output_cb, encoder_t **enc); +void encoder_encode(encoder_t *enc, int buffer_fd, size_t size, int64_t timestamp_us); +void encoder_reload_params(encoder_t *enc, const parameters_t *params); + +#endif diff --git a/main.c b/main.c new file mode 100644 index 0000000..bb37921 --- /dev/null +++ b/main.c @@ -0,0 +1,116 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "parameters.h" +#include "pipe.h" +#include "camera.h" +#include "text.h" +#include "encoder.h" + +static int pipe_video_fd; +static pthread_mutex_t pipe_video_mutex; +static text_t *text; +static encoder_t *enc; + +static void on_frame( + uint8_t *mapped_buffer, + int stride, + int height, + int buffer_fd, + uint64_t size, + uint64_t timestamp) { + text_draw(text, mapped_buffer, stride, height); + encoder_encode(enc, buffer_fd, size, timestamp); +} + +static void on_encoder_output(uint64_t ts, const uint8_t *buf, uint64_t size) { + pthread_mutex_lock(&pipe_video_mutex); + pipe_write_buf(pipe_video_fd, ts, buf, size); + pthread_mutex_unlock(&pipe_video_mutex); +} + +int main() { + int pipe_conf_fd = atoi(getenv("PIPE_CONF_FD")); + pipe_video_fd = atoi(getenv("PIPE_VIDEO_FD")); + + uint8_t *buf; + uint32_t n = pipe_read(pipe_conf_fd, &buf); + + parameters_t params; + bool ok = parameters_unserialize(¶ms, &buf[1], n-1); + free(buf); + if (!ok) { + pipe_write_error(pipe_video_fd, "parameters_unserialize(): %s", parameters_get_error()); + return 5; + } + + pthread_mutex_init(&pipe_video_mutex, NULL); + pthread_mutex_lock(&pipe_video_mutex); + + camera_t *cam; + ok = camera_create( + ¶ms, + on_frame, + &cam); + if (!ok) { + pipe_write_error(pipe_video_fd, "camera_create(): %s", camera_get_error()); + return 5; + } + + ok = text_create(¶ms, &text); + if (!ok) { + pipe_write_error(pipe_video_fd, "text_create(): %s", text_get_error()); + return 5; + } + + ok = encoder_create( + ¶ms, + camera_get_mode_stride(cam), + camera_get_mode_colorspace(cam), + on_encoder_output, + &enc); + if (!ok) { + pipe_write_error(pipe_video_fd, "encoder_create(): %s", encoder_get_error()); + return 5; + } + + ok = camera_start(cam); + if (!ok) { + pipe_write_error(pipe_video_fd, "camera_start(): %s", camera_get_error()); + return 5; + } + + pipe_write_ready(pipe_video_fd); + pthread_mutex_unlock(&pipe_video_mutex); + + while (true) { + uint8_t *buf; + uint32_t n = pipe_read(pipe_conf_fd, &buf); + + switch (buf[0]) { + case 'e': + return 0; + + case 'c': + { + parameters_t params; + bool ok = parameters_unserialize(¶ms, &buf[1], n-1); + free(buf); + if (!ok) { + printf("skipping reloading parameters since they are invalid: %s\n", parameters_get_error()); + continue; + } + camera_reload_params(cam, ¶ms); + encoder_reload_params(enc, ¶ms); + parameters_destroy(¶ms); + } + } + } + + return 0; +} diff --git a/parameters.c b/parameters.c new file mode 100644 index 0000000..9805d2f --- /dev/null +++ b/parameters.c @@ -0,0 +1,210 @@ +#include +#include +#include +#include +#include + +#include + +#include "base64.h" +#include "parameters.h" + +static char errbuf[256]; + +static void set_error(const char *format, ...) { + va_list args; + va_start(args, format); + vsnprintf(errbuf, 256, format, args); +} + +const char *parameters_get_error() { + return errbuf; +} + +bool parameters_unserialize(parameters_t *params, const uint8_t *buf, size_t buf_size) { + memset(params, 0, sizeof(parameters_t)); + + char *tmp = malloc(buf_size + 1); + memcpy(tmp, buf, buf_size); + tmp[buf_size] = 0x00; + + while (true) { + char *entry = strsep(&tmp, " "); + if (entry == NULL) { + break; + } + + char *key = strsep(&entry, ":"); + char *val = strsep(&entry, ":"); + + if (strcmp(key, "LogLevel") == 0) { + params->log_level = base64_decode(val); + } else if (strcmp(key, "CameraID") == 0) { + params->camera_id = atoi(val); + } else if (strcmp(key, "Width") == 0) { + params->width = atoi(val); + } else if (strcmp(key, "Height") == 0) { + params->height = atoi(val); + } else if (strcmp(key, "HFlip") == 0) { + params->h_flip = (strcmp(val, "1") == 0); + } else if (strcmp(key, "VFlip") == 0) { + params->v_flip = (strcmp(val, "1") == 0); + } else if (strcmp(key, "Brightness") == 0) { + params->brightness = atof(val); + } else if (strcmp(key, "Contrast") == 0) { + params->contrast = atof(val); + } else if (strcmp(key, "Saturation") == 0) { + params->saturation = atof(val); + } else if (strcmp(key, "Sharpness") == 0) { + params->sharpness = atof(val); + } else if (strcmp(key, "Exposure") == 0) { + params->exposure = base64_decode(val); + } else if (strcmp(key, "AWB") == 0) { + params->awb = base64_decode(val); + } else if (strcmp(key, "AWBGainRed") == 0) { + params->awb_gain_red = atof(val); + } else if (strcmp(key, "AWBGainBlue") == 0) { + params->awb_gain_blue = atof(val); + } else if (strcmp(key, "Denoise") == 0) { + params->denoise = base64_decode(val); + } else if (strcmp(key, "Shutter") == 0) { + params->shutter = atoi(val); + } else if (strcmp(key, "Metering") == 0) { + params->metering = base64_decode(val); + } else if (strcmp(key, "Gain") == 0) { + params->gain = atof(val); + } else if (strcmp(key, "EV") == 0) { + params->ev = atof(val); + } else if (strcmp(key, "ROI") == 0) { + char *decoded_val = base64_decode(val); + if (strlen(decoded_val) != 0) { + params->roi = malloc(sizeof(window_t)); + bool ok = window_load(decoded_val, params->roi); + if (!ok) { + set_error("invalid ROI"); + free(decoded_val); + goto failed; + } + } + free(decoded_val); + } else if (strcmp(key, "HDR") == 0) { + params->hdr = (strcmp(val, "1") == 0); + } else if (strcmp(key, "TuningFile") == 0) { + params->tuning_file = base64_decode(val); + } else if (strcmp(key, "Mode") == 0) { + char *decoded_val = base64_decode(val); + if (strlen(decoded_val) != 0) { + params->mode = malloc(sizeof(sensor_mode_t)); + bool ok = sensor_mode_load(decoded_val, params->mode); + if (!ok) { + set_error("invalid sensor mode"); + free(decoded_val); + goto failed; + } + } + free(decoded_val); + } else if (strcmp(key, "FPS") == 0) { + params->fps = atof(val); + } else if (strcmp(key, "IDRPeriod") == 0) { + params->idr_period = atoi(val); + } else if (strcmp(key, "Bitrate") == 0) { + params->bitrate = atoi(val); + } else if (strcmp(key, "Profile") == 0) { + char *decoded_val = base64_decode(val); + if (strcmp(decoded_val, "baseline") == 0) { + params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE; + } else if (strcmp(decoded_val, "main") == 0) { + params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_MAIN; + } else { + params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH; + } + free(decoded_val); + } else if (strcmp(key, "Level") == 0) { + char *decoded_val = base64_decode(val); + if (strcmp(decoded_val, "4.0") == 0) { + params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_0; + } else if (strcmp(decoded_val, "4.1") == 0) { + params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_1; + } else { + params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_2; + } + free(decoded_val); + } else if (strcmp(key, "AfMode") == 0) { + params->af_mode = base64_decode(val); + } else if (strcmp(key, "AfRange") == 0) { + params->af_range = base64_decode(val); + } else if (strcmp(key, "AfSpeed") == 0) { + params->af_speed = base64_decode(val); + } else if (strcmp(key, "LensPosition") == 0) { + params->lens_position = atof(val); + } else if (strcmp(key, "AfWindow") == 0) { + char *decoded_val = base64_decode(val); + if (strlen(decoded_val) != 0) { + params->af_window = malloc(sizeof(window_t)); + bool ok = window_load(decoded_val, params->af_window); + if (!ok) { + set_error("invalid AfWindow"); + free(decoded_val); + goto failed; + } + } + free(decoded_val); + } else if (strcmp(key, "TextOverlayEnable") == 0) { + params->text_overlay_enable = (strcmp(val, "1") == 0); + } else if (strcmp(key, "TextOverlay") == 0) { + params->text_overlay = base64_decode(val); + } + } + + free(tmp); + + params->buffer_count = 6; + params->capture_buffer_count = params->buffer_count * 2; + + return true; + +failed: + free(tmp); + parameters_destroy(params); + + return false; +} + +void parameters_destroy(parameters_t *params) { + if (params->exposure != NULL) { + free(params->exposure); + } + if (params->awb != NULL) { + free(params->awb); + } + if (params->denoise != NULL) { + free(params->denoise); + } + if (params->metering != NULL) { + free(params->metering); + } + if (params->roi != NULL) { + free(params->roi); + } + if (params->tuning_file != NULL) { + free(params->tuning_file); + } + if (params->mode != NULL) { + free(params->mode); + } + if (params->af_mode != NULL) { + free(params->af_mode); + } + if (params->af_range != NULL) { + free(params->af_range); + } + if (params->af_speed != NULL) { + free(params->af_speed); + } + if (params->af_window != NULL) { + free(params->af_window); + } + if (params->text_overlay != NULL) { + free(params->text_overlay); + } +} diff --git a/parameters.h b/parameters.h new file mode 100644 index 0000000..36ced18 --- /dev/null +++ b/parameters.h @@ -0,0 +1,64 @@ +#ifndef __PARAMETERS_H__ +#define __PARAMETERS_H__ + +#include +#include + +#include "window.h" +#include "sensor_mode.h" + +typedef struct { + char *log_level; + unsigned int camera_id; + unsigned int width; + unsigned int height; + bool h_flip; + bool v_flip; + float brightness; + float contrast; + float saturation; + float sharpness; + char *exposure; + char *awb; + float awb_gain_red; + float awb_gain_blue; + char *denoise; + unsigned int shutter; + char *metering; + float gain; + float ev; + window_t *roi; + bool hdr; + char *tuning_file; + sensor_mode_t *mode; + float fps; + unsigned int idr_period; + unsigned int bitrate; + unsigned int profile; + unsigned int level; + char *af_mode; + char *af_range; + char *af_speed; + float lens_position; + window_t *af_window; + bool text_overlay_enable; + char *text_overlay; + + // private + unsigned int buffer_count; + unsigned int capture_buffer_count; +} parameters_t; + +#ifdef __cplusplus +extern "C" { +#endif + +const char *parameters_get_error(); +bool parameters_unserialize(parameters_t *params, const uint8_t *buf, size_t buf_size); +void parameters_destroy(parameters_t *params); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/pipe.c b/pipe.c new file mode 100644 index 0000000..38d2437 --- /dev/null +++ b/pipe.c @@ -0,0 +1,44 @@ +#include +#include +#include +#include +#include +#include + +#include "pipe.h" + +void pipe_write_error(int fd, const char *format, ...) { + char buf[256]; + buf[0] = 'e'; + va_list args; + va_start(args, format); + vsnprintf(&buf[1], 255, format, args); + uint32_t n = strlen(buf); + write(fd, &n, 4); + write(fd, buf, n); +} + +void pipe_write_ready(int fd) { + char buf[] = {'r'}; + uint32_t n = 1; + write(fd, &n, 4); + write(fd, buf, n); +} + +void pipe_write_buf(int fd, uint64_t ts, const uint8_t *buf, uint32_t n) { + char head[] = {'b'}; + n += 1 + sizeof(uint64_t); + write(fd, &n, 4); + write(fd, head, 1); + write(fd, &ts, sizeof(uint64_t)); + write(fd, buf, n - 1 - sizeof(uint64_t)); +} + +uint32_t pipe_read(int fd, uint8_t **pbuf) { + uint32_t n; + read(fd, &n, 4); + + *pbuf = malloc(n); + read(fd, *pbuf, n); + return n; +} diff --git a/pipe.h b/pipe.h new file mode 100644 index 0000000..2663045 --- /dev/null +++ b/pipe.h @@ -0,0 +1,12 @@ +#ifndef __PIPE_H__ +#define __PIPE_H__ + +#include +#include + +void pipe_write_error(int fd, const char *format, ...); +void pipe_write_ready(int fd); +void pipe_write_buf(int fd, uint64_t ts, const uint8_t *buf, uint32_t n); +uint32_t pipe_read(int fd, uint8_t **pbuf); + +#endif diff --git a/sensor_mode.c b/sensor_mode.c new file mode 100644 index 0000000..55932e2 --- /dev/null +++ b/sensor_mode.c @@ -0,0 +1,27 @@ +#include +#include +#include + +#include "sensor_mode.h" + +bool sensor_mode_load(const char *encoded, sensor_mode_t *mode) { + char p; + int n = sscanf(encoded, "%u:%u:%u:%c", &(mode->width), &(mode->height), &(mode->bit_depth), &p); + if (n < 2) { + return false; + } + + if (n < 4) { + mode->packed = true; + } else if (toupper(p) == 'P') { + mode->packed = true; + } else if (toupper(p) == 'U') { + mode->packed = false; + } + + if (n < 3) { + mode->bit_depth = 12; + } + + return true; +} diff --git a/sensor_mode.h b/sensor_mode.h new file mode 100644 index 0000000..6306ee8 --- /dev/null +++ b/sensor_mode.h @@ -0,0 +1,15 @@ +#ifndef __SENSOR_MODE_H__ +#define __SENSOR_MODE_H__ + +#include + +typedef struct { + int width; + int height; + int bit_depth; + bool packed; +} sensor_mode_t; + +bool sensor_mode_load(const char *encoded, sensor_mode_t *mode); + +#endif diff --git a/text.c b/text.c new file mode 100644 index 0000000..4ec8593 --- /dev/null +++ b/text.c @@ -0,0 +1,171 @@ +#include + +#include +#include FT_FREETYPE_H + +#include "text_font.h" +#include "text.h" + +static char errbuf[256]; + +static void set_error(const char *format, ...) { + va_list args; + va_start(args, format); + vsnprintf(errbuf, 256, format, args); +} + +const char *text_get_error() { + return errbuf; +} + +typedef struct { + bool enabled; + char *text_overlay; + FT_Library library; + FT_Face face; +} text_priv_t; + +bool text_create(const parameters_t *params, text_t **text) { + *text = malloc(sizeof(text_priv_t)); + text_priv_t *textp = (text_priv_t *)(*text); + memset(textp, 0, sizeof(text_priv_t)); + + textp->enabled = params->text_overlay_enable; + textp->text_overlay = strdup(params->text_overlay); + + if (textp->enabled) { + int error = FT_Init_FreeType(&textp->library); + if (error) { + set_error("FT_Init_FreeType() failed"); + goto failed; + } + + error = FT_New_Memory_Face( + textp->library, + text_font_ttf, + sizeof(text_font_ttf), + 0, + &textp->face); + if (error) { + set_error("FT_New_Memory_Face() failed"); + goto failed; + } + + error = FT_Set_Pixel_Sizes( + textp->face, + 25, + 25); + if (error) { + set_error("FT_Set_Pixel_Sizes() failed"); + goto failed; + } + } + + return true; + +failed: + free(textp); + + return false; +} + +static void draw_rect(uint8_t *buf, int stride, int height, int x, int y, unsigned int rect_width, unsigned int rect_height) { + uint8_t *Y = buf; + uint8_t *U = Y + stride * height; + uint8_t *V = U + (stride / 2) * (height / 2); + const uint8_t color[3] = {0, 128, 128}; + uint32_t opacity = 45; + + for (unsigned int src_y = 0; src_y < rect_height; src_y++) { + for (unsigned int src_x = 0; src_x < rect_width; src_x++) { + unsigned int dest_x = x + src_x; + unsigned int dest_y = y + src_y; + int i1 = dest_y*stride + dest_x; + int i2 = dest_y/2*stride/2 + dest_x/2; + + Y[i1] = ((color[0] * opacity) + (uint32_t)Y[i1] * (255 - opacity)) / 255; + U[i2] = ((color[1] * opacity) + (uint32_t)U[i2] * (255 - opacity)) / 255; + V[i2] = ((color[2] * opacity) + (uint32_t)V[i2] * (255 - opacity)) / 255; + } + } +} + +static void draw_bitmap(uint8_t *buf, int stride, int height, const FT_Bitmap *bitmap, int x, int y) { + uint8_t *Y = buf; + uint8_t *U = Y + stride * height; + uint8_t *V = U + (stride / 2) * (height / 2); + + for (unsigned int src_y = 0; src_y < bitmap->rows; src_y++) { + for (unsigned int src_x = 0; src_x < bitmap->width; src_x++) { + uint8_t v = bitmap->buffer[src_y*bitmap->pitch + src_x]; + + if (v != 0) { + unsigned int dest_x = x + src_x; + unsigned int dest_y = y + src_y; + int i1 = dest_y*stride + dest_x; + int i2 = dest_y/2*stride/2 + dest_x/2; + uint32_t opacity = (uint32_t)v; + + Y[i1] = (uint8_t)(((uint32_t)v * opacity + (uint32_t)Y[i1] * (255 - opacity)) / 255); + U[i2] = (uint8_t)((128 * opacity + (uint32_t)U[i2] * (255 - opacity)) / 255); + V[i2] = (uint8_t)((128 * opacity + (uint32_t)V[i2] * (255 - opacity)) / 255); + } + } + } +} + +static int get_text_width(FT_Face face, const char *text) { + int ret = 0; + + for (const char *ptr = text; *ptr != 0x00; ptr++) { + int error = FT_Load_Char(face, *ptr, FT_LOAD_RENDER); + if (error) { + continue; + } + + ret += face->glyph->advance.x >> 6; + } + + return ret; +} + +void text_draw(text_t *text, uint8_t *buf, int stride, int height) { + text_priv_t *textp = (text_priv_t *)text; + + if (textp->enabled) { + time_t timer = time(NULL); + struct tm *tm_info = localtime(&timer); + char buffer[255]; + memset(buffer, 0, sizeof(buffer)); + strftime(buffer, 255, textp->text_overlay, tm_info); + + draw_rect( + buf, + stride, + height, + 7, + 7, + get_text_width(textp->face, buffer) + 10, + 34); + + int x = 12; + int y = 33; + + for (const char *ptr = buffer; *ptr != 0x00; ptr++) { + int error = FT_Load_Char(textp->face, *ptr, FT_LOAD_RENDER); + if (error) { + continue; + } + + draw_bitmap( + buf, + stride, + height, + &textp->face->glyph->bitmap, + x + textp->face->glyph->bitmap_left, + y - textp->face->glyph->bitmap_top); + + x += textp->face->glyph->advance.x >> 6; + } + } +} diff --git a/text.h b/text.h new file mode 100644 index 0000000..235bf7a --- /dev/null +++ b/text.h @@ -0,0 +1,15 @@ +#ifndef __TEXT_H__ +#define __TEXT_H__ + +#include +#include + +#include "parameters.h" + +typedef void text_t; + +const char *text_get_error(); +bool text_create(const parameters_t *params, text_t **text); +void text_draw(text_t *text, uint8_t *buf, int stride, int height); + +#endif diff --git a/utils.mk b/utils.mk new file mode 100644 index 0000000..5738bf0 --- /dev/null +++ b/utils.mk @@ -0,0 +1,61 @@ +ALPINE_IMAGE = alpine:3.20 +RPI32_IMAGE = balenalib/raspberry-pi:bullseye-run-20240508 +RPI64_IMAGE = balenalib/raspberrypi3-64:bullseye-run-20240429 + +help: + @echo "usage: make [action]" + @echo "" + @echo "available actions:" + @echo "" + @echo " build build binaries for all platforms" + @echo " build32 build binaries for the 64-bit platform" + @echo " build64 build binaries for the 32-bit platform" + @echo "" + +build: build32 build64 + +define DOCKERFILE_BUILD32 +FROM $(RPI32_IMAGE) AS build +RUN ["cross-build-start"] +RUN apt update && apt install -y --no-install-recommends \ + g++ \ + pkg-config \ + make \ + libcamera-dev \ + libfreetype-dev \ + xxd \ + wget +WORKDIR /s +COPY . . +RUN make -j$$(nproc) +FROM $(ALPINE_IMAGE) +COPY --from=build /s/rpicc /s/rpicc +endef +export DOCKERFILE_BUILD32 + +define DOCKERFILE_BUILD64 +FROM $(RPI64_IMAGE) AS build +RUN ["cross-build-start"] +RUN apt update && apt install -y --no-install-recommends \ + g++ \ + pkg-config \ + make \ + libcamera-dev \ + libfreetype-dev \ + xxd \ + wget +WORKDIR /s +COPY . . +RUN make -j$$(nproc) +FROM $(ALPINE_IMAGE) +COPY --from=build /s/rpicc /s/rpicc +endef +export DOCKERFILE_BUILD64 + +build32: + echo "$$DOCKERFILE_BUILD32" | docker build . -f - -t build32 + docker run --rm -v $(PWD):/o build32 sh -c "mv /s/rpicc /o/rpicc_32" + +build64: + echo "$$DOCKERFILE_BUILD64" | docker build . -f - -t build64 + docker run --rm -v $(PWD):/o build64 sh -c "mv /s/rpicc /o/rpicc_64" diff --git a/window.c b/window.c new file mode 100644 index 0000000..83e6535 --- /dev/null +++ b/window.c @@ -0,0 +1,30 @@ +#include +#include + +#include "window.h" + +bool window_load(const char *encoded, window_t *window) { + float vals[4]; + int i = 0; + char *token = strtok((char *)encoded, ","); + while (token != NULL) { + vals[i] = atof(token); + if (vals[i] < 0 || vals[i] > 1) { + return false; + } + + i++; + token = strtok(NULL, ","); + } + + if (i != 4) { + return false; + } + + window->x = vals[0]; + window->y = vals[1]; + window->width = vals[2]; + window->height = vals[3]; + + return true; +} diff --git a/window.h b/window.h new file mode 100644 index 0000000..f27e470 --- /dev/null +++ b/window.h @@ -0,0 +1,15 @@ +#ifndef __WINDOW_H__ +#define __WINDOW_H__ + +#include + +typedef struct { + float x; + float y; + float width; + float height; +} window_t; + +bool window_load(const char *encoded, window_t *window); + +#endif