build: split the engine into liboakengine.so; worker drops the UI entirely

Physical split: app/{audio,cli,codec,common,config,node,pluginSupport,
render,task,timeline,undo,tool,shaders} plus coreengine, version and
ui/icons+colorcoding move to a new top-level engine/ tree, built as
liboakengine.so (shared). The render backends (oakgl/oakvulkan) move
with it and link the engine library instead of embedding a static
render-core subset (libolive-rendercore is gone).

- oak-render-worker now links liboakengine instead of the whole
  libolive-editor object set: 336MB -> 2.9MB, no Qt Widgets UI
- the editor links liboakengine for the engine and keeps only UI
  objects in libolive-editor
- install/packaging: GNUInstallDirs libdir on Linux, bundle copy on
  macOS, oakengine.dll staged for NSIS, AppImage validation entry
- fix backend lookup for the new layout: DynamicRenderer searched
  ../app but backends now live in engine/; a stale pre-split liboakgl
  in the build tree got dlopened instead, re-initialized and later
  destroyed the interposed engine statics (full-suite segfault at
  DialogSequenceParameterTab, found via gdb watchpoint)
This commit is contained in:
2026-07-20 03:23:28 +08:00
parent 026ff94b5e
commit 28c4426236
604 changed files with 243 additions and 172 deletions
+473
View File
@@ -0,0 +1,473 @@
/***
Olive - Non-Linear Video Editor
Copyright (C) 2022 Olive Team
Modifications Copyright (C) 2025 mikesolar
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/>.
***/
#include "ffmpegencoder.h"
#include <cstring>
#include <vector>
#include <QFile>
#include "common/ffmpegutils.h"
namespace olive
{
FFmpegEncoder::FFmpegEncoder(const EncodingParams &params)
: Encoder(params)
, encoder_(nullptr)
, open_(false)
{
}
bool FFmpegEncoder::get_color_tags_for_colorspace(const QString &colorspace,
int *primaries, int *trc,
int *matrix)
{
const QString name = colorspace.toLower();
if (name.contains(QStringLiteral("pq")) ||
name.contains(QStringLiteral("2084"))) {
*primaries = fb_color_primaries_bt2020;
*trc = fb_color_trc_pq;
*matrix = fb_col_spc_b_t2020_ncl;
return true;
}
if (name.contains(QStringLiteral("hlg"))) {
*primaries = fb_color_primaries_bt2020;
*trc = fb_color_trc_hlg;
*matrix = fb_col_spc_b_t2020_ncl;
return true;
}
if (name.contains(QStringLiteral("2020"))) {
*primaries = fb_color_primaries_bt2020;
*trc = fb_color_trc_bt709;
*matrix = fb_col_spc_b_t2020_ncl;
return true;
}
if (name.contains(QStringLiteral("p3"))) {
*primaries = fb_color_primaries_smpte432;
*trc = fb_color_trc_srgb;
*matrix = fb_col_spc_b_t709;
return true;
}
if (name.contains(QStringLiteral("srgb"))) {
*primaries = fb_color_primaries_bt709;
*trc = fb_color_trc_srgb;
*matrix = fb_col_spc_b_t709;
return true;
}
if (name.contains(QStringLiteral("pal"))) {
*primaries = fb_color_primaries_bt470bg;
*trc = fb_color_trc_gamma28;
*matrix = fb_col_spc_b_t470_bg;
return true;
}
if (name.contains(QStringLiteral("ntsc"))) {
*primaries = fb_color_primaries_smpte170m;
*trc = fb_color_trc_smpte170m;
*matrix = fb_col_spc_smpt_e170_m;
return true;
}
if (name.contains(QStringLiteral("1886")) ||
name.contains(QStringLiteral("709"))) {
*primaries = fb_color_primaries_bt709;
*trc = fb_color_trc_bt709;
*matrix = fb_col_spc_b_t709;
return true;
}
return false;
}
QStringList FFmpegEncoder::get_pixel_formats_for_codec(ExportCodec::Codec c) const
{
QStringList pix_fmts;
int bridge_codec = export_codec_to_bridge(c);
if (bridge_codec != fb_codec_none) {
int count =
fb_encoder_codec_get_pixel_formats(bridge_codec, nullptr, 0);
if (count > 0) {
std::vector<const char *> names(static_cast<size_t>(count));
fb_encoder_codec_get_pixel_formats(bridge_codec, names.data(),
count);
for (int i = 0; i < count; i++) {
pix_fmts.append(QString::fromUtf8(names[size_t(i)]));
}
}
}
return pix_fmts;
}
std::vector<SampleFormat>
FFmpegEncoder::get_sample_formats_for_codec(ExportCodec::Codec c) const
{
std::vector<SampleFormat> f;
if (c == ExportCodec::k_codec_pcm) {
// FFmpeg lists these as separate codecs so we need custom functionality here
// We list signed 16 first because ExportDialog will always use the first element by default
// (because first element is the "default" in FFmpeg)
f = { SampleFormat::s16, SampleFormat::u8, SampleFormat::s32,
SampleFormat::s64, SampleFormat::f32, SampleFormat::f64 };
} else {
int bridge_codec = export_codec_to_bridge(c);
if (bridge_codec != fb_codec_none) {
int count =
fb_encoder_codec_get_sample_formats(bridge_codec, nullptr, 0);
if (count > 0) {
std::vector<int> fmts(static_cast<size_t>(count));
fb_encoder_codec_get_sample_formats(bridge_codec, fmts.data(),
count);
for (int fmt : fmts) {
SampleFormat native =
FFmpegUtils::get_native_sample_format(fmt);
if (native != SampleFormat::invalid) {
f.push_back(native);
}
}
}
}
}
return f;
}
bool FFmpegEncoder::open()
{
if (open_) {
return true;
}
// Convert QString to C string
QByteArray filename_bytes = params().filename().toUtf8();
FBEncoderConfig config;
memset(&config, 0, sizeof(config));
config.filename = filename_bytes.constData();
// Storage keeping C strings alive until fb_encoder_create deep-copies them
QByteArray pix_fmt_bytes;
QByteArray subtitle_header;
std::vector<QByteArray> opt_key_storage;
std::vector<QByteArray> opt_value_storage;
std::vector<const char *> opt_keys;
std::vector<const char *> opt_values;
// Set up video if it's enabled
if (params().video_enabled()) {
config.video_enabled = 1;
config.video_codec = export_codec_to_bridge(params().video_codec());
config.video_width = params().video_params().width();
config.video_height = params().video_params().height();
config.video_pixel_aspect_num =
params().video_params().pixel_aspect_ratio().numerator();
config.video_pixel_aspect_den =
params().video_params().pixel_aspect_ratio().denominator();
config.video_time_base_num =
params().video_params().frame_rate_as_time_base().numerator();
config.video_time_base_den =
params().video_params().frame_rate_as_time_base().denominator();
config.video_frame_rate_num =
params().video_params().frame_rate().numerator();
config.video_frame_rate_den =
params().video_params().frame_rate().denominator();
pix_fmt_bytes = params().video_pix_fmt().toUtf8();
config.video_pix_fmt = pix_fmt_bytes.constData();
// This is the format we will expect frames received in Write() to be in
PixelFormat native_pixel_fmt = params().video_params().format();
// This is the format we will need to convert the frame to for the bridge to understand it
video_conversion_fmt_ =
FFmpegUtils::get_compatible_pixel_format(native_pixel_fmt);
// These are the equivalent pixel formats as bridge pixel formats
int src_alpha_pix_fmt = FFmpegUtils::get_f_fmpeg_pixel_format(
video_conversion_fmt_, VideoParams::k_rgba_channel_count);
int src_noalpha_pix_fmt = FFmpegUtils::get_f_fmpeg_pixel_format(
video_conversion_fmt_, VideoParams::k_rgb_channel_count);
if (src_alpha_pix_fmt == fb_pix_fmt_none ||
src_noalpha_pix_fmt == fb_pix_fmt_none) {
set_error(
tr("Failed to find suitable pixel format for this buffer"));
return false;
}
config.video_src_pix_fmt = src_alpha_pix_fmt;
config.video_color_range =
params().video_params().color_range() ==
VideoParams::k_color_range_full ?
fb_color_range_jpeg :
fb_color_range_mpeg;
switch (params().video_params().interlacing()) {
case VideoParams::k_interlaced_top_first:
config.video_field_order = fb_field_order_tt;
break;
case VideoParams::k_interlaced_bottom_first:
config.video_field_order = fb_field_order_bb;
break;
default:
config.video_field_order = fb_field_order_progressive;
break;
}
config.video_bit_rate = params().video_bit_rate();
config.video_min_bit_rate = params().video_min_bit_rate();
config.video_max_bit_rate = params().video_max_bit_rate();
config.video_buffer_size = params().video_buffer_size();
config.video_threads = params().video_threads();
config.video_color_srgb =
params().color_transform().output().contains(
QStringLiteral("sRGB"), Qt::CaseInsensitive) ?
1 :
0;
// Derive explicit nclc tags (HDR etc.) from the export colorspace;
// the bridge falls back to the legacy sRGB/Rec.709 logic when these
// are unspecified
int color_primaries = fb_color_primaries_unspec;
int color_trc = fb_color_trc_unspec;
int color_matrix = fb_col_spc_unspec;
get_color_tags_for_colorspace(params().color_transform().output(),
&color_primaries, &color_trc,
&color_matrix);
config.video_color_primaries = color_primaries;
config.video_color_trc = color_trc;
config.video_colorspace = color_matrix;
// Custom options (skip Olive-internal keys)
for (auto i = params().video_opts().begin();
i != params().video_opts().end(); i++) {
if (!i.key().startsWith(QStringLiteral("ove_"))) {
opt_key_storage.push_back(i.key().toUtf8());
opt_value_storage.push_back(i.value().toUtf8());
}
}
for (size_t i = 0; i < opt_key_storage.size(); i++) {
opt_keys.push_back(opt_key_storage[i].constData());
opt_values.push_back(opt_value_storage[i].constData());
}
config.video_opt_keys = opt_keys.data();
config.video_opt_values = opt_values.data();
config.video_opt_count = int(opt_keys.size());
}
// Set up audio if it's enabled
if (params().audio_enabled()) {
config.audio_enabled = 1;
config.audio_codec = export_codec_to_bridge(params().audio_codec());
config.audio_sample_rate = params().audio_params().sample_rate();
config.audio_channel_layout_mask =
params().audio_params().channel_layout();
config.audio_sample_format = FFmpegUtils::get_f_fmpeg_sample_format(
params().audio_params().format());
config.audio_bit_rate = params().audio_bit_rate();
}
// Set up subtitles if they're enabled
if (params().subtitles_enabled()) {
config.subtitles_enabled = 1;
config.subtitle_codec = export_codec_to_bridge(params().subtitles_codec());
subtitle_header = SubtitleParams::generate_ass_header().toUtf8();
config.subtitle_header =
reinterpret_cast<const uint8_t *>(subtitle_header.constData());
config.subtitle_header_size = subtitle_header.size();
}
encoder_ = fb_encoder_create(&config);
if (!encoder_) {
set_error(tr("Failed to create encoder"));
return false;
}
if (fb_encoder_open(encoder_) != 0) {
set_error_from_bridge();
fb_encoder_free(&encoder_);
return false;
}
open_ = true;
return true;
}
bool FFmpegEncoder::write_frame(FramePtr frame, Rational time)
{
// We may need to convert this frame to a frame that the bridge will understand
if (frame->format() != video_conversion_fmt_) {
frame = frame->convert(video_conversion_fmt_);
}
int src_pix_fmt = FFmpegUtils::get_f_fmpeg_pixel_format(frame->format(),
frame->channel_count());
int r = fb_encoder_write_video_frame(
encoder_, frame->width(), frame->height(), src_pix_fmt,
reinterpret_cast<const uint8_t *>(frame->data()),
frame->linesize_bytes(), time.to_double());
if (r != 0) {
set_error_from_bridge();
return false;
}
return true;
}
bool FFmpegEncoder::write_audio(const SampleBuffer &audio)
{
if (!audio.is_allocated()) {
return true;
}
const AudioParams &audio_params = audio.audio_params().is_valid() ?
audio.audio_params() :
params().audio_params();
std::vector<const uint8_t *> channel_data(
size_t(audio.audio_params().channel_count()));
for (size_t i = 0; i < channel_data.size(); i++) {
channel_data[i] =
reinterpret_cast<const uint8_t *>(audio.data(int(i)));
}
int r = fb_encoder_write_audio(
encoder_, channel_data.data(),
audio.audio_params().channel_count(),
FFmpegUtils::get_f_fmpeg_sample_format(audio.audio_params().format()),
audio_params.sample_rate(), audio_params.channel_layout(),
int64_t(audio.sample_count()));
if (r != 0) {
set_error_from_bridge();
return false;
}
return true;
}
bool FFmpegEncoder::write_audio_data(const AudioParams &audio_params,
const uint8_t **data,
int input_sample_count)
{
int r = fb_encoder_write_audio(
encoder_, data, audio_params.channel_count(),
FFmpegUtils::get_f_fmpeg_sample_format(audio_params.format()),
audio_params.sample_rate(), audio_params.channel_layout(),
input_sample_count);
if (r != 0) {
set_error_from_bridge();
return false;
}
return true;
}
bool FFmpegEncoder::write_subtitle(const SubtitleBlock *sub_block)
{
QByteArray utf8_sub = sub_block->get_text().toUtf8();
int r = fb_encoder_write_subtitle(encoder_, utf8_sub.constData(),
sub_block->in().to_double(),
sub_block->length().to_double());
if (r != 0) {
set_error_from_bridge();
return false;
}
return true;
}
void FFmpegEncoder::close()
{
if (encoder_) {
// Flushes encoders, writes the trailer, and frees everything
fb_encoder_free(&encoder_);
}
open_ = false;
}
void FFmpegEncoder::set_error_from_bridge()
{
set_error(QString::fromUtf8(fb_encoder_get_error(encoder_)));
}
int FFmpegEncoder::export_codec_to_bridge(ExportCodec::Codec c)
{
switch (c) {
case ExportCodec::k_codec_h264:
return fb_codec_h264;
case ExportCodec::k_codec_h264rgb:
return fb_codec_h264_rgb;
case ExportCodec::k_codec_d_nx_hd:
return fb_codec_dnxhd;
case ExportCodec::k_codec_pro_res:
return fb_codec_prores;
case ExportCodec::k_codec_cineform:
return fb_codec_cineform;
case ExportCodec::k_codec_h265:
return fb_codec_h265;
case ExportCodec::k_codec_v_p9:
return fb_codec_v_p9;
case ExportCodec::k_codec_a_v1:
return fb_codec_a_v1;
case ExportCodec::k_codec_open_exr:
return fb_codec_openexr;
case ExportCodec::k_codec_png:
return fb_codec_png;
case ExportCodec::k_codec_tiff:
return fb_codec_tiff;
case ExportCodec::k_codec_m_p2:
return fb_codec_m_p2;
case ExportCodec::k_codec_m_p3:
return fb_codec_m_p3;
case ExportCodec::k_codec_aac:
return fb_codec_aac;
case ExportCodec::k_codec_pcm:
return fb_codec_pcm;
case ExportCodec::k_codec_flac:
return fb_codec_flac;
case ExportCodec::k_codec_opus:
return fb_codec_opus;
case ExportCodec::k_codec_vorbis:
return fb_codec_vorbis;
case ExportCodec::k_codec_srt:
return fb_codec_srt;
case ExportCodec::k_codec_count:
break;
}
return fb_codec_none;
}
}