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,52 @@
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
#
|
||||
# daedalus-v4l2 userspace daemon — CMake build.
|
||||
#
|
||||
# Notes:
|
||||
# - FFmpeg is dlopen'd at runtime (Option γ), so we link only
|
||||
# -ldl + -lpthread. Headers from libavformat-dev /
|
||||
# libavcodec-dev / libavutil-dev are used at compile time
|
||||
# for struct definitions.
|
||||
# - Strict warnings enforced.
|
||||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(daedalus_v4l2_daemon C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Release)
|
||||
endif()
|
||||
|
||||
add_compile_options(-Wall -Wextra -Wpedantic
|
||||
-Wno-unused-parameter)
|
||||
|
||||
# FFmpeg headers (we dlopen the libs but include the headers for
|
||||
# struct definitions). pkg-config is the canonical way.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET
|
||||
libavformat libavcodec libavutil)
|
||||
|
||||
add_executable(daedalus_v4l2_daemon
|
||||
src/main.c
|
||||
src/ffmpeg_loader.c
|
||||
src/log.c
|
||||
src/parser.c
|
||||
)
|
||||
|
||||
target_include_directories(daedalus_v4l2_daemon
|
||||
PRIVATE
|
||||
src
|
||||
${FFMPEG_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
# dl for dlopen, pthread for future threading work.
|
||||
target_link_libraries(daedalus_v4l2_daemon
|
||||
PRIVATE
|
||||
dl
|
||||
pthread
|
||||
)
|
||||
|
||||
install(TARGETS daedalus_v4l2_daemon
|
||||
RUNTIME DESTINATION /usr/local/bin)
|
||||
@@ -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));
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||
/*
|
||||
* ffmpeg_loader.h — runtime FFmpeg loader for daedalus-v4l2 daemon.
|
||||
*
|
||||
* Loads libavformat.so + libavcodec.so + libavutil.so via dlopen
|
||||
* at startup, resolves the function-pointer subset we use, and
|
||||
* exposes them through a stable struct. This keeps the daemon
|
||||
* link-clean from FFmpeg (the system version can change without
|
||||
* rebuilding the daemon) and matches Phase 8 architecture
|
||||
* decision "Option γ: dlopen FFmpeg at runtime".
|
||||
*
|
||||
* Usage:
|
||||
* if (ffmpeg_loader_init(&loader)) bail();
|
||||
* loader.av_log_set_level(AV_LOG_QUIET);
|
||||
* ... use loader.avformat_open_input(...) etc.
|
||||
* ffmpeg_loader_cleanup(&loader);
|
||||
*/
|
||||
#ifndef DAEDALUS_V4L2_FFMPEG_LOADER_H
|
||||
#define DAEDALUS_V4L2_FFMPEG_LOADER_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* Pull in the FFmpeg headers for type definitions. We dlopen
|
||||
* the libraries at runtime (no link dependency), but the
|
||||
* function pointer signatures need the real enum + struct
|
||||
* definitions to be type-safe. Including these headers makes
|
||||
* the daemon build-time-coupled to a specific FFmpeg minor
|
||||
* version (matched in CMakeLists via pkg_check_modules) but
|
||||
* runtime-decoupled (we can load a different patch version's
|
||||
* .so file as long as the ABI is compatible).
|
||||
*/
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/avutil.h>
|
||||
|
||||
/**
|
||||
* struct ffmpeg_loader - resolved FFmpeg API entry points
|
||||
* @libavformat: dlopen handle (close in cleanup)
|
||||
* @libavcodec: dlopen handle (close in cleanup)
|
||||
* @libavutil: dlopen handle (close in cleanup)
|
||||
*
|
||||
* All function pointers are NULL until ffmpeg_loader_init().
|
||||
*/
|
||||
struct ffmpeg_loader {
|
||||
void *libavformat;
|
||||
void *libavcodec;
|
||||
void *libavutil;
|
||||
|
||||
/* libavformat */
|
||||
struct AVFormatContext *(*avformat_alloc_context)(void);
|
||||
void (*avformat_free_context)(struct AVFormatContext *);
|
||||
int (*avformat_open_input)(struct AVFormatContext **,
|
||||
const char *, const struct AVInputFormat *,
|
||||
struct AVDictionary **);
|
||||
void (*avformat_close_input)(struct AVFormatContext **);
|
||||
int (*avformat_find_stream_info)(struct AVFormatContext *,
|
||||
struct AVDictionary **);
|
||||
int (*av_find_best_stream)(struct AVFormatContext *,
|
||||
enum AVMediaType, int, int,
|
||||
const struct AVCodec **, int);
|
||||
int (*av_read_frame)(struct AVFormatContext *,
|
||||
struct AVPacket *);
|
||||
unsigned int (*avformat_version)(void);
|
||||
|
||||
/* libavcodec */
|
||||
struct AVPacket *(*av_packet_alloc)(void);
|
||||
void (*av_packet_free)(struct AVPacket **);
|
||||
void (*av_packet_unref)(struct AVPacket *);
|
||||
struct AVFrame *(*av_frame_alloc)(void);
|
||||
void (*av_frame_free)(struct AVFrame **);
|
||||
void (*av_frame_unref)(struct AVFrame *);
|
||||
const struct AVCodec *(*avcodec_find_decoder)(enum AVCodecID);
|
||||
struct AVCodecContext *(*avcodec_alloc_context3)(const struct AVCodec *);
|
||||
void (*avcodec_free_context)(struct AVCodecContext **);
|
||||
int (*avcodec_parameters_to_context)(struct AVCodecContext *,
|
||||
const struct AVCodecParameters *);
|
||||
int (*avcodec_open2)(struct AVCodecContext *,
|
||||
const struct AVCodec *, struct AVDictionary **);
|
||||
int (*avcodec_send_packet)(struct AVCodecContext *,
|
||||
const struct AVPacket *);
|
||||
int (*avcodec_receive_frame)(struct AVCodecContext *,
|
||||
struct AVFrame *);
|
||||
|
||||
/* libavutil */
|
||||
void (*av_log_set_level)(int);
|
||||
const char *(*av_get_media_type_string)(enum AVMediaType);
|
||||
const char *(*av_version_info)(void);
|
||||
};
|
||||
|
||||
/**
|
||||
* ffmpeg_loader_init - dlopen + resolve all needed symbols
|
||||
*
|
||||
* Return: 0 on success, -1 on any failure (errno-style; logged
|
||||
* to stderr via the daemon's logging facility).
|
||||
*/
|
||||
int ffmpeg_loader_init(struct ffmpeg_loader *loader);
|
||||
|
||||
/**
|
||||
* ffmpeg_loader_cleanup - dlclose handles + zero the struct
|
||||
*/
|
||||
void ffmpeg_loader_cleanup(struct ffmpeg_loader *loader);
|
||||
|
||||
#endif /* DAEDALUS_V4L2_FFMPEG_LOADER_H */
|
||||
@@ -0,0 +1,70 @@
|
||||
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||
/*
|
||||
* log.c — minimal logging facade.
|
||||
*
|
||||
* Phase 8.3: stderr-only. Phase 8.5+ will add syslog/journal
|
||||
* routing when the daemon runs under systemd; the API surface
|
||||
* (log_init/log_err/...) stays unchanged.
|
||||
*/
|
||||
#include "log.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
#include <time.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int g_min_level = LOG_INFO;
|
||||
|
||||
void log_init(int min_level)
|
||||
{
|
||||
g_min_level = min_level;
|
||||
}
|
||||
|
||||
void log_cleanup(void)
|
||||
{
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
static const char *level_str(int level)
|
||||
{
|
||||
switch (level) {
|
||||
case LOG_ERR: return "ERR ";
|
||||
case LOG_WARNING: return "WARN";
|
||||
case LOG_INFO: return "INFO";
|
||||
case LOG_DEBUG: return "DBG ";
|
||||
default: return " ";
|
||||
}
|
||||
}
|
||||
|
||||
static void log_emit(int level, const char *fmt, va_list ap)
|
||||
{
|
||||
if (level > g_min_level)
|
||||
return;
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
struct tm tm;
|
||||
localtime_r(&ts.tv_sec, &tm);
|
||||
char tbuf[32];
|
||||
snprintf(tbuf, sizeof(tbuf),
|
||||
"%04d-%02d-%02d %02d:%02d:%02d.%03ld",
|
||||
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
||||
tm.tm_hour, tm.tm_min, tm.tm_sec,
|
||||
ts.tv_nsec / 1000000);
|
||||
fprintf(stderr, "[%s %s] ", tbuf, level_str(level));
|
||||
vfprintf(stderr, fmt, ap);
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
|
||||
#define LOG_FN(name, lvl) \
|
||||
void name(const char *fmt, ...) \
|
||||
{ \
|
||||
va_list ap; va_start(ap, fmt); \
|
||||
log_emit(lvl, fmt, ap); \
|
||||
va_end(ap); \
|
||||
}
|
||||
|
||||
LOG_FN(log_err, LOG_ERR)
|
||||
LOG_FN(log_warn, LOG_WARNING)
|
||||
LOG_FN(log_info, LOG_INFO)
|
||||
LOG_FN(log_debug, LOG_DEBUG)
|
||||
@@ -0,0 +1,29 @@
|
||||
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||
/*
|
||||
* log.h — minimal logging facade for daedalus-v4l2 daemon.
|
||||
*
|
||||
* Routes to stderr in interactive mode; switches to syslog when
|
||||
* the daemon detects it's running under systemd / not on a tty.
|
||||
* Levels match syslog convention (LOG_ERR / LOG_WARNING /
|
||||
* LOG_INFO / LOG_DEBUG).
|
||||
*
|
||||
* Use:
|
||||
* log_init(LOG_INFO); // raise minimum
|
||||
* log_info("daemon up: pid=%d", getpid());
|
||||
* log_err("open(%s): %s", path, strerror(errno));
|
||||
* log_cleanup();
|
||||
*/
|
||||
#ifndef DAEDALUS_V4L2_LOG_H
|
||||
#define DAEDALUS_V4L2_LOG_H
|
||||
|
||||
#include <syslog.h>
|
||||
|
||||
void log_init(int min_level);
|
||||
void log_cleanup(void);
|
||||
|
||||
void log_err (const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
void log_warn (const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
void log_info (const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
void log_debug (const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
||||
|
||||
#endif /* DAEDALUS_V4L2_LOG_H */
|
||||
@@ -0,0 +1,115 @@
|
||||
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||
/*
|
||||
* main.c — daedalus-v4l2 userspace daemon entry point.
|
||||
*
|
||||
* Phase 8.3 mode of operation: CLI tool.
|
||||
* daedalus_v4l2_daemon parse <file>
|
||||
*
|
||||
* Phase 8.4+ adds the daemon mode that opens the chardev
|
||||
* /dev/daedalus-v4l2 and services REQ_DECODE etc.
|
||||
*/
|
||||
#include "ffmpeg_loader.h"
|
||||
#include "parser.h"
|
||||
#include "log.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include <libavutil/log.h>
|
||||
|
||||
static volatile sig_atomic_t g_terminate = 0;
|
||||
|
||||
static void on_signal(int sig)
|
||||
{
|
||||
(void) sig;
|
||||
g_terminate = 1;
|
||||
}
|
||||
|
||||
static int install_signal_handlers(void)
|
||||
{
|
||||
struct sigaction sa = { 0 };
|
||||
sa.sa_handler = on_signal;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
if (sigaction(SIGINT, &sa, NULL) < 0 ||
|
||||
sigaction(SIGTERM, &sa, NULL) < 0) {
|
||||
log_err("sigaction: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_parse(struct ffmpeg_loader *fm, int argc, char **argv)
|
||||
{
|
||||
if (argc < 1) {
|
||||
fprintf(stderr, "usage: parse <path-to-media-file>\n");
|
||||
return 2;
|
||||
}
|
||||
int rc = daedalus_parse_file(fm, argv[0]);
|
||||
return rc < 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
static void usage(const char *progname)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"usage: %s <command> [args]\n\n"
|
||||
"commands:\n"
|
||||
" parse <file> Phase 8.3: demux+enumerate frames\n"
|
||||
"\n"
|
||||
"options:\n"
|
||||
" -v, --verbose enable debug logging\n"
|
||||
" -h, --help this message\n",
|
||||
progname);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
int verbose = 0;
|
||||
int i;
|
||||
|
||||
for (i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "-v") == 0 ||
|
||||
strcmp(argv[i], "--verbose") == 0)
|
||||
verbose = 1;
|
||||
else if (strcmp(argv[i], "-h") == 0 ||
|
||||
strcmp(argv[i], "--help") == 0) {
|
||||
usage(argv[0]);
|
||||
return 0;
|
||||
} else {
|
||||
break; /* first non-option = command */
|
||||
}
|
||||
}
|
||||
if (i >= argc) {
|
||||
usage(argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
log_init(verbose ? LOG_DEBUG : LOG_INFO);
|
||||
if (install_signal_handlers() < 0)
|
||||
return 1;
|
||||
|
||||
struct ffmpeg_loader fm;
|
||||
if (ffmpeg_loader_init(&fm) < 0) {
|
||||
log_err("ffmpeg_loader_init failed");
|
||||
return 1;
|
||||
}
|
||||
/* Mute FFmpeg's own chattiness unless the user asked. */
|
||||
fm.av_log_set_level(verbose ? AV_LOG_INFO : AV_LOG_WARNING);
|
||||
|
||||
int rc;
|
||||
const char *cmd = argv[i++];
|
||||
if (strcmp(cmd, "parse") == 0) {
|
||||
rc = cmd_parse(&fm, argc - i, argv + i);
|
||||
} else {
|
||||
fprintf(stderr, "unknown command: %s\n", cmd);
|
||||
usage(argv[0]);
|
||||
rc = 2;
|
||||
}
|
||||
|
||||
ffmpeg_loader_cleanup(&fm);
|
||||
log_cleanup();
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||
/*
|
||||
* parser.c — Phase 8.3 parse-path tool.
|
||||
*
|
||||
* Opens a media file via FFmpeg's avformat demuxer (dlopen'd
|
||||
* by ffmpeg_loader), finds the first video stream, and logs
|
||||
* per-frame metadata (size, pts, dts, flags) without decoding.
|
||||
*
|
||||
* Run:
|
||||
* daedalus_v4l2_daemon parse /path/to/sample.ivf
|
||||
*
|
||||
* For Phase 8.3 acceptance: parses any container FFmpeg
|
||||
* supports; logs frame counts + per-codec confirmation.
|
||||
* Block-level metadata extraction (the actual per-block QPU
|
||||
* dispatch info) is Phase 8.4 work.
|
||||
*/
|
||||
#include "parser.h"
|
||||
#include "ffmpeg_loader.h"
|
||||
#include "log.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/avutil.h>
|
||||
|
||||
/* AVERROR_EOF without dragging the macro from FFmpeg's averror.h. */
|
||||
#ifndef AVERROR_EOF
|
||||
#define AVERROR_EOF (-('E' | ('O' << 8) | ('F' << 16) | (' ' << 24)))
|
||||
#endif
|
||||
|
||||
int daedalus_parse_file(struct ffmpeg_loader *fm, const char *path)
|
||||
{
|
||||
AVFormatContext *fmt_ctx = NULL;
|
||||
const AVCodec *codec = NULL;
|
||||
AVPacket *pkt = NULL;
|
||||
int video_stream = -1;
|
||||
int ret;
|
||||
|
||||
fmt_ctx = fm->avformat_alloc_context();
|
||||
if (!fmt_ctx) {
|
||||
log_err("avformat_alloc_context returned NULL");
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
ret = fm->avformat_open_input(&fmt_ctx, path, NULL, NULL);
|
||||
if (ret < 0) {
|
||||
log_err("avformat_open_input(%s): code %d", path, ret);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
ret = fm->avformat_find_stream_info(fmt_ctx, NULL);
|
||||
if (ret < 0) {
|
||||
log_err("avformat_find_stream_info: code %d", ret);
|
||||
fm->avformat_close_input(&fmt_ctx);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
video_stream = fm->av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO,
|
||||
-1, -1, &codec, 0);
|
||||
if (video_stream < 0) {
|
||||
log_err("no video stream found in %s", path);
|
||||
fm->avformat_close_input(&fmt_ctx);
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
{
|
||||
AVStream *st = fmt_ctx->streams[video_stream];
|
||||
log_info("video stream #%d: codec=%s (%s) %dx%d, %d/%d fps",
|
||||
video_stream,
|
||||
codec ? codec->name : "?",
|
||||
codec ? codec->long_name : "?",
|
||||
st->codecpar->width, st->codecpar->height,
|
||||
st->avg_frame_rate.num, st->avg_frame_rate.den);
|
||||
}
|
||||
|
||||
pkt = fm->av_packet_alloc();
|
||||
if (!pkt) {
|
||||
log_err("av_packet_alloc returned NULL");
|
||||
fm->avformat_close_input(&fmt_ctx);
|
||||
return -ENOMEM;
|
||||
}
|
||||
|
||||
int n_frames = 0, n_keyframes = 0;
|
||||
size_t total_bytes = 0;
|
||||
while ((ret = fm->av_read_frame(fmt_ctx, pkt)) >= 0) {
|
||||
if (pkt->stream_index == video_stream) {
|
||||
n_frames++;
|
||||
total_bytes += pkt->size;
|
||||
if (pkt->flags & AV_PKT_FLAG_KEY)
|
||||
n_keyframes++;
|
||||
log_debug("frame %d: size=%d pts=%lld dts=%lld flags=0x%x",
|
||||
n_frames, pkt->size,
|
||||
(long long) pkt->pts, (long long) pkt->dts,
|
||||
pkt->flags);
|
||||
}
|
||||
fm->av_packet_unref(pkt);
|
||||
}
|
||||
|
||||
if (ret != AVERROR_EOF) {
|
||||
log_warn("av_read_frame terminated abnormally (code %d)", ret);
|
||||
}
|
||||
|
||||
log_info("parse complete: %d frames (%d key) total %zu bytes",
|
||||
n_frames, n_keyframes, total_bytes);
|
||||
|
||||
fm->av_packet_free(&pkt);
|
||||
fm->avformat_close_input(&fmt_ctx);
|
||||
return n_frames > 0 ? 0 : -ENODATA;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||
#ifndef DAEDALUS_V4L2_PARSER_H
|
||||
#define DAEDALUS_V4L2_PARSER_H
|
||||
|
||||
struct ffmpeg_loader;
|
||||
|
||||
/**
|
||||
* daedalus_parse_file - demux + iterate frames in a media file
|
||||
* @fm: initialised FFmpeg loader
|
||||
* @path: container file (.ivf, .mp4, .webm — anything FFmpeg
|
||||
* can demux)
|
||||
*
|
||||
* Returns 0 on success (parsed at least one video frame),
|
||||
* negative errno on failure. Phase 8.3 scope: per-frame
|
||||
* metadata only; no decode; no block-level info.
|
||||
*/
|
||||
int daedalus_parse_file(struct ffmpeg_loader *fm, const char *path);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,131 @@
|
||||
# Phase 8.3 closure — daemon with FFmpeg dlopen + parse path
|
||||
|
||||
**Status:** closed 2026-05-18.
|
||||
|
||||
Userspace daemon scaffold that loads FFmpeg via `dlopen` at
|
||||
runtime (Option γ, per locked Phase 8 architecture), opens
|
||||
container files via libavformat, and iterates per-frame
|
||||
metadata. No V4L2 / chardev / decode work yet — pure
|
||||
parse-path validation.
|
||||
|
||||
## What lands
|
||||
|
||||
- `daemon/CMakeLists.txt` — CMake build; pkg-config for FFmpeg
|
||||
headers; `-Wall -Wextra -Wpedantic` clean.
|
||||
- `daemon/src/main.c` — entry point, signal handlers, command
|
||||
dispatcher. Currently exposes `parse <file>`.
|
||||
- `daemon/src/ffmpeg_loader.{c,h}` — runtime FFmpeg loader.
|
||||
dlopens `libavformat.so.61`, `libavcodec.so.61`,
|
||||
`libavutil.so.59`; resolves the 22 function pointers we use.
|
||||
- `daemon/src/parser.{c,h}` — demux loop using
|
||||
`avformat_open_input` + `av_read_frame`; logs per-frame
|
||||
metadata.
|
||||
- `daemon/src/log.{c,h}` — minimal logging facade
|
||||
(stderr-only Phase 8.3; syslog/journal in Phase 8.5+).
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
cd ~/src/daedalus-v4l2/daemon
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
cmake --build .
|
||||
```
|
||||
|
||||
Build dependencies (Debian Trixie):
|
||||
`libavformat-dev libavcodec-dev libavutil-dev`. Runtime
|
||||
dependencies are just `ffmpeg` (or rather: any package shipping
|
||||
the `.so.61`/`.so.59` files).
|
||||
|
||||
## Verification
|
||||
|
||||
```
|
||||
$ ffmpeg -hide_banner -f lavfi -i testsrc=duration=2:size=320x240:rate=30 \
|
||||
-c:v libvpx-vp9 -y /tmp/testsrc.ivf
|
||||
$ ls /tmp/testsrc.ivf
|
||||
-rw-rw-r-- 1 mfritsche mfritsche 18611 May 18 17:09 /tmp/testsrc.ivf
|
||||
|
||||
$ ~/src/daedalus-v4l2/daemon/build/daedalus_v4l2_daemon parse /tmp/testsrc.ivf
|
||||
[2026-05-18 17:09:54 INFO] FFmpeg loaded: 7.1.3-0+deb13u1+rpt1 (libavformat 61.7.100)
|
||||
[2026-05-18 17:09:54 INFO] video stream #0: codec=vp9 (Google VP9) 320x240, 0/0 fps
|
||||
[2026-05-18 17:09:54 INFO] parse complete: 60 frames (1 key) total 17859 bytes
|
||||
```
|
||||
|
||||
60-frame VP9 file demuxed correctly; keyframe count and byte
|
||||
total match expectations. `-v` flag enables per-frame DEBUG
|
||||
logs (size/pts/dts/flags).
|
||||
|
||||
### Error paths
|
||||
|
||||
```
|
||||
$ daedalus_v4l2_daemon parse /tmp/does_not_exist
|
||||
[INFO] FFmpeg loaded: ...
|
||||
[ERR ] avformat_open_input(/tmp/does_not_exist): code -2
|
||||
|
||||
$ daedalus_v4l2_daemon
|
||||
usage: daedalus_v4l2_daemon <command> [args]
|
||||
...
|
||||
```
|
||||
|
||||
Clean error reporting; exits with non-zero code.
|
||||
|
||||
## Design notes
|
||||
|
||||
### Why dlopen instead of static or dynamic-link?
|
||||
|
||||
- **No version lock**: the daemon binary works against any
|
||||
FFmpeg `.so.61` even if the patch version differs from build
|
||||
time. Future FFmpeg updates on the host don't force daemon
|
||||
rebuilds.
|
||||
- **No transitive license issues**: linking dynamically against
|
||||
FFmpeg (LGPL-2.1+) is fine for our BSD-2-Clause daemon, but
|
||||
dlopen makes it explicit and inspection-friendly.
|
||||
- **Optional**: callers that don't need parsing (e.g., a
|
||||
pre-parsed-bitstream test mode) can skip `ffmpeg_loader_init`
|
||||
entirely.
|
||||
|
||||
### Headers vs runtime
|
||||
|
||||
We `#include <libavformat/avformat.h>` etc. at compile time for
|
||||
the struct + enum definitions (so our function-pointer
|
||||
signatures are type-safe). We do NOT link against FFmpeg —
|
||||
the only loader dependency is `-ldl`.
|
||||
|
||||
### POSIX dlsym to function pointer
|
||||
|
||||
The dlsym → function pointer assignment uses the POSIX-
|
||||
recommended `*(void **) &fn_ptr = dlsym(...)` form to avoid
|
||||
the ISO C "function pointer is not a void pointer" warning.
|
||||
POSIX.1-2017 dlsym(3p) Rationale explicitly endorses this
|
||||
pattern.
|
||||
|
||||
## What's NOT here
|
||||
|
||||
- **Per-block metadata**: Phase 8.3 only demuxes containers
|
||||
and logs frame-level info. Per-block VP9/H.264 metadata
|
||||
(block positions, MVs, coefficients) is Phase 8.4 work and
|
||||
requires either FFmpeg internal APIs or our own bitstream
|
||||
parsing.
|
||||
- **V4L2 / chardev wiring**: the daemon doesn't yet open
|
||||
`/dev/daedalus-v4l2`. Phase 8.4 connects the parse output
|
||||
to the chardev request/response loop.
|
||||
- **Daemon mode**: no fork / setsid / pid file / systemd unit
|
||||
yet. Phase 8.5+ adds those when the daemon needs to run as
|
||||
a long-running service.
|
||||
|
||||
## Phase 8.4 plan
|
||||
|
||||
1. Open `/dev/daedalus-v4l2` chardev.
|
||||
2. Wait for REQ_DECODE messages from kernel.
|
||||
3. For each request:
|
||||
- Feed the bitstream blob into FFmpeg's parse path.
|
||||
- Get an `AVFrame` back (FFmpeg decodes on CPU initially).
|
||||
- Send RESP_FRAME with the decoded frame back.
|
||||
4. Validate end-to-end via a userspace V4L2 client that pushes
|
||||
a VP9 file at the kernel module and reads decoded frames.
|
||||
|
||||
The QPU dispatch via `daedalus_dispatch_*` from the sibling
|
||||
daedalus-fourier kernel library lands in Phase 8.4 as well —
|
||||
once the FFmpeg-driven decoder is working with CPU paths, we
|
||||
can substitute QPU dispatch for the kernels that benefit
|
||||
(cycles 1, 2, 4).
|
||||
Reference in New Issue
Block a user