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