cli: add oak-cli, the first pure C ABI consumer of liboakengine
- oak-cli info prints project/sequence/footage details; oak-cli render writes PPM frames and a PCM WAV through the facade only - no engine C++ headers, no Qt headers, links just liboakengine - exit code 2 marks rendering-unavailable so ctest can skip cleanly on machines without a GL render backend; info test runs everywhere - facade fix uncovered by the CLI: project load now absolutizes the path, so relative footage paths can't be mistaken for a moved project; the fixture project now carries probed footage wired into its sequence so it renders real content - packaged like the other binaries (Linux bin install, macOS bundle, Windows DLL copies); DESTDIR-verified
This commit is contained in:
@@ -309,6 +309,7 @@ add_subdirectory(third_party/KDDockWidgets EXCLUDE_FROM_ALL)
|
|||||||
|
|
||||||
add_subdirectory(third_party/openfx/HostSupport)
|
add_subdirectory(third_party/openfx/HostSupport)
|
||||||
add_subdirectory(engine)
|
add_subdirectory(engine)
|
||||||
|
add_subdirectory(cli)
|
||||||
|
|
||||||
# Everything above the engine library links against it
|
# Everything above the engine library links against it
|
||||||
list(APPEND OLIVE_LIBRARIES oakengine)
|
list(APPEND OLIVE_LIBRARIES oakengine)
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# Oak - Non-Linear Video Editor
|
||||||
|
# Copyright (C) 2026 Oak Team
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# oak-cli: headless command-line consumer of the liboakengine C ABI facade.
|
||||||
|
# It is the first real consumer of the facade and therefore links ONLY
|
||||||
|
# against liboakengine (which publicly re-exports liboakcore); the source
|
||||||
|
# includes only the public oakengine/*.h C headers -- no engine C++ headers,
|
||||||
|
# no Qt headers.
|
||||||
|
|
||||||
|
add_executable(oak-cli
|
||||||
|
main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
# oakengine leaves olive::k_app_version to the final executable; the version
|
||||||
|
# object library provides it (same as the engine C ABI tests and worker).
|
||||||
|
target_sources(oak-cli PRIVATE $<TARGET_OBJECTS:olive-version-obj>)
|
||||||
|
|
||||||
|
target_link_libraries(oak-cli PRIVATE oakengine)
|
||||||
|
target_include_directories(oak-cli PRIVATE
|
||||||
|
${CMAKE_SOURCE_DIR}/engine/include
|
||||||
|
)
|
||||||
|
set_target_properties(oak-cli PROPERTIES
|
||||||
|
OUTPUT_NAME oak-cli
|
||||||
|
)
|
||||||
|
|
||||||
|
if (APPLE)
|
||||||
|
# Distribute inside Oak.app/Contents/MacOS next to the other helper
|
||||||
|
# binaries (mirrors the oak-render-worker copies in app/CMakeLists.txt).
|
||||||
|
add_custom_command(TARGET oak-cli POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_BUNDLE_DIR:olive-editor>/Contents/MacOS
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:oak-cli> $<TARGET_BUNDLE_DIR:olive-editor>/Contents/MacOS/
|
||||||
|
COMMENT "Copying oak-cli into Oak.app"
|
||||||
|
)
|
||||||
|
elseif (UNIX)
|
||||||
|
install(TARGETS oak-cli RUNTIME DESTINATION bin)
|
||||||
|
# liboakengine installs into the platform lib dir (see
|
||||||
|
# engine/CMakeLists.txt), resolve it relative to the binary.
|
||||||
|
set_target_properties(oak-cli PROPERTIES INSTALL_RPATH "$ORIGIN/../lib")
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (WIN32)
|
||||||
|
# Windows has no RPATH: shared libraries must sit next to the executable
|
||||||
|
# (mirrors tests/gtest/CMakeLists.txt).
|
||||||
|
add_custom_command(TARGET oak-cli POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:ffmpeg_bridge> $<TARGET_FILE_DIR:oak-cli>
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:olivecore> $<TARGET_FILE_DIR:oak-cli>
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:oakengine> $<TARGET_FILE_DIR:oak-cli>
|
||||||
|
)
|
||||||
|
endif ()
|
||||||
|
|
||||||
|
if (BUILD_TESTS)
|
||||||
|
enable_testing()
|
||||||
|
|
||||||
|
# info always runs (headless); the output must mention the fixture's
|
||||||
|
# sequence name and footage filename.
|
||||||
|
add_test(NAME oak_cli_info
|
||||||
|
COMMAND oak-cli info ${CMAKE_SOURCE_DIR}/tests/project_with_footage.ove
|
||||||
|
)
|
||||||
|
set_tests_properties(oak_cli_info PROPERTIES
|
||||||
|
PASS_REGULAR_EXPRESSION "Fixture Sequence.*demo\\.mp4"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Render smoke test: without a GL render backend oak-cli reports the
|
||||||
|
# failure and exits 2, which ctest treats as a skip.
|
||||||
|
add_test(NAME oak_cli_render
|
||||||
|
COMMAND oak-cli render ${CMAKE_SOURCE_DIR}/tests/project_with_footage.ove 0 1 ${CMAKE_CURRENT_BINARY_DIR}/oak_cli_render_out
|
||||||
|
)
|
||||||
|
set_tests_properties(oak_cli_render PROPERTIES
|
||||||
|
SKIP_RETURN_CODE 2
|
||||||
|
)
|
||||||
|
# Rendering needs the render worker and the dynamic backend plugins.
|
||||||
|
if (TARGET olive-render-worker)
|
||||||
|
add_dependencies(oak-cli olive-render-worker)
|
||||||
|
endif ()
|
||||||
|
if (TARGET oakgl)
|
||||||
|
add_dependencies(oak-cli oakgl)
|
||||||
|
endif ()
|
||||||
|
if (TARGET oakvulkan)
|
||||||
|
add_dependencies(oak-cli oakvulkan)
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
+491
@@ -0,0 +1,491 @@
|
|||||||
|
/***
|
||||||
|
|
||||||
|
Oak - Non-Linear Video Editor
|
||||||
|
Copyright (C) 2026 Oak Team
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
***/
|
||||||
|
|
||||||
|
// oak-cli: headless command-line consumer of the liboakengine C ABI facade.
|
||||||
|
//
|
||||||
|
// This binary is a pure C ABI consumer: it includes ONLY the public
|
||||||
|
// oakengine/*.h C headers (no engine C++ headers, no Qt headers) and links
|
||||||
|
// only against liboakengine. Everything it does goes through the facade:
|
||||||
|
//
|
||||||
|
// oak-cli info <project.ove>
|
||||||
|
// Print the project name, its sequences (name, length, frame rate,
|
||||||
|
// track counts, playhead) and its footage (filename, online/offline).
|
||||||
|
//
|
||||||
|
// oak-cli render <project.ove> <start_seconds> <end_seconds> <out_dir>
|
||||||
|
// Render the first sequence frame by frame at the sequence frame rate
|
||||||
|
// to PPM (P6) images plus the whole audio range to a PCM s16 WAV.
|
||||||
|
//
|
||||||
|
// Exit codes:
|
||||||
|
// 0 success
|
||||||
|
// 1 general error (bad project file, no sequence, I/O failure)
|
||||||
|
// 2 rendering unavailable or failed (e.g. no GL render backend); ctest
|
||||||
|
// treats this as SKIP via SKIP_RETURN_CODE
|
||||||
|
// 64 usage error (wrong arguments)
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "oakengine/init.h"
|
||||||
|
#include "oakengine/project.h"
|
||||||
|
#include "oakengine/renderer.h"
|
||||||
|
#include "oakengine/timeline.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
constexpr int k_exit_ok = 0;
|
||||||
|
constexpr int k_exit_error = 1;
|
||||||
|
constexpr int k_exit_render_unavailable = 2;
|
||||||
|
constexpr int k_exit_usage = 64;
|
||||||
|
|
||||||
|
// The facade does not expose sequence dimensions yet, so rendering uses the
|
||||||
|
// application's default sequence size (also the native size of the demo
|
||||||
|
// assets).
|
||||||
|
constexpr int k_render_width = 1920;
|
||||||
|
constexpr int k_render_height = 1080;
|
||||||
|
constexpr int k_pixel_format_f32 = 4; // olive::core::PixelFormat::f32
|
||||||
|
|
||||||
|
void print_usage(FILE *out)
|
||||||
|
{
|
||||||
|
fprintf(out,
|
||||||
|
"oak-cli - headless consumer of the liboakengine C ABI\n"
|
||||||
|
"\n"
|
||||||
|
"Usage:\n"
|
||||||
|
" oak-cli info <project.ove>\n"
|
||||||
|
" Print project name, sequences and footage.\n"
|
||||||
|
"\n"
|
||||||
|
" oak-cli render <project.ove> <start_seconds> <end_seconds> <out_dir>\n"
|
||||||
|
" 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 --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"
|
||||||
|
" 2 rendering unavailable or failed (e.g. no GL render backend)\n"
|
||||||
|
" 64 usage error\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read a facade string (buf/size convention) into a std::string.
|
||||||
|
std::string facade_string(int (*getter)(const void *, char *, int),
|
||||||
|
const void *handle)
|
||||||
|
{
|
||||||
|
const int size = getter(handle, nullptr, 0);
|
||||||
|
if (size < 0) {
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
std::string s(size_t(size) + 1, '\0');
|
||||||
|
getter(handle, s.data(), size + 1);
|
||||||
|
s.resize(size_t(size));
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
int project_name_adapter(const void *handle, char *buf, int size)
|
||||||
|
{
|
||||||
|
return oakengine_project_name(
|
||||||
|
reinterpret_cast<const OakEngineProject *>(handle), buf, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
int sequence_name_adapter(const void *handle, char *buf, int size)
|
||||||
|
{
|
||||||
|
return oakengine_sequence_name(
|
||||||
|
reinterpret_cast<const OakEngineSequence *>(handle), buf, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_sequence(OakEngineSequence *seq, int index)
|
||||||
|
{
|
||||||
|
const std::string name =
|
||||||
|
facade_string(sequence_name_adapter, reinterpret_cast<void *>(seq));
|
||||||
|
|
||||||
|
double seconds = 0.0;
|
||||||
|
oakengine_sequence_get_length(seq, &seconds);
|
||||||
|
int len_num = 0, len_den = 0;
|
||||||
|
oakengine_sequence_get_length_rational(seq, &len_num, &len_den);
|
||||||
|
int fr_num = 0, fr_den = 0;
|
||||||
|
oakengine_sequence_get_frame_rate(seq, &fr_num, &fr_den);
|
||||||
|
int video = 0, audio = 0, subtitle = 0;
|
||||||
|
oakengine_sequence_track_count(seq, &video, &audio, &subtitle);
|
||||||
|
int64_t playhead = 0;
|
||||||
|
double playhead_seconds = 0.0;
|
||||||
|
oakengine_sequence_get_playhead(seq, &playhead);
|
||||||
|
oakengine_sequence_get_playhead_seconds(seq, &playhead_seconds);
|
||||||
|
|
||||||
|
printf(" [%d] \"%s\"\n", index, name.c_str());
|
||||||
|
printf(" length: %.6f s (%d/%d)\n", seconds, len_num, len_den);
|
||||||
|
printf(" frame rate: %d/%d (%.3f fps)\n", fr_num, fr_den,
|
||||||
|
fr_den ? double(fr_num) / double(fr_den) : 0.0);
|
||||||
|
printf(" tracks: video=%d audio=%d subtitle=%d\n", video, audio,
|
||||||
|
subtitle);
|
||||||
|
printf(" playhead: %lld (%.6f s)\n", (long long)playhead,
|
||||||
|
playhead_seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
int cmd_info(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;
|
||||||
|
OakEngineProject *project = oakengine_project_create();
|
||||||
|
char err[1024];
|
||||||
|
if (oakengine_project_load(project, path, err, sizeof(err)) !=
|
||||||
|
OAKENGINE_OK) {
|
||||||
|
fprintf(stderr, "error: failed to load \"%s\": %s\n", path, err);
|
||||||
|
rc = k_exit_error;
|
||||||
|
} else {
|
||||||
|
const std::string name =
|
||||||
|
facade_string(project_name_adapter, reinterpret_cast<void *>(project));
|
||||||
|
char filename[4096];
|
||||||
|
oakengine_project_filename(project, filename, sizeof(filename));
|
||||||
|
|
||||||
|
printf("Project: %s\n", name.c_str());
|
||||||
|
printf("File: %s\n", filename);
|
||||||
|
printf("Modified: %s\n",
|
||||||
|
oakengine_project_is_modified(project) ? "yes" : "no");
|
||||||
|
|
||||||
|
const int sequences = oakengine_project_sequence_count(project);
|
||||||
|
printf("Sequences: %d\n", sequences);
|
||||||
|
for (int i = 0; i < sequences; i++) {
|
||||||
|
print_sequence(oakengine_project_sequence_at(project, i), i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int footage = oakengine_project_footage_count(project);
|
||||||
|
printf("Footage: %d\n", footage);
|
||||||
|
for (int i = 0; i < footage; i++) {
|
||||||
|
const int size =
|
||||||
|
oakengine_project_footage_filename(project, i, nullptr, 0);
|
||||||
|
std::string fn(size_t(size > 0 ? size : 0), '\0');
|
||||||
|
if (size > 0) {
|
||||||
|
oakengine_project_footage_filename(project, i, fn.data(),
|
||||||
|
size + 1);
|
||||||
|
}
|
||||||
|
const int online =
|
||||||
|
oakengine_project_footage_is_online(project, i);
|
||||||
|
printf(" [%d] \"%s\" %s\n", i, fn.c_str(),
|
||||||
|
online == 1 ? "online" : "offline");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oakengine_project_free(project);
|
||||||
|
oakengine_shutdown();
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// f32 RGBA -> 8-bit RGB triple, clamped.
|
||||||
|
void write_ppm(const OakEngineFrame *frame, const std::string &path)
|
||||||
|
{
|
||||||
|
const int width = oakengine_frame_width(frame);
|
||||||
|
const int height = oakengine_frame_height(frame);
|
||||||
|
const int format = oakengine_frame_format(frame);
|
||||||
|
const int channels = oakengine_frame_channel_count(frame);
|
||||||
|
const int linesize = oakengine_frame_linesize_bytes(frame);
|
||||||
|
const char *data =
|
||||||
|
reinterpret_cast<const char *>(oakengine_frame_data(frame));
|
||||||
|
|
||||||
|
FILE *f = fopen(path.c_str(), "wb");
|
||||||
|
if (!f) {
|
||||||
|
throw std::string("cannot open \"" + path + "\" for writing");
|
||||||
|
}
|
||||||
|
fprintf(f, "P6\n%d %d\n255\n", width, height);
|
||||||
|
|
||||||
|
std::vector<unsigned char> row(size_t(width) * 3);
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
const char *line = data + ptrdiff_t(y) * linesize;
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
for (int c = 0; c < 3; c++) {
|
||||||
|
unsigned char v = 0;
|
||||||
|
if (format == k_pixel_format_f32) {
|
||||||
|
// f32: 4 bytes per channel
|
||||||
|
const float *px = reinterpret_cast<const float *>(line) +
|
||||||
|
ptrdiff_t(x) * channels;
|
||||||
|
const float clamped = px[c] < 0.0f ? 0.0f :
|
||||||
|
px[c] > 1.0f ? 1.0f :
|
||||||
|
px[c];
|
||||||
|
v = static_cast<unsigned char>(clamped * 255.0f + 0.5f);
|
||||||
|
} else if (format == 0) {
|
||||||
|
// u8: 1 byte per channel
|
||||||
|
v = static_cast<unsigned char>(
|
||||||
|
line[ptrdiff_t(x) * channels + c]);
|
||||||
|
} else {
|
||||||
|
fclose(f);
|
||||||
|
throw std::string("unsupported frame pixel format " +
|
||||||
|
std::to_string(format));
|
||||||
|
}
|
||||||
|
row[size_t(x) * 3 + c] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fwrite(row.data(), 1, row.size(), f);
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_u16_le(FILE *f, uint16_t v)
|
||||||
|
{
|
||||||
|
fputc(v & 0xFF, f);
|
||||||
|
fputc((v >> 8) & 0xFF, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_u32_le(FILE *f, uint32_t v)
|
||||||
|
{
|
||||||
|
write_u16_le(f, uint16_t(v & 0xFFFF));
|
||||||
|
write_u16_le(f, uint16_t(v >> 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Planar float samples -> interleaved PCM s16 WAV.
|
||||||
|
void write_wav(const OakEngineAudioBuffer *audio, const std::string &path)
|
||||||
|
{
|
||||||
|
const int rate = oakengine_audio_sample_rate(audio);
|
||||||
|
const int channels = oakengine_audio_channel_count(audio);
|
||||||
|
const int64_t samples = oakengine_audio_sample_count(audio);
|
||||||
|
|
||||||
|
FILE *f = fopen(path.c_str(), "wb");
|
||||||
|
if (!f) {
|
||||||
|
throw std::string("cannot open \"" + path + "\" for writing");
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t data_size = uint32_t(samples) * uint32_t(channels) * 2;
|
||||||
|
fwrite("RIFF", 1, 4, f);
|
||||||
|
write_u32_le(f, 36 + data_size);
|
||||||
|
fwrite("WAVE", 1, 4, f);
|
||||||
|
fwrite("fmt ", 1, 4, f);
|
||||||
|
write_u32_le(f, 16); // fmt chunk size
|
||||||
|
write_u16_le(f, 1); // PCM
|
||||||
|
write_u16_le(f, uint16_t(channels));
|
||||||
|
write_u32_le(f, uint32_t(rate));
|
||||||
|
write_u32_le(f, uint32_t(rate * channels * 2)); // byte rate
|
||||||
|
write_u16_le(f, uint16_t(channels * 2)); // block align
|
||||||
|
write_u16_le(f, 16); // bits per sample
|
||||||
|
fwrite("data", 1, 4, f);
|
||||||
|
write_u32_le(f, data_size);
|
||||||
|
|
||||||
|
for (int64_t i = 0; i < samples; i++) {
|
||||||
|
for (int ch = 0; ch < channels; ch++) {
|
||||||
|
const float *channel_data = oakengine_audio_data(audio, ch);
|
||||||
|
const float v = channel_data[i];
|
||||||
|
const float clamped = v < -1.0f ? -1.0f : v > 1.0f ? 1.0f : v;
|
||||||
|
const int16_t s = static_cast<int16_t>(clamped * 32767.0f);
|
||||||
|
write_u16_le(f, uint16_t(s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
int renderer_fail(OakEngineRenderer *renderer, const char *what)
|
||||||
|
{
|
||||||
|
char err[1024];
|
||||||
|
if (oakengine_renderer_last_error(renderer, err, sizeof(err)) > 0) {
|
||||||
|
fprintf(stderr, "error: %s failed: %s\n", what, err);
|
||||||
|
} else {
|
||||||
|
fprintf(stderr, "error: %s failed\n", what);
|
||||||
|
}
|
||||||
|
return k_exit_render_unavailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cmd_render(const char *path, const char *start_str, const char *end_str,
|
||||||
|
const char *out_dir)
|
||||||
|
{
|
||||||
|
char *end = nullptr;
|
||||||
|
const double start_seconds = std::strtod(start_str, &end);
|
||||||
|
if (end == start_str || *end != '\0') {
|
||||||
|
fprintf(stderr, "error: invalid start seconds \"%s\"\n", start_str);
|
||||||
|
return k_exit_usage;
|
||||||
|
}
|
||||||
|
const double end_seconds = std::strtod(end_str, &end);
|
||||||
|
if (end == end_str || *end != '\0' || end_seconds <= start_seconds) {
|
||||||
|
fprintf(stderr, "error: invalid end seconds \"%s\"\n", end_str);
|
||||||
|
return k_exit_usage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oakengine_init(OAKENGINE_INIT_HEADLESS | OAKENGINE_INIT_RENDER) !=
|
||||||
|
OAKENGINE_OK) {
|
||||||
|
fprintf(stderr, "error: failed to initialize the engine\n");
|
||||||
|
return k_exit_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = k_exit_ok;
|
||||||
|
OakEngineProject *project = nullptr;
|
||||||
|
OakEngineRenderer *renderer = nullptr;
|
||||||
|
do {
|
||||||
|
// Footage paths are stored relative to the project file, and the
|
||||||
|
// engine probes/decodes them against the process cwd (both when the
|
||||||
|
// project loads and when audio renders in-process), so resolve from
|
||||||
|
// the project's directory -- the same rule as
|
||||||
|
// oakengine_project_footage_is_online(). The project path itself is
|
||||||
|
// made absolute first so it survives the chdir.
|
||||||
|
std::error_code ec;
|
||||||
|
const std::filesystem::path abs_project =
|
||||||
|
std::filesystem::absolute(std::filesystem::path(path), ec);
|
||||||
|
if (ec) {
|
||||||
|
fprintf(stderr, "error: cannot resolve \"%s\": %s\n", path,
|
||||||
|
ec.message().c_str());
|
||||||
|
rc = k_exit_error;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const std::filesystem::path project_dir = abs_project.parent_path();
|
||||||
|
if (!project_dir.empty()) {
|
||||||
|
std::filesystem::current_path(project_dir, ec);
|
||||||
|
}
|
||||||
|
|
||||||
|
project = oakengine_project_create();
|
||||||
|
char err[1024];
|
||||||
|
if (oakengine_project_load(project, abs_project.string().c_str(), err,
|
||||||
|
sizeof(err)) != OAKENGINE_OK) {
|
||||||
|
fprintf(stderr, "error: failed to load \"%s\": %s\n", path, err);
|
||||||
|
rc = k_exit_error;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oakengine_project_sequence_count(project) < 1) {
|
||||||
|
fprintf(stderr, "error: project has no sequence to render\n");
|
||||||
|
rc = k_exit_error;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
OakEngineSequence *seq = oakengine_project_sequence_at(project, 0);
|
||||||
|
|
||||||
|
int fr_num = 0, fr_den = 0;
|
||||||
|
if (oakengine_sequence_get_frame_rate(seq, &fr_num, &fr_den) !=
|
||||||
|
OAKENGINE_OK ||
|
||||||
|
fr_num <= 0 || fr_den <= 0) {
|
||||||
|
fprintf(stderr, "error: sequence has no valid frame rate\n");
|
||||||
|
rc = k_exit_error;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const double fps = double(fr_num) / double(fr_den);
|
||||||
|
const int64_t start_ts = std::llround(start_seconds * fps);
|
||||||
|
const int64_t end_ts = std::llround(end_seconds * fps);
|
||||||
|
const int64_t frame_count = end_ts - start_ts;
|
||||||
|
|
||||||
|
std::filesystem::create_directories(out_dir, ec);
|
||||||
|
if (ec) {
|
||||||
|
fprintf(stderr, "error: cannot create output directory \"%s\": %s\n",
|
||||||
|
out_dir, ec.message().c_str());
|
||||||
|
rc = k_exit_error;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer = oakengine_renderer_create(seq, k_render_width,
|
||||||
|
k_render_height, k_pixel_format_f32,
|
||||||
|
fr_num, fr_den, nullptr);
|
||||||
|
if (!renderer) {
|
||||||
|
fprintf(stderr, "error: failed to create renderer\n");
|
||||||
|
rc = k_exit_error;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (int64_t ts = start_ts; ts < end_ts; ts++) {
|
||||||
|
OakEngineFrame *frame =
|
||||||
|
oakengine_renderer_render_frame(renderer, ts);
|
||||||
|
if (!frame) {
|
||||||
|
rc = renderer_fail(renderer, "render_frame");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
fprintf(stderr, "frame %lld/%lld (ts=%lld)\n",
|
||||||
|
(long long)(ts - start_ts + 1), (long long)frame_count,
|
||||||
|
(long long)ts);
|
||||||
|
|
||||||
|
char name[64];
|
||||||
|
snprintf(name, sizeof(name), "frame-%04lld.ppm",
|
||||||
|
(long long)(ts - start_ts));
|
||||||
|
const std::filesystem::path ppm_path =
|
||||||
|
std::filesystem::path(out_dir) / name;
|
||||||
|
write_ppm(frame, ppm_path.string());
|
||||||
|
oakengine_frame_free(frame);
|
||||||
|
}
|
||||||
|
} catch (const std::string &e) {
|
||||||
|
fprintf(stderr, "error: %s\n", e.c_str());
|
||||||
|
if (rc == k_exit_ok) {
|
||||||
|
rc = k_exit_error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rc == k_exit_ok) {
|
||||||
|
OakEngineAudioBuffer *audio = oakengine_renderer_render_audio(
|
||||||
|
renderer, start_ts, frame_count);
|
||||||
|
if (!audio) {
|
||||||
|
rc = renderer_fail(renderer, "render_audio");
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
write_wav(audio,
|
||||||
|
(std::filesystem::path(out_dir) / "audio.wav")
|
||||||
|
.string());
|
||||||
|
} catch (const std::string &e) {
|
||||||
|
fprintf(stderr, "error: %s\n", e.c_str());
|
||||||
|
rc = k_exit_error;
|
||||||
|
}
|
||||||
|
oakengine_audio_free(audio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rc == k_exit_ok) {
|
||||||
|
printf("wrote %lld PPM frame(s) and audio.wav to \"%s\"\n",
|
||||||
|
(long long)frame_count, out_dir);
|
||||||
|
}
|
||||||
|
} while (false);
|
||||||
|
|
||||||
|
oakengine_renderer_free(renderer);
|
||||||
|
oakengine_project_free(project);
|
||||||
|
oakengine_shutdown();
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
if (argc < 2) {
|
||||||
|
print_usage(stderr);
|
||||||
|
return k_exit_usage;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string command = argv[1];
|
||||||
|
if (command == "--help" || command == "-h") {
|
||||||
|
print_usage(stdout);
|
||||||
|
return k_exit_ok;
|
||||||
|
}
|
||||||
|
if (command == "info") {
|
||||||
|
if (argc != 3) {
|
||||||
|
print_usage(stderr);
|
||||||
|
return k_exit_usage;
|
||||||
|
}
|
||||||
|
return cmd_info(argv[2]);
|
||||||
|
}
|
||||||
|
if (command == "render") {
|
||||||
|
if (argc != 6) {
|
||||||
|
print_usage(stderr);
|
||||||
|
return k_exit_usage;
|
||||||
|
}
|
||||||
|
return cmd_render(argv[2], argv[3], argv[4], argv[5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(stderr, "error: unknown command \"%s\"\n", argv[1]);
|
||||||
|
print_usage(stderr);
|
||||||
|
return k_exit_usage;
|
||||||
|
}
|
||||||
@@ -193,7 +193,12 @@ int oakengine_project_load(OakEngineProject *self, const char *path,
|
|||||||
return OAKENGINE_E_STATE;
|
return OAKENGINE_E_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString filename = QString::fromUtf8(path);
|
// Normalize to an absolute path so the stored filename matches the
|
||||||
|
// file's saved_url; otherwise the footage validator treats the project
|
||||||
|
// as moved (absolute saved_url vs. relative filename) and rewrites
|
||||||
|
// relative footage paths against the caller's cwd.
|
||||||
|
const QString filename =
|
||||||
|
QFileInfo(QString::fromUtf8(path)).absoluteFilePath();
|
||||||
project->set_filename(filename);
|
project->set_filename(filename);
|
||||||
|
|
||||||
olive::ProjectSerializer::Result result = olive::ProjectSerializer::load(
|
olive::ProjectSerializer::Result result = olive::ProjectSerializer::load(
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<olive version="230220" url="/home/mikesolar/Projects/oak/tests/project_with_footage.ove">
|
<olive version="230220" url="/home/mikesolar/Projects/oak/tests/project_with_footage.ove">
|
||||||
<project>
|
<project>
|
||||||
<project version="1">
|
<project version="1">
|
||||||
<uuid>{b9147285-72ca-4b6b-b980-ce1dfbec408f}</uuid>
|
<uuid>{9f610139-af81-4c3f-a689-caeac18ffa5e}</uuid>
|
||||||
<nodes>
|
<nodes>
|
||||||
<node version="1" id="org.olivevideoeditor.Olive.folder" ptr="94695150158256">
|
<node version="1" id="org.olivevideoeditor.Olive.folder" ptr="94432914284304">
|
||||||
<label>Root</label>
|
<label>Root</label>
|
||||||
<input id="enabled_in">
|
<input id="enabled_in">
|
||||||
<primary>
|
<primary>
|
||||||
@@ -35,21 +35,21 @@
|
|||||||
</input>
|
</input>
|
||||||
<connections>
|
<connections>
|
||||||
<connection input="child_in" element="0">
|
<connection input="child_in" element="0">
|
||||||
<output>94695150298656</output>
|
<output>94432914429312</output>
|
||||||
</connection>
|
</connection>
|
||||||
<connection input="child_in" element="1">
|
<connection input="child_in" element="1">
|
||||||
<output>94695150363552</output>
|
<output>94432914494208</output>
|
||||||
</connection>
|
</connection>
|
||||||
</connections>
|
</connections>
|
||||||
<caches>
|
<caches>
|
||||||
<audio>{725969c5-6732-4d64-9fc0-0cbf24c45dff}</audio>
|
<audio>{0acc30ab-841e-4143-8aa5-0393dd76d4ff}</audio>
|
||||||
<video>{819cf072-671a-413b-a5c2-ec8c254ceec6}</video>
|
<video>{4df67d75-38c7-46ef-ac3d-11fc80cfce8b}</video>
|
||||||
<thumb>{3f699391-032f-4b27-9ab9-297faa2781ba}</thumb>
|
<thumb>{a97d0036-322c-4e52-800b-4d97d49ccaa9}</thumb>
|
||||||
<waveform>{a11aa028-d950-460f-a7a6-d71df445ce68}</waveform>
|
<waveform>{fa87a9bb-5f9d-451d-be93-5ba6ee738f33}</waveform>
|
||||||
</caches>
|
</caches>
|
||||||
<custom/>
|
<custom/>
|
||||||
</node>
|
</node>
|
||||||
<node version="1" id="org.olivevideoeditor.Olive.footage" ptr="94695150298656">
|
<node version="1" id="org.olivevideoeditor.Olive.footage" ptr="94432914429312">
|
||||||
<label>demo.mp4</label>
|
<label>demo.mp4</label>
|
||||||
<input id="file_in">
|
<input id="file_in">
|
||||||
<primary>
|
<primary>
|
||||||
@@ -95,6 +95,36 @@
|
|||||||
</track>
|
</track>
|
||||||
</standard>
|
</standard>
|
||||||
</primary>
|
</primary>
|
||||||
|
<subelements count="1">
|
||||||
|
<element>
|
||||||
|
<standard>
|
||||||
|
<track>
|
||||||
|
<width>1920</width>
|
||||||
|
<height>1080</height>
|
||||||
|
<depth>0</depth>
|
||||||
|
<timebase>1/12800</timebase>
|
||||||
|
<format>0</format>
|
||||||
|
<channelcount>4</channelcount>
|
||||||
|
<pixelaspectratio>1/1</pixelaspectratio>
|
||||||
|
<interlacing>0</interlacing>
|
||||||
|
<divider>1</divider>
|
||||||
|
<enabled>1</enabled>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<streamindex>0</streamindex>
|
||||||
|
<videotype>0</videotype>
|
||||||
|
<framerate>25/1</framerate>
|
||||||
|
<starttime>0</starttime>
|
||||||
|
<duration>217600</duration>
|
||||||
|
<premultipliedalpha>0</premultipliedalpha>
|
||||||
|
<colorspace></colorspace>
|
||||||
|
<colorrange>0</colorrange>
|
||||||
|
<colorprimaries>1</colorprimaries>
|
||||||
|
<colortransfer>1</colortransfer>
|
||||||
|
</track>
|
||||||
|
</standard>
|
||||||
|
</element>
|
||||||
|
</subelements>
|
||||||
</input>
|
</input>
|
||||||
<input id="audio_param_in">
|
<input id="audio_param_in">
|
||||||
<primary>
|
<primary>
|
||||||
@@ -110,6 +140,21 @@
|
|||||||
</track>
|
</track>
|
||||||
</standard>
|
</standard>
|
||||||
</primary>
|
</primary>
|
||||||
|
<subelements count="1">
|
||||||
|
<element>
|
||||||
|
<standard>
|
||||||
|
<track>
|
||||||
|
<samplerate>48000</samplerate>
|
||||||
|
<channellayout>3</channellayout>
|
||||||
|
<format>f32p</format>
|
||||||
|
<enabled>1</enabled>
|
||||||
|
<streamindex>1</streamindex>
|
||||||
|
<duration>816000</duration>
|
||||||
|
<timebase>1/48000</timebase>
|
||||||
|
</track>
|
||||||
|
</standard>
|
||||||
|
</element>
|
||||||
|
</subelements>
|
||||||
</input>
|
</input>
|
||||||
<input id="subtitle_param_in">
|
<input id="subtitle_param_in">
|
||||||
<primary>
|
<primary>
|
||||||
@@ -119,13 +164,14 @@
|
|||||||
</primary>
|
</primary>
|
||||||
</input>
|
</input>
|
||||||
<caches>
|
<caches>
|
||||||
<audio>{03070a9c-e9bd-4a6e-8532-7d502d8fea5e}</audio>
|
<audio>{248ab9c2-17e4-43f1-8731-7d4b2eaaf8c9}</audio>
|
||||||
<video>{680e8c34-0d38-40c3-87b9-810db7df6ff0}</video>
|
<video>{399f53b5-7543-470c-854d-48a4b7d68c21}</video>
|
||||||
<thumb>{d7346615-f439-4910-9c0b-c9b94df27883}</thumb>
|
<thumb>{3761c26e-2564-431f-9787-9e9e104f6ece}</thumb>
|
||||||
<waveform>{7d154ff3-1794-451c-9495-528c5d5c4576}</waveform>
|
<waveform>{90e619c9-3a4c-40e2-9701-e6fcfb20fc9a}</waveform>
|
||||||
</caches>
|
</caches>
|
||||||
<custom>
|
<custom>
|
||||||
<timestamp>0</timestamp>
|
<timestamp>1780763070093</timestamp>
|
||||||
|
<sourcestarttime source="timecode">3600/1</sourcestarttime>
|
||||||
<viewer>
|
<viewer>
|
||||||
<workarea version="1">
|
<workarea version="1">
|
||||||
<enabled>0</enabled>
|
<enabled>0</enabled>
|
||||||
@@ -136,7 +182,7 @@
|
|||||||
</viewer>
|
</viewer>
|
||||||
</custom>
|
</custom>
|
||||||
</node>
|
</node>
|
||||||
<node version="1" id="org.olivevideoeditor.Olive.sequence" ptr="94695150363552">
|
<node version="1" id="org.olivevideoeditor.Olive.sequence" ptr="94432914494208">
|
||||||
<label>Fixture Sequence</label>
|
<label>Fixture Sequence</label>
|
||||||
<input id="enabled_in">
|
<input id="enabled_in">
|
||||||
<primary>
|
<primary>
|
||||||
@@ -182,8 +228,8 @@
|
|||||||
<width>1920</width>
|
<width>1920</width>
|
||||||
<height>1080</height>
|
<height>1080</height>
|
||||||
<depth>1</depth>
|
<depth>1</depth>
|
||||||
<timebase>1/10</timebase>
|
<timebase>1001/30000</timebase>
|
||||||
<format>3</format>
|
<format>4</format>
|
||||||
<channelcount>4</channelcount>
|
<channelcount>4</channelcount>
|
||||||
<pixelaspectratio>1/1</pixelaspectratio>
|
<pixelaspectratio>1/1</pixelaspectratio>
|
||||||
<interlacing>0</interlacing>
|
<interlacing>0</interlacing>
|
||||||
@@ -193,7 +239,7 @@
|
|||||||
<y>0</y>
|
<y>0</y>
|
||||||
<streamindex>0</streamindex>
|
<streamindex>0</streamindex>
|
||||||
<videotype>0</videotype>
|
<videotype>0</videotype>
|
||||||
<framerate>10/1</framerate>
|
<framerate>30000/1001</framerate>
|
||||||
<starttime>0</starttime>
|
<starttime>0</starttime>
|
||||||
<duration>0</duration>
|
<duration>0</duration>
|
||||||
<premultipliedalpha>0</premultipliedalpha>
|
<premultipliedalpha>0</premultipliedalpha>
|
||||||
@@ -278,11 +324,19 @@
|
|||||||
</standard>
|
</standard>
|
||||||
</primary>
|
</primary>
|
||||||
</input>
|
</input>
|
||||||
|
<connections>
|
||||||
|
<connection input="samples_in" element="-1">
|
||||||
|
<output>94432914429312</output>
|
||||||
|
</connection>
|
||||||
|
<connection input="tex_in" element="-1">
|
||||||
|
<output>94432914429312</output>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
<caches>
|
<caches>
|
||||||
<audio>{c312cebb-c206-4be3-8685-44b1cefd2000}</audio>
|
<audio>{102fc5ac-7748-46ee-b0a9-6face77861da}</audio>
|
||||||
<video>{6f929c83-c178-43de-8427-7f2aea486c96}</video>
|
<video>{f2b74020-3926-4c05-a62a-179301cec7d6}</video>
|
||||||
<thumb>{edd6c747-f4a8-4ecd-869c-e2d522ea648f}</thumb>
|
<thumb>{efb47fba-1935-49ef-8578-c8eee2792a98}</thumb>
|
||||||
<waveform>{654584d4-b0c7-40d7-9c12-95971f981e69}</waveform>
|
<waveform>{faac7cd3-084d-40d6-a352-2622d65d3502}</waveform>
|
||||||
</caches>
|
</caches>
|
||||||
<custom>
|
<custom>
|
||||||
<workarea version="1">
|
<workarea version="1">
|
||||||
@@ -297,7 +351,7 @@
|
|||||||
<settings>
|
<settings>
|
||||||
<colorreferencespace>scene_linear</colorreferencespace>
|
<colorreferencespace>scene_linear</colorreferencespace>
|
||||||
<defaultinputcolorspace>Rec.709 OETF</defaultinputcolorspace>
|
<defaultinputcolorspace>Rec.709 OETF</defaultinputcolorspace>
|
||||||
<root>94695150158256</root>
|
<root>94432914284304</root>
|
||||||
</settings>
|
</settings>
|
||||||
</project>
|
</project>
|
||||||
<layout version="1">
|
<layout version="1">
|
||||||
|
|||||||
Reference in New Issue
Block a user