SkyLok/camera_example/camera.cpp
2026-05-21 15:23:13 +02:00

279 lines
8.4 KiB
C++

#include <atomic>
#include <csignal>
#include <cstdint>
#include <errno.h>
#include <iostream>
#include <memory>
#include <thread>
#include <unordered_map>
#include <vector>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <unistd.h>
#include <turbojpeg.h>
#include <libcamera/libcamera.h>
#include <libcamera/control_ids.h>
#include <libcamera/formats.h>
#include "event_loop.hpp"
using namespace libcamera;
// --- knobs -------------------------------------------------------------------
// Goal: throughput >= latency >= quality. Tune here and rebuild.
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)
// 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> camera;
EventLoop loop;
std::atomic<int> client_fd{-1};
struct Mapped { uint8_t *data; size_t size; };
std::unordered_map<FrameBuffer *, Mapped> 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)
{
while (n) {
ssize_t r = ::send(fd, p, n, MSG_NOSIGNAL);
if (r <= 0) return false;
p += r;
n -= r;
}
return true;
}
void closeClient()
{
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<sockaddr *>(&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<uint32_t>(size));
if (!sendAll(fd, reinterpret_cast<uint8_t *>(&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()
{
std::signal(SIGPIPE, SIG_IGN);
std::signal(SIGINT, [](int) { loop.exit(0); });
std::thread(serverThread).detach();
auto cm = std::make_unique<CameraManager>();
cm->start();
if (cm->cameras().empty()) {
std::cerr << "no cameras\n";
cm->stop();
return 1;
}
camera = cm->get(cm->cameras()[0]->id());
camera->acquire();
auto config = camera->generateConfiguration({ StreamRole::Viewfinder });
StreamConfiguration &sc = config->at(0);
sc.pixelFormat = formats::XRGB8888;
sc.size = { kWidth, kHeight };
sc.bufferCount = kBufferCount;
if (config->validate() == CameraConfiguration::Invalid
|| sc.pixelFormat != formats::XRGB8888) {
std::cerr << "got " << sc.pixelFormat.toString()
<< ", need XRGB8888\n";
return 1;
}
std::cerr << "stream " << sc.size.toString()
<< " " << sc.pixelFormat.toString()
<< " stride " << sc.stride
<< " buffers " << sc.bufferCount << "\n";
if (camera->configure(config.get()) < 0) {
std::cerr << "configure failed\n";
return 1;
}
auto *allocator = new FrameBufferAllocator(camera);
Stream *stream = sc.stream();
if (allocator->allocate(stream) < 0) {
std::cerr << "alloc failed\n";
return 1;
}
for (const std::unique_ptr<FrameBuffer> &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<uint8_t *>(m), p.length };
}
tj_handle = tjInitCompress();
jpeg_cap = tjBufSize(sc.size.width, sc.size.height, kJpegSubsamp);
jpeg_buf = tjAlloc(jpeg_cap);
std::vector<std::unique_ptr<Request>> requests;
for (const std::unique_ptr<FrameBuffer> &b : allocator->buffers(stream)) {
std::unique_ptr<Request> 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));
}
// Initial controls: AF + frame-rate cap.
ControlList ctrls(camera->controls());
const ControlInfoMap &caps = camera->controls();
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<const int64_t, 2>({ dur, dur }));
}
camera->requestCompleted.connect(requestComplete);
if (camera->start(&ctrls) < 0) {
std::cerr << "camera start failed\n";
return 1;
}
for (std::unique_ptr<Request> &r : requests)
camera->queueRequest(r.get());
std::cerr << "streaming, Ctrl+C to stop\n";
loop.exec();
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;
}