engine: add the footage family to the C ABI facade; oak-cli probe

- oakengine_footage_probe inspects media without a project (decoder
  probe): stream counts, per-stream video info (dimensions, rate,
  duration, color tags, interlacing), audio info, duration, decoder
  name, source start time; thread_local last_error for the NULL-handle
  failure paths
- oakengine_project_import_footage adds probed footage to a project
  through the same undoable command path as the app
- oak-cli probe prints decoder/duration/per-stream details and runs as
  a ctest everywhere (no GL)
- dual ownership documented: probe handles are owned, imported footage
  is borrowed from its project
This commit is contained in:
2026-07-20 06:03:16 +08:00
parent 6118260e01
commit 1a7029fd8f
7 changed files with 919 additions and 1 deletions
+9
View File
@@ -81,6 +81,15 @@ if (BUILD_TESTS)
set_tests_properties(oak_cli_render PROPERTIES
SKIP_RETURN_CODE 2
)
# probe is headless and always runs; the output must mention the demo
# file's 1920x1080 video and 48000 Hz audio.
add_test(NAME oak_cli_probe
COMMAND oak-cli probe ${CMAKE_SOURCE_DIR}/tests/demo.mp4
)
set_tests_properties(oak_cli_probe PROPERTIES
PASS_REGULAR_EXPRESSION "1920.*48000"
)
# Rendering needs the render worker and the dynamic backend plugins.
if (TARGET olive-render-worker)
add_dependencies(oak-cli olive-render-worker)
+85 -1
View File
@@ -48,6 +48,7 @@
#include <string>
#include <vector>
#include "oakengine/footage.h"
#include "oakengine/init.h"
#include "oakengine/project.h"
#include "oakengine/renderer.h"
@@ -76,12 +77,15 @@ void print_usage(FILE *out)
" Render the first sequence to PPM frames (P6, 8-bit RGB) and the\n"
" audio range to a PCM s16 WAV file in <out_dir>.\n"
"\n"
" oak-cli probe <mediafile>\n"
" Probe a media file: decoder, duration, video and audio streams.\n"
"\n"
" oak-cli --help\n"
" Show this text.\n"
"\n"
"Exit codes:\n"
" 0 success\n"
" 1 general error (bad project file, no sequence, I/O failure)\n"
" 1 general error (bad project/media file, no sequence, I/O failure)\n"
" 2 rendering unavailable or failed (e.g. no GL render backend)\n"
" 64 usage error\n");
}
@@ -460,6 +464,79 @@ int cmd_render(const char *path, const char *start_str, const char *end_str,
return rc;
}
int cmd_probe(const char *path)
{
if (oakengine_init(OAKENGINE_INIT_HEADLESS) != OAKENGINE_OK) {
fprintf(stderr, "error: failed to initialize the engine\n");
return k_exit_error;
}
int rc = k_exit_ok;
OakEngineFootage *f = oakengine_footage_probe(path);
if (!f) {
char err[1024];
fprintf(stderr, "error: %s\n",
oakengine_footage_last_error(err, sizeof(err)) > 0 ?
err :
"probe failed");
rc = k_exit_error;
} else {
char decoder[64];
oakengine_footage_get_decoder_name(f, decoder, sizeof(decoder));
double duration = 0.0;
oakengine_footage_get_duration(f, &duration);
printf("Decoder: %s\n", decoder);
printf("Duration: %.6f s\n", duration);
const int videos = oakengine_footage_get_video_stream_count(f);
printf("Video streams: %d\n", videos);
for (int i = 0; i < videos; i++) {
oak_footage_video_info vi;
if (oakengine_footage_get_video_stream_info(f, i, &vi) !=
OAKENGINE_OK) {
continue;
}
const double secs = vi.time_base_den ?
double(vi.duration_ts) * vi.time_base_num / vi.time_base_den :
0.0;
const double fps = vi.frame_rate_den ?
double(vi.frame_rate_num) / vi.frame_rate_den :
0.0;
printf(" [%d] stream %d: %dx%d, %d/%d fps (%.3f), duration "
"%lld/%d (%f s), primaries=%d trc=%d, %s\n",
i, vi.stream_index, vi.width, vi.height,
vi.frame_rate_num, vi.frame_rate_den, fps,
(long long)vi.duration_ts, vi.time_base_den, secs,
vi.color_primaries, vi.color_trc,
vi.interlaced ? "interlaced" : "progressive");
}
const int audios = oakengine_footage_get_audio_stream_count(f);
printf("Audio streams: %d\n", audios);
for (int i = 0; i < audios; i++) {
oak_footage_audio_info ai;
if (oakengine_footage_get_audio_stream_info(f, i, &ai) !=
OAKENGINE_OK) {
continue;
}
const double secs = ai.time_base_den ?
double(ai.duration_ts) * ai.time_base_num / ai.time_base_den :
0.0;
printf(" [%d] stream %d: %d Hz, %d channels, duration %lld/%d "
"(%f s)\n",
i, ai.stream_index, ai.sample_rate, ai.channel_count,
(long long)ai.duration_ts, ai.time_base_den, secs);
}
printf("Subtitle streams: %d\n",
oakengine_footage_get_subtitle_stream_count(f));
oakengine_footage_free(f);
}
oakengine_shutdown();
return rc;
}
} // namespace
int main(int argc, char *argv[])
@@ -488,6 +565,13 @@ int main(int argc, char *argv[])
}
return cmd_render(argv[2], argv[3], argv[4], argv[5]);
}
if (command == "probe") {
if (argc != 3) {
print_usage(stderr);
return k_exit_usage;
}
return cmd_probe(argv[2]);
}
fprintf(stderr, "error: unknown command \"%s\"\n", argv[1]);
print_usage(stderr);