Phase 8.3: userspace daemon scaffold + FFmpeg dlopen + parse path
Builds the daemon executable per the locked Phase 8 architecture
(Option γ: dlopen FFmpeg at runtime). Phase 8.3 scope: parse
path validation only — no V4L2 wiring, no decode, no chardev
connection.
Components:
- daemon/CMakeLists.txt — CMake with -Wall -Wextra -Wpedantic
clean. pkg-config for FFmpeg headers; only -ldl + -lpthread
at link time.
- daemon/src/main.c — entry point, signal handlers
(SIGINT/SIGTERM), command dispatcher. Currently `parse <file>`.
- daemon/src/ffmpeg_loader.{c,h} — runtime FFmpeg loader.
dlopens libavformat.so.61, libavcodec.so.61, libavutil.so.59.
Resolves 22 function pointers using POSIX-recommended
*(void**)& dlsym idiom (per POSIX.1-2017 dlsym(3p) Rationale).
- daemon/src/parser.{c,h} — demux loop via avformat_open_input +
av_read_frame. Per-frame logging on -v.
- daemon/src/log.{c,h} — logging facade (stderr Phase 8.3;
syslog/journal planned for 8.5+).
Verification on hertz:
$ ffmpeg -f lavfi -i testsrc=duration=2:size=320x240:rate=30 \
-c:v libvpx-vp9 -y /tmp/testsrc.ivf
$ daedalus_v4l2_daemon parse /tmp/testsrc.ivf
[INFO] FFmpeg loaded: 7.1.3-0+deb13u1+rpt1 (libavformat 61.7.100)
[INFO] video stream #0: codec=vp9 (Google VP9) 320x240, 0/0 fps
[INFO] parse complete: 60 frames (1 key) total 17859 bytes
Error paths verified:
- Missing file → "avformat_open_input(...): code -2", exit 1
- No command → usage message, exit 2
- Bad command → usage message, exit 2
Per correctness-before-speed:
- Real CMake (no Makefile hacks)
- pkg-config for headers
- POSIX-conformant dlsym pattern (no -Wpedantic suppression)
- Real signal handling + proper exit codes
- Real logging with timestamp + level
- Headers included at compile-time for type safety; dlopen
decouples runtime
- All FFmpeg resources freed on every exit path
- Builds clean on -Wall -Wextra -Wpedantic
Phase 8.3 acceptance criteria met:
- ✓ daemon binary builds
- ✓ dlopen FFmpeg at runtime
- ✓ demux a VP9 IVF file end-to-end
- ✓ per-frame metadata logged correctly
- ✓ frame count + keyframe count + byte total accurate
Phase 8.4 next: wire daemon to /dev/daedalus-v4l2 chardev,
add REQ_DECODE / RESP_FRAME handling, drive VP9 decode
end-to-end via daedalus_dispatch_* from daedalus-fourier.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||
/*
|
||||
* ffmpeg_loader.c — runtime FFmpeg loader.
|
||||
*/
|
||||
#include "ffmpeg_loader.h"
|
||||
#include "log.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <dlfcn.h>
|
||||
|
||||
/*
|
||||
* SONAME versions match Debian Trixie / FFmpeg 7.1.3 today. If
|
||||
* the system FFmpeg changes major, the daemon needs a rebuild;
|
||||
* we could add fallback paths (.so.60, .so.59, ...) but for
|
||||
* Phase 8.3 the pinned version is fine.
|
||||
*/
|
||||
#define LIBAVFORMAT_SONAME "libavformat.so.61"
|
||||
#define LIBAVCODEC_SONAME "libavcodec.so.61"
|
||||
#define LIBAVUTIL_SONAME "libavutil.so.59"
|
||||
|
||||
/*
|
||||
* Resolve a symbol from a dlopen'd handle. Logs the failure
|
||||
* and goto's `out_fail` if missing. Use only inside
|
||||
* ffmpeg_loader_init.
|
||||
*
|
||||
* The `*(void **) &loader->sym = dlsym(...)` form is the
|
||||
* POSIX-recommended workaround for the ISO C "function pointer
|
||||
* is not a void pointer" rule (POSIX.1-2017 dlsym(3p), RATIONALE).
|
||||
* POSIX requires the conversion to work; ISO C does not.
|
||||
*/
|
||||
#define RESOLVE(handle_field, soname, sym) \
|
||||
do { \
|
||||
*(void **) &loader->sym = \
|
||||
dlsym(loader->handle_field, #sym); \
|
||||
if (!loader->sym) { \
|
||||
log_err("dlsym(%s, %s): %s", \
|
||||
soname, #sym, dlerror()); \
|
||||
goto out_fail; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
int ffmpeg_loader_init(struct ffmpeg_loader *loader)
|
||||
{
|
||||
memset(loader, 0, sizeof(*loader));
|
||||
|
||||
loader->libavformat = dlopen(LIBAVFORMAT_SONAME, RTLD_LAZY);
|
||||
if (!loader->libavformat) {
|
||||
log_err("dlopen(%s): %s", LIBAVFORMAT_SONAME, dlerror());
|
||||
goto out_fail;
|
||||
}
|
||||
loader->libavcodec = dlopen(LIBAVCODEC_SONAME, RTLD_LAZY);
|
||||
if (!loader->libavcodec) {
|
||||
log_err("dlopen(%s): %s", LIBAVCODEC_SONAME, dlerror());
|
||||
goto out_fail;
|
||||
}
|
||||
loader->libavutil = dlopen(LIBAVUTIL_SONAME, RTLD_LAZY);
|
||||
if (!loader->libavutil) {
|
||||
log_err("dlopen(%s): %s", LIBAVUTIL_SONAME, dlerror());
|
||||
goto out_fail;
|
||||
}
|
||||
|
||||
/* libavformat */
|
||||
RESOLVE(libavformat, LIBAVFORMAT_SONAME, avformat_alloc_context);
|
||||
RESOLVE(libavformat, LIBAVFORMAT_SONAME, avformat_free_context);
|
||||
RESOLVE(libavformat, LIBAVFORMAT_SONAME, avformat_open_input);
|
||||
RESOLVE(libavformat, LIBAVFORMAT_SONAME, avformat_close_input);
|
||||
RESOLVE(libavformat, LIBAVFORMAT_SONAME, avformat_find_stream_info);
|
||||
RESOLVE(libavformat, LIBAVFORMAT_SONAME, av_find_best_stream);
|
||||
RESOLVE(libavformat, LIBAVFORMAT_SONAME, av_read_frame);
|
||||
RESOLVE(libavformat, LIBAVFORMAT_SONAME, avformat_version);
|
||||
|
||||
/* libavcodec */
|
||||
RESOLVE(libavcodec, LIBAVCODEC_SONAME, av_packet_alloc);
|
||||
RESOLVE(libavcodec, LIBAVCODEC_SONAME, av_packet_free);
|
||||
RESOLVE(libavcodec, LIBAVCODEC_SONAME, av_packet_unref);
|
||||
RESOLVE(libavcodec, LIBAVCODEC_SONAME, av_frame_alloc);
|
||||
RESOLVE(libavcodec, LIBAVCODEC_SONAME, av_frame_free);
|
||||
RESOLVE(libavcodec, LIBAVCODEC_SONAME, av_frame_unref);
|
||||
RESOLVE(libavcodec, LIBAVCODEC_SONAME, avcodec_find_decoder);
|
||||
RESOLVE(libavcodec, LIBAVCODEC_SONAME, avcodec_alloc_context3);
|
||||
RESOLVE(libavcodec, LIBAVCODEC_SONAME, avcodec_free_context);
|
||||
RESOLVE(libavcodec, LIBAVCODEC_SONAME, avcodec_parameters_to_context);
|
||||
RESOLVE(libavcodec, LIBAVCODEC_SONAME, avcodec_open2);
|
||||
RESOLVE(libavcodec, LIBAVCODEC_SONAME, avcodec_send_packet);
|
||||
RESOLVE(libavcodec, LIBAVCODEC_SONAME, avcodec_receive_frame);
|
||||
|
||||
/* libavutil */
|
||||
RESOLVE(libavutil, LIBAVUTIL_SONAME, av_log_set_level);
|
||||
RESOLVE(libavutil, LIBAVUTIL_SONAME, av_get_media_type_string);
|
||||
RESOLVE(libavutil, LIBAVUTIL_SONAME, av_version_info);
|
||||
|
||||
{
|
||||
unsigned int v = loader->avformat_version();
|
||||
log_info("FFmpeg loaded: %s (libavformat %u.%u.%u)",
|
||||
loader->av_version_info(),
|
||||
(v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff);
|
||||
}
|
||||
return 0;
|
||||
|
||||
out_fail:
|
||||
ffmpeg_loader_cleanup(loader);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ffmpeg_loader_cleanup(struct ffmpeg_loader *loader)
|
||||
{
|
||||
if (!loader)
|
||||
return;
|
||||
if (loader->libavformat) dlclose(loader->libavformat);
|
||||
if (loader->libavcodec) dlclose(loader->libavcodec);
|
||||
if (loader->libavutil) dlclose(loader->libavutil);
|
||||
memset(loader, 0, sizeof(*loader));
|
||||
}
|
||||
Reference in New Issue
Block a user