diff --git a/camera_example/camera.cpp b/camera_example/camera.cpp index 6877a1d..62e80f5 100644 --- a/camera_example/camera.cpp +++ b/camera_example/camera.cpp @@ -1,402 +1,277 @@ -/* SPDX-License-Identifier: GPL-2.0-or-later */ -/* - * Copyright (C) 2020, Ideas on Board Oy. - * - * A simple libcamera capture example - */ - -#include +#include +#include +#include +#include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include #include +#include +#include #include "event_loop.hpp" -#define TIMEOUT_SEC 3 - using namespace libcamera; -static std::shared_ptr camera; -static EventLoop loop; -/* - * -------------------------------------------------------------------- - * Handle RequestComplete - * - * For each Camera::requestCompleted Signal emitted from the Camera the - * connected Slot is invoked. - * - * The Slot is invoked in the CameraManager's thread, hence one should avoid - * any heavy processing here. The processing of the request shall be re-directed - * to the application's thread instead, so as not to block the CameraManager's - * thread for large amount of time. - * - * The Slot receives the Request as a parameter. - */ +// --- knobs ------------------------------------------------------------------- +// Goal: throughput >= latency >= quality. Tune here and rebuild. -static void processRequest(Request *request); +constexpr int kPort = 5000; +constexpr int kWidth = 640; +constexpr int kHeight = 480; +constexpr int kTargetFps = 30; // 0 = leave to camera defaults +constexpr int kBufferCount = 4; // pipeline depth +constexpr int kJpegQuality = 80; +constexpr int kJpegSubsamp = TJSAMP_420; +constexpr int kSendBufBytes = 1 << 20; // 1 MiB SO_SNDBUF (throughput) -static void requestComplete(Request *request) +// Autofocus: true → continuous AF; false → fixed lens at kLensDiopters +// diopters: 0 = infinity, ~10 = closest. Silently ignored on cameras +// without AF (Pi cam v1/v2). Pi cam v3 supports it. +constexpr bool kAutoFocus = true; +constexpr float kLensDiopters = 0.0f; + +// Pi viewfinder native format: XRGB8888 — DRM byte order, so memory layout +// is B,G,R,X. Feed turbojpeg TJPF_BGRX so it walks 4 B/pixel and ignores X. +// No software color conversion, no row copy. +constexpr int kTjPixelFmt = TJPF_BGRX; + +// --- state ------------------------------------------------------------------- + +namespace { + +std::shared_ptr camera; +EventLoop loop; + +std::atomic client_fd{-1}; + +struct Mapped { uint8_t *data; size_t size; }; +std::unordered_map mapped; + +tjhandle tj_handle = nullptr; +uint8_t *jpeg_buf = nullptr; +unsigned long jpeg_cap = 0; + +// --- net --------------------------------------------------------------------- + +bool sendAll(int fd, const uint8_t *p, size_t n) { - if (request->status() == Request::RequestCancelled) - return; - - loop.callLater(std::bind(&processRequest, request)); + while (n) { + ssize_t r = ::send(fd, p, n, MSG_NOSIGNAL); + if (r <= 0) return false; + p += r; + n -= r; + } + return true; } -static void processRequest(Request *request) +void closeClient() { - std::cout << std::endl - << "Request completed: " << request->toString() << std::endl; - - /* - * When a request has completed, it is populated with a metadata control - * list that allows an application to determine various properties of - * the completed request. This can include the timestamp of the Sensor - * capture, or its gain and exposure values, or properties from the IPA - * such as the state of the 3A algorithms. - * - * ControlValue types have a toString, so to examine each request, print - * all the metadata for inspection. A custom application can parse each - * of these items and process them according to its needs. - */ - const ControlList &requestMetadata = request->metadata(); - for (const auto &ctrl : requestMetadata) { - const ControlId *id = controls::controls.at(ctrl.first); - const ControlValue &value = ctrl.second; - - std::cout << "\t" << id->name() << " = " << value.toString() - << std::endl; - } - - /* - * Each buffer has its own FrameMetadata to describe its state, or the - * usage of each buffer. While in our simple capture we only provide one - * buffer per request, a request can have a buffer for each stream that - * is established when configuring the camera. - * - * This allows a viewfinder and a still image to be processed at the - * same time, or to allow obtaining the RAW capture buffer from the - * sensor along with the image as processed by the ISP. - */ - const Request::BufferMap &buffers = request->buffers(); - for (auto bufferPair : buffers) { - // (Unused) Stream *stream = bufferPair.first; - FrameBuffer *buffer = bufferPair.second; - const FrameMetadata &metadata = buffer->metadata(); - - /* Print some information about the buffer which has completed. */ - std::cout << " seq: " << std::setw(6) << std::setfill('0') << metadata.sequence - << " timestamp: " << metadata.timestamp - << " bytesused: "; - - unsigned int nplane = 0; - for (const FrameMetadata::Plane &plane : metadata.planes()) - { - std::cout << plane.bytesused; - if (++nplane < metadata.planes().size()) - std::cout << "/"; - } - - std::cout << std::endl; - - /* - * Image data can be accessed here, but the FrameBuffer - * must be mapped by the application - */ - } - - /* Re-queue the Request to the camera. */ - request->reuse(Request::ReuseBuffers); - camera->queueRequest(request); + int fd = client_fd.exchange(-1); + if (fd >= 0) ::close(fd); } +void serverThread() +{ + int srv = ::socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0); + int yes = 1; + ::setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(kPort); + addr.sin_addr.s_addr = INADDR_ANY; + + if (::bind(srv, reinterpret_cast(&addr), sizeof(addr)) < 0 + || ::listen(srv, 4) < 0) { + perror("bind/listen"); + ::close(srv); + return; + } + + std::cerr << "tcp://0.0.0.0:" << kPort << " waiting...\n"; + + for (;;) { + int c = ::accept(srv, nullptr, nullptr); + if (c < 0) { + if (errno == EINTR) continue; + break; + } + int one = 1, sndbuf = kSendBufBytes; + ::setsockopt(c, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); + ::setsockopt(c, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf)); + int old = client_fd.exchange(c); + if (old >= 0) ::close(old); + std::cerr << "client connected\n"; + } +} + +// --- camera ------------------------------------------------------------------ + +void processRequest(Request *request); + +void requestComplete(Request *request) +{ + if (request->status() == Request::RequestCancelled) + return; + loop.callLater([request] { processRequest(request); }); +} + +void processRequest(Request *request) +{ + auto it = request->buffers().begin(); + const Stream *stream = it->first; + FrameBuffer *buffer = it->second; + + int fd = client_fd.load(std::memory_order_acquire); + if (fd >= 0 && buffer->metadata().status == FrameMetadata::FrameSuccess) { + const StreamConfiguration &cfg = stream->configuration(); + const Mapped &m = mapped[buffer]; + + unsigned long size = jpeg_cap; + if (tjCompress2(tj_handle, + m.data, cfg.size.width, cfg.stride, cfg.size.height, + kTjPixelFmt, + &jpeg_buf, &size, + kJpegSubsamp, kJpegQuality, + TJFLAG_FASTDCT | TJFLAG_NOREALLOC) == 0) { + uint32_t len = htonl(static_cast(size)); + if (!sendAll(fd, reinterpret_cast(&len), sizeof(len)) + || !sendAll(fd, jpeg_buf, size)) { + std::cerr << "client gone\n"; + closeClient(); + } + } else { + std::cerr << "tjCompress2: " << tjGetErrorStr2(tj_handle) << "\n"; + } + } + + request->reuse(Request::ReuseBuffers); + camera->queueRequest(request); +} + +} // namespace + +// --- main -------------------------------------------------------------------- + int main() { - /* - * -------------------------------------------------------------------- - * Create a Camera Manager. - * - * The Camera Manager is responsible for enumerating all the Camera - * in the system, by associating Pipeline Handlers with media entities - * registered in the system. - * - * The CameraManager provides a list of available Cameras that - * applications can operate on. - * - * When the CameraManager is no longer to be used, it should be deleted. - * We use a unique_ptr here to manage the lifetime automatically during - * the scope of this function. - * - * There can only be a single CameraManager constructed within any - * process space. - */ - std::unique_ptr cm = std::make_unique(); - cm->start(); + std::signal(SIGPIPE, SIG_IGN); + std::thread(serverThread).detach(); - /* - * Just as a test, generate names of the Cameras registered in the - * system, and list them. - */ - for (auto const &camera : cm->cameras()) - std::cout << " - " << camera.get()->id() << std::endl; + auto cm = std::make_unique(); + cm->start(); + if (cm->cameras().empty()) { + std::cerr << "no cameras\n"; + cm->stop(); + return 1; + } - /* - * -------------------------------------------------------------------- - * Camera - * - * Camera are entities created by pipeline handlers, inspecting the - * entities registered in the system and reported to applications - * by the CameraManager. - * - * In general terms, a Camera corresponds to a single image source - * available in the system, such as an image sensor. - * - * Application lock usage of Camera by 'acquiring' them. - * Once done with it, application shall similarly 'release' the Camera. - * - * As an example, use the first available camera in the system after - * making sure that at least one camera is available. - * - * Cameras can be obtained by their ID or their index, to demonstrate - * this, the following code gets the ID of the first camera; then gets - * the camera associated with that ID (which is of course the same as - * cm->cameras()[0]). - */ - if (cm->cameras().empty()) { - std::cout << "No cameras were identified on the system." - << std::endl; - cm->stop(); - return EXIT_FAILURE; - } + camera = cm->get(cm->cameras()[0]->id()); + camera->acquire(); - std::string cameraId = cm->cameras()[0]->id(); - camera = cm->get(cameraId); - camera->acquire(); + auto config = camera->generateConfiguration({ StreamRole::Viewfinder }); + StreamConfiguration &sc = config->at(0); + sc.pixelFormat = formats::XRGB8888; + sc.size = { kWidth, kHeight }; + sc.bufferCount = kBufferCount; - /* - * Stream - * - * Each Camera supports a variable number of Stream. A Stream is - * produced by processing data produced by an image source, usually - * by an ISP. - * - * +-------------------------------------------------------+ - * | Camera | - * | +-----------+ | - * | +--------+ | |------> [ Main output ] | - * | | Image | | | | - * | | |---->| ISP |------> [ Viewfinder ] | - * | | Source | | | | - * | +--------+ | |------> [ Still Capture ] | - * | +-----------+ | - * +-------------------------------------------------------+ - * - * The number and capabilities of the Stream in a Camera are - * a platform dependent property, and it's the pipeline handler - * implementation that has the responsibility of correctly - * report them. - */ + if (config->validate() == CameraConfiguration::Invalid + || sc.pixelFormat != formats::XRGB8888) { + std::cerr << "got " << sc.pixelFormat.toString() + << ", need XRGB8888\n"; + return 1; + } - /* - * -------------------------------------------------------------------- - * Camera Configuration. - * - * Camera configuration is tricky! It boils down to assign resources - * of the system (such as DMA engines, scalers, format converters) to - * the different image streams an application has requested. - * - * Depending on the system characteristics, some combinations of - * sizes, formats and stream usages might or might not be possible. - * - * A Camera produces a CameraConfigration based on a set of intended - * roles for each Stream the application requires. - */ - std::unique_ptr config = - camera->generateConfiguration( { StreamRole::Viewfinder } ); + std::cerr << "stream " << sc.size.toString() + << " " << sc.pixelFormat.toString() + << " stride " << sc.stride + << " buffers " << sc.bufferCount << "\n"; - /* - * The CameraConfiguration contains a StreamConfiguration instance - * for each StreamRole requested by the application, provided - * the Camera can support all of them. - * - * Each StreamConfiguration has default size and format, assigned - * by the Camera depending on the Role the application has requested. - */ - StreamConfiguration &streamConfig = config->at(0); - std::cout << "Default viewfinder configuration is: " - << streamConfig.toString() << std::endl; + if (camera->configure(config.get()) < 0) { + std::cerr << "configure failed\n"; + return 1; + } - /* - * Each StreamConfiguration parameter which is part of a - * CameraConfiguration can be independently modified by the - * application. - * - * In order to validate the modified parameter, the CameraConfiguration - * should be validated -before- the CameraConfiguration gets applied - * to the Camera. - * - * The CameraConfiguration validation process adjusts each - * StreamConfiguration to a valid value. - */ + auto *allocator = new FrameBufferAllocator(camera); + Stream *stream = sc.stream(); + if (allocator->allocate(stream) < 0) { + std::cerr << "alloc failed\n"; + return 1; + } - /* - * Validating a CameraConfiguration -before- applying it will adjust it - * to a valid configuration which is as close as possible to the one - * requested. - */ - config->validate(); - std::cout << "Validated viewfinder configuration is: " - << streamConfig.toString() << std::endl; + for (const std::unique_ptr &b : allocator->buffers(stream)) { + const FrameBuffer::Plane &p = b->planes()[0]; + void *m = mmap(nullptr, p.length, PROT_READ, MAP_SHARED, + p.fd.get(), p.offset); + if (m == MAP_FAILED) { perror("mmap"); return 1; } + mapped[b.get()] = { static_cast(m), p.length }; + } - /* - * Once we have a validated configuration, we can apply it to the - * Camera. - */ - camera->configure(config.get()); + tj_handle = tjInitCompress(); + jpeg_cap = tjBufSize(sc.size.width, sc.size.height, kJpegSubsamp); + jpeg_buf = tjAlloc(jpeg_cap); - /* - * -------------------------------------------------------------------- - * Buffer Allocation - * - * Now that a camera has been configured, it knows all about its - * Streams sizes and formats. The captured images need to be stored in - * framebuffers which can either be provided by the application to the - * library, or allocated in the Camera and exposed to the application - * by libcamera. - * - * An application may decide to allocate framebuffers from elsewhere, - * for example in memory allocated by the display driver that will - * render the captured frames. The application will provide them to - * libcamera by constructing FrameBuffer instances to capture images - * directly into. - * - * Alternatively libcamera can help the application by exporting - * buffers allocated in the Camera using a FrameBufferAllocator - * instance and referencing a configured Camera to determine the - * appropriate buffer size and types to create. - */ - FrameBufferAllocator *allocator = new FrameBufferAllocator(camera); + std::vector> requests; + for (const std::unique_ptr &b : allocator->buffers(stream)) { + std::unique_ptr r = camera->createRequest(); + if (!r || r->addBuffer(stream, b.get()) < 0) { + std::cerr << "request setup failed\n"; + return 1; + } + requests.push_back(std::move(r)); + } - for (StreamConfiguration &cfg : *config) { - int ret = allocator->allocate(cfg.stream()); - if (ret < 0) { - std::cerr << "Can't allocate buffers" << std::endl; - return EXIT_FAILURE; - } + // Initial controls: AF + frame-rate cap. + ControlList ctrls(camera->controls()); + const ControlInfoMap &caps = camera->controls(); - size_t allocated = allocator->buffers(cfg.stream()).size(); - std::cout << "Allocated " << allocated << " buffers for stream" << std::endl; - } + if (caps.count(&controls::AfMode)) { + if (kAutoFocus) { + ctrls.set(controls::AfMode, controls::AfModeContinuous); + } else { + ctrls.set(controls::AfMode, controls::AfModeManual); + ctrls.set(controls::LensPosition, kLensDiopters); + } + } + if (kTargetFps > 0) { + int64_t dur = 1000000 / kTargetFps; + ctrls.set(controls::FrameDurationLimits, + Span({ dur, dur })); + } - /* - * -------------------------------------------------------------------- - * Frame Capture - * - * libcamera frames capture model is based on the 'Request' concept. - * For each frame a Request has to be queued to the Camera. - * - * A Request refers to (at least one) Stream for which a Buffer that - * will be filled with image data shall be added to the Request. - * - * A Request is associated with a list of Controls, which are tunable - * parameters (similar to v4l2_controls) that have to be applied to - * the image. - * - * Once a request completes, all its buffers will contain image data - * that applications can access and for each of them a list of metadata - * properties that reports the capture parameters applied to the image. - */ - Stream *stream = streamConfig.stream(); - const std::vector> &buffers = allocator->buffers(stream); - std::vector> requests; - for (unsigned int i = 0; i < buffers.size(); ++i) { - std::unique_ptr request = camera->createRequest(); - if (!request) - { - std::cerr << "Can't create request" << std::endl; - return EXIT_FAILURE; - } + camera->requestCompleted.connect(requestComplete); + if (camera->start(&ctrls) < 0) { + std::cerr << "camera start failed\n"; + return 1; + } + for (std::unique_ptr &r : requests) + camera->queueRequest(r.get()); - const std::unique_ptr &buffer = buffers[i]; - int ret = request->addBuffer(stream, buffer.get()); - if (ret < 0) - { - std::cerr << "Can't set buffer for request" - << std::endl; - return EXIT_FAILURE; - } + std::cerr << "streaming, Ctrl+C to stop\n"; + loop.exec(); - /* - * Controls can be added to a request on a per frame basis. - */ - ControlList &controls = request->controls(); - controls.set(controls::Brightness, 0.5); - - requests.push_back(std::move(request)); - } - - /* - * -------------------------------------------------------------------- - * Signal&Slots - * - * libcamera uses a Signal&Slot based system to connect events to - * callback operations meant to handle them, inspired by the QT graphic - * toolkit. - * - * Signals are events 'emitted' by a class instance. - * Slots are callbacks that can be 'connected' to a Signal. - * - * A Camera exposes Signals, to report the completion of a Request and - * the completion of a Buffer part of a Request to support partial - * Request completions. - * - * In order to receive the notification for request completions, - * applications shall connecte a Slot to the Camera 'requestCompleted' - * Signal before the camera is started. - */ - camera->requestCompleted.connect(requestComplete); - - /* - * -------------------------------------------------------------------- - * Start Capture - * - * In order to capture frames the Camera has to be started and - * Request queued to it. Enough Request to fill the Camera pipeline - * depth have to be queued before the Camera start delivering frames. - * - * For each delivered frame, the Slot connected to the - * Camera::requestCompleted Signal is called. - */ - camera->start(); - for (std::unique_ptr &request : requests) - camera->queueRequest(request.get()); - - /* - * -------------------------------------------------------------------- - * Run an EventLoop - * - * In order to dispatch events received from the video devices, such - * as buffer completions, an event loop has to be run. - */ - loop.timeout(TIMEOUT_SEC); - int ret = loop.exec(); - std::cout << "Capture ran for " << TIMEOUT_SEC << " seconds and " - << "stopped with exit status: " << ret << std::endl; - - /* - * -------------------------------------------------------------------- - * Clean Up - * - * Stop the Camera, release resources and stop the CameraManager. - * libcamera has now released all resources it owned. - */ - camera->stop(); - allocator->free(stream); - delete allocator; - camera->release(); - camera.reset(); - cm->stop(); - - return EXIT_SUCCESS; + camera->stop(); + camera->requestCompleted.disconnect(); + for (auto &kv : mapped) munmap(kv.second.data, kv.second.size); + allocator->free(stream); + delete allocator; + camera->release(); + camera.reset(); + cm->stop(); + if (jpeg_buf) tjFree(jpeg_buf); + if (tj_handle) tjDestroy(tj_handle); + closeClient(); + return 0; }