export: dialog execution path now goes through the facade
- oakengine_export_render_ex covers the dialog's entire option surface (formats, codecs, pix fmts, audio params, ranges incl. still frame, subtitles, scaling, threads, custom OCIO color transform names, per-codec key/value options) - zero feature reduction - the dialog's Start now creates a FacadeExportTask that drives oakengine_export_render_ex instead of constructing ExportTask in the UI; progress flows through the facade callback and cancel through oakengine_export_cancel (OAKENGINE_E_CANCELLED preserves the keep-dialog-open semantics) - two real fixes: audio sample format 0 no longer means an AAC-unsupported u8_p (default is f32_p), and image-sequence exports use the engine's real [#####] placeholder instead of a made-up -%04d
This commit is contained in:
@@ -33,11 +33,13 @@
|
|||||||
|
|
||||||
#include "common/digit.h"
|
#include "common/digit.h"
|
||||||
#include "common/qtutils.h"
|
#include "common/qtutils.h"
|
||||||
|
#include "codec/ffmpeg/ffmpegencoder.h"
|
||||||
#include "dialog/msgbox.h"
|
#include "dialog/msgbox.h"
|
||||||
#include "dialog/task/task.h"
|
#include "dialog/task/task.h"
|
||||||
#include "exportsavepresetdialog.h"
|
#include "exportsavepresetdialog.h"
|
||||||
#include "node/project.h"
|
#include "node/project.h"
|
||||||
#include "node/project/sequence/sequence.h"
|
#include "node/project/sequence/sequence.h"
|
||||||
|
#include "oakengine/exporter.h"
|
||||||
#include "task/taskmanager.h"
|
#include "task/taskmanager.h"
|
||||||
#include "ui/icons/icons.h"
|
#include "ui/icons/icons.h"
|
||||||
#include "widget/timeruler/timeruler.h"
|
#include "widget/timeruler/timeruler.h"
|
||||||
@@ -47,6 +49,164 @@ namespace olive
|
|||||||
|
|
||||||
#define super QDialog
|
#define super QDialog
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// pix_fmt string (e.g. "yuv420p") to its index in the codec's supported
|
||||||
|
// list; 0 (the codec's preferred format) when absent.
|
||||||
|
int pix_fmt_index(ExportCodec::Codec codec, const QString &pix_fmt)
|
||||||
|
{
|
||||||
|
if (pix_fmt.isEmpty()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
FFmpegEncoder probe{ EncodingParams() };
|
||||||
|
const int index = probe.get_pixel_formats_for_codec(codec).indexOf(pix_fmt);
|
||||||
|
return index >= 0 ? index : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodingParams (assembled by the dialog) -> facade POD. One-to-one with
|
||||||
|
// oak_export_options_ex; see oakengine/exporter.h for the field docs.
|
||||||
|
oak_export_options_ex params_to_ex(const EncodingParams &p)
|
||||||
|
{
|
||||||
|
oak_export_options_ex o = {};
|
||||||
|
|
||||||
|
const VideoParams &vp = p.video_params();
|
||||||
|
const Rational tb = vp.frame_rate().flipped();
|
||||||
|
|
||||||
|
if (p.has_custom_range()) {
|
||||||
|
o.range_mode = OAKENGINE_EXPORT_RANGE_CUSTOM;
|
||||||
|
o.range_in_ts = Timecode::time_to_timestamp(p.custom_range().in(), tb);
|
||||||
|
o.range_out_ts =
|
||||||
|
Timecode::time_to_timestamp(p.custom_range().out(), tb);
|
||||||
|
} else {
|
||||||
|
o.range_mode = OAKENGINE_EXPORT_RANGE_ENTIRE;
|
||||||
|
}
|
||||||
|
|
||||||
|
o.format = int(p.format());
|
||||||
|
o.video_enabled = p.video_enabled() ? 1 : 0;
|
||||||
|
o.video_codec = int(p.video_codec());
|
||||||
|
o.audio_enabled = p.audio_enabled() ? 1 : 0;
|
||||||
|
o.audio_codec = int(p.audio_codec());
|
||||||
|
o.subtitles_enabled = p.subtitles_enabled() ? 1 : 0;
|
||||||
|
o.subtitles_sidecar = p.subtitles_are_sidecar() ? 1 : 0;
|
||||||
|
o.subtitles_format =
|
||||||
|
p.subtitles_are_sidecar() ? int(p.subtitle_sidecar_fmt()) : 0;
|
||||||
|
o.subtitles_codec = p.subtitles_enabled() ? int(p.subtitles_codec()) : 0;
|
||||||
|
|
||||||
|
o.video_bit_rate = p.video_bit_rate();
|
||||||
|
o.audio_bit_rate = p.audio_bit_rate();
|
||||||
|
o.video_pix_fmt = pix_fmt_index(p.video_codec(), p.video_pix_fmt());
|
||||||
|
|
||||||
|
o.audio_sample_rate = p.audio_params().sample_rate();
|
||||||
|
o.audio_channel_layout = p.audio_params().channel_layout();
|
||||||
|
o.audio_sample_format = int(p.audio_params().format());
|
||||||
|
|
||||||
|
const QString ct = p.color_transform().output();
|
||||||
|
if (ct.isEmpty()) {
|
||||||
|
o.color_transform = OAKENGINE_EXPORT_COLOR_REFERENCE;
|
||||||
|
} else if (ct == QStringLiteral("sRGB OETF")) {
|
||||||
|
o.color_transform = OAKENGINE_EXPORT_COLOR_SRGB_OETF;
|
||||||
|
} else if (ct == QStringLiteral("Rec.709 OETF")) {
|
||||||
|
o.color_transform = OAKENGINE_EXPORT_COLOR_REC709_OETF;
|
||||||
|
} else if (ct == QStringLiteral("BT.1886 EOTF")) {
|
||||||
|
o.color_transform = OAKENGINE_EXPORT_COLOR_BT1886_EOTF;
|
||||||
|
} else {
|
||||||
|
o.color_transform = OAKENGINE_EXPORT_COLOR_CUSTOM;
|
||||||
|
const QByteArray utf = ct.toUtf8();
|
||||||
|
snprintf(o.color_transform_name, sizeof(o.color_transform_name),
|
||||||
|
"%s", utf.constData());
|
||||||
|
}
|
||||||
|
|
||||||
|
o.video_width = vp.width();
|
||||||
|
o.video_height = vp.height();
|
||||||
|
o.frame_rate_num = vp.frame_rate().numerator();
|
||||||
|
o.frame_rate_den = vp.frame_rate().denominator();
|
||||||
|
o.pixel_aspect_num = vp.pixel_aspect_ratio().numerator();
|
||||||
|
o.pixel_aspect_den = vp.pixel_aspect_ratio().denominator();
|
||||||
|
o.interlacing = int(vp.interlacing());
|
||||||
|
o.pixel_format = int(vp.format());
|
||||||
|
o.scaling_method = int(p.video_scaling_method());
|
||||||
|
o.color_range = int(vp.color_range());
|
||||||
|
o.video_threads = p.video_threads();
|
||||||
|
o.is_image_sequence = p.video_is_image_sequence() ? 1 : 0;
|
||||||
|
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief ExportTask replacement driven by the liboakengine C ABI facade
|
||||||
|
*
|
||||||
|
* Same Task contract as the engine's ExportTask (progress via
|
||||||
|
* progress_changed, cancel via CancelEvent), but the actual
|
||||||
|
* render+encode goes through oakengine_export_render_ex(): the facade
|
||||||
|
* owns the ExportTask instance, its event-loop drive and the conform
|
||||||
|
* prewarm. Cancellation is forwarded to the facade
|
||||||
|
* (oakengine_export_cancel()), which reports OAKENGINE_E_CANCELLED back.
|
||||||
|
*/
|
||||||
|
class FacadeExportTask : public Task {
|
||||||
|
public:
|
||||||
|
FacadeExportTask(ViewerOutput *viewer_node, const EncodingParams ¶ms)
|
||||||
|
: sequence_(reinterpret_cast<OakEngineSequence *>(viewer_node))
|
||||||
|
, params_(params)
|
||||||
|
{
|
||||||
|
set_title(tr("Exporting \"%1\"").arg(viewer_node->get_label()));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual bool run() override
|
||||||
|
{
|
||||||
|
oak_export_options_ex o = params_to_ex(params_);
|
||||||
|
// Pass the codec section's encoder-specific options through.
|
||||||
|
for (auto it = params_.video_opts().cbegin();
|
||||||
|
it != params_.video_opts().cend(); ++it) {
|
||||||
|
oakengine_export_set_video_option(it.key().toUtf8().constData(),
|
||||||
|
it.value().toUtf8().constData());
|
||||||
|
}
|
||||||
|
oakengine_export_set_progress_callback(
|
||||||
|
&FacadeExportTask::forward_progress, this);
|
||||||
|
const int rc = oakengine_export_render_ex(
|
||||||
|
sequence_, params_.filename().toUtf8().constData(), &o);
|
||||||
|
oakengine_export_set_progress_callback(nullptr, nullptr);
|
||||||
|
oakengine_export_set_video_option("", nullptr);
|
||||||
|
|
||||||
|
if (rc == OAKENGINE_E_CANCELLED) {
|
||||||
|
// Mirror the engine task's cancelled state for TaskDialog.
|
||||||
|
cancel();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (rc != OAKENGINE_OK) {
|
||||||
|
char err[1024];
|
||||||
|
err[0] = '\0';
|
||||||
|
oakengine_export_last_error(err, sizeof(err));
|
||||||
|
set_error(err[0] ? QString::fromUtf8(err) :
|
||||||
|
QStringLiteral("Export failed"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void CancelEvent() override
|
||||||
|
{
|
||||||
|
oakengine_export_cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static void forward_progress(double fraction, void *userdata)
|
||||||
|
{
|
||||||
|
static_cast<FacadeExportTask *>(userdata)->emit_progress(fraction);
|
||||||
|
}
|
||||||
|
|
||||||
|
void emit_progress(double fraction)
|
||||||
|
{
|
||||||
|
emit progress_changed(fraction);
|
||||||
|
}
|
||||||
|
|
||||||
|
OakEngineSequence *sequence_;
|
||||||
|
EncodingParams params_;
|
||||||
|
};
|
||||||
|
|
||||||
ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode,
|
ExportDialog::ExportDialog(ViewerOutput *viewer_node, bool stills_only_mode,
|
||||||
QWidget *parent)
|
QWidget *parent)
|
||||||
: super(parent)
|
: super(parent)
|
||||||
@@ -398,8 +558,8 @@ void ExportDialog::start_export()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ExportTask *task =
|
FacadeExportTask *task =
|
||||||
new ExportTask(viewer_node_, color_manager_, generate_params());
|
new FacadeExportTask(viewer_node_, generate_params());
|
||||||
|
|
||||||
if (export_bkg_box_->isChecked()) {
|
if (export_bkg_box_->isChecked()) {
|
||||||
// Send to TaskManager to export in background
|
// Send to TaskManager to export in background
|
||||||
|
|||||||
@@ -28,13 +28,13 @@
|
|||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QProgressBar>
|
#include <QProgressBar>
|
||||||
|
|
||||||
|
#include "codec/encoder.h"
|
||||||
#include "codec/exportcodec.h"
|
#include "codec/exportcodec.h"
|
||||||
#include "codec/exportformat.h"
|
#include "codec/exportformat.h"
|
||||||
#include "dialog/export/exportformatcombobox.h"
|
#include "dialog/export/exportformatcombobox.h"
|
||||||
#include "exportaudiotab.h"
|
#include "exportaudiotab.h"
|
||||||
#include "exportsubtitlestab.h"
|
#include "exportsubtitlestab.h"
|
||||||
#include "exportvideotab.h"
|
#include "exportvideotab.h"
|
||||||
#include "task/export/export.h"
|
|
||||||
#include "widget/nodeparamview/nodeparamviewwidgetbridge.h"
|
#include "widget/nodeparamview/nodeparamviewwidgetbridge.h"
|
||||||
#include "widget/viewer/viewer.h"
|
#include "widget/viewer/viewer.h"
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,10 @@ typedef struct oak_export_options {
|
|||||||
* The call blocks until the export finishes. Progress is reported through
|
* The call blocks until the export finishes. Progress is reported through
|
||||||
* the callback set with oakengine_export_set_progress_callback().
|
* the callback set with oakengine_export_set_progress_callback().
|
||||||
*
|
*
|
||||||
|
* Image sequences: `path` is the filename template; the engine's frame
|
||||||
|
* placeholder ("_[#####]") is inserted before the extension when absent
|
||||||
|
* (same bracketed-hash form the application uses).
|
||||||
|
*
|
||||||
* @return OAKENGINE_OK on success; OAKENGINE_E_INVALID for bad arguments;
|
* @return OAKENGINE_OK on success; OAKENGINE_E_INVALID for bad arguments;
|
||||||
* OAKENGINE_E_STATE when the engine lacks OAKENGINE_INIT_RENDER;
|
* OAKENGINE_E_STATE when the engine lacks OAKENGINE_INIT_RENDER;
|
||||||
* OAKENGINE_E_FAILED for render/encode failures (see
|
* OAKENGINE_E_FAILED for render/encode failures (see
|
||||||
@@ -144,6 +148,158 @@ OAKENGINE_API void
|
|||||||
oakengine_export_set_progress_callback(oakengine_export_progress_fn fn,
|
oakengine_export_set_progress_callback(oakengine_export_progress_fn fn,
|
||||||
void *userdata);
|
void *userdata);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Cancel the currently running oakengine_export_render()/_ex()
|
||||||
|
* call, if any (from another thread).
|
||||||
|
*
|
||||||
|
* The export aborts at the next opportunity and the blocked render call
|
||||||
|
* returns OAKENGINE_E_CANCELLED. A no-op when no export is running.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API void oakengine_export_cancel(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Cancellation return code of oakengine_export_render()/_ex().
|
||||||
|
*/
|
||||||
|
#define OAKENGINE_E_CANCELLED (-5)
|
||||||
|
|
||||||
|
/* ---- Extended export (oakengine_export_render_ex) -------------------------
|
||||||
|
*
|
||||||
|
* The extended entry point covers every field of the engine's
|
||||||
|
* EncodingParams (engine/codec/encoder.h) through the POD below, so
|
||||||
|
* full-featured consumers (like the application's export dialog) can drive
|
||||||
|
* the same export path without engine headers. Enum int fields carry the
|
||||||
|
* engine's own enum values (olive::ExportFormat::Format,
|
||||||
|
* olive::ExportCodec::Codec, olive::VideoParams::Interlacing/ColorRange,
|
||||||
|
* olive::core::SampleFormat::Format) unless documented otherwise; <= 0
|
||||||
|
* (or -1 where noted) selects the documented default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @brief range_mode values: what part of the sequence to export. */
|
||||||
|
#define OAKENGINE_EXPORT_RANGE_ENTIRE 0 /**< Whole sequence. */
|
||||||
|
#define OAKENGINE_EXPORT_RANGE_CUSTOM 1 /**< [range_in_ts, range_out_ts). */
|
||||||
|
#define OAKENGINE_EXPORT_RANGE_STILL 2 /**< Single frame at still_time_ts. */
|
||||||
|
|
||||||
|
/** @brief color_transform values: output color transform. */
|
||||||
|
#define OAKENGINE_EXPORT_COLOR_SRGB_OETF 0 /**< sRGB OETF (dialog default). */
|
||||||
|
#define OAKENGINE_EXPORT_COLOR_REC709_OETF 1 /**< Rec.709 OETF. */
|
||||||
|
#define OAKENGINE_EXPORT_COLOR_REFERENCE 2 /**< Reference space, no transform. */
|
||||||
|
#define OAKENGINE_EXPORT_COLOR_BT1886_EOTF 3 /**< BT.1886 EOTF. */
|
||||||
|
#define OAKENGINE_EXPORT_COLOR_CUSTOM (-1) /**< Use color_transform_name. */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Full export parameters (POD; covers EncodingParams).
|
||||||
|
*/
|
||||||
|
typedef struct oak_export_options_ex {
|
||||||
|
/** OAKENGINE_EXPORT_RANGE_*; default ENTIRE. */
|
||||||
|
int range_mode;
|
||||||
|
/** range_mode == CUSTOM: range [in, out) as frame timestamps in the
|
||||||
|
* sequence's frame-rate timebase. */
|
||||||
|
int64_t range_in_ts;
|
||||||
|
int64_t range_out_ts;
|
||||||
|
/** range_mode == STILL: the frame to export as a timestamp. */
|
||||||
|
int64_t still_time_ts;
|
||||||
|
|
||||||
|
/** Container format as olive::ExportFormat::Format
|
||||||
|
* (0 = MP4, 5 = PNG sequence, others per engine/codec/exportformat.h). */
|
||||||
|
int format;
|
||||||
|
|
||||||
|
int video_enabled; /**< 0/1; default 1. */
|
||||||
|
/** Video codec as olive::ExportCodec::Codec (1 = H.264, 3 = H.265,
|
||||||
|
* 5 = PNG, others per engine/codec/exportcodec.h). */
|
||||||
|
int video_codec;
|
||||||
|
int audio_enabled; /**< 0/1; default 1. */
|
||||||
|
/** Audio codec as olive::ExportCodec::Codec (11 = AAC, 12 = PCM,
|
||||||
|
* others per engine/codec/exportcodec.h). */
|
||||||
|
int audio_codec;
|
||||||
|
|
||||||
|
int subtitles_enabled; /**< 0/1; default 0. */
|
||||||
|
int subtitles_sidecar; /**< 0 = embedded, 1 = sidecar file; default 0. */
|
||||||
|
/** Sidecar container as olive::ExportFormat::Format (13 = SRT). */
|
||||||
|
int subtitles_format;
|
||||||
|
/** Subtitle codec as olive::ExportCodec::Codec (16 = SRT). */
|
||||||
|
int subtitles_codec;
|
||||||
|
|
||||||
|
/** Video bit rate in bit/s; <= 0 lets the encoder choose. */
|
||||||
|
int64_t video_bit_rate;
|
||||||
|
/** Audio bit rate in bit/s; <= 0 lets the encoder choose. */
|
||||||
|
int64_t audio_bit_rate;
|
||||||
|
|
||||||
|
/** Encoded pixel format as an index into
|
||||||
|
* FFmpegEncoder::get_pixel_formats_for_codec(video_codec)
|
||||||
|
* (0 = the codec's preferred format, e.g. yuv420p for H.264;
|
||||||
|
* -1 = same as 0). Out-of-range indexes fail with
|
||||||
|
* OAKENGINE_E_INVALID. */
|
||||||
|
int video_pix_fmt;
|
||||||
|
|
||||||
|
/** Audio sample rate in Hz; <= 0 uses the sequence's rate. */
|
||||||
|
int audio_sample_rate;
|
||||||
|
/** Audio channel layout mask (0 = the sequence's layout). */
|
||||||
|
uint64_t audio_channel_layout;
|
||||||
|
/** Audio sample format: <= 0 selects the engine's float planar default
|
||||||
|
* (f32_p); >= 1 is an explicit olive::core::SampleFormat::Format value
|
||||||
|
* in engine units (s16_p = 1, s32_p = 2, s64_p = 3, f32_p = 4,
|
||||||
|
* f64_p = 5, u8 = 6, s16 = 7, s32 = 8, s64 = 9, f32 = 10, f64 = 11).
|
||||||
|
* u8_p (raw 0) overlaps the default and is not expressible. */
|
||||||
|
int audio_sample_format;
|
||||||
|
|
||||||
|
/** OAKENGINE_EXPORT_COLOR_*; default SRGB_OETF. */
|
||||||
|
int color_transform;
|
||||||
|
/** OCIO color space name, used when color_transform ==
|
||||||
|
* OAKENGINE_EXPORT_COLOR_CUSTOM. Empty otherwise. */
|
||||||
|
char color_transform_name[64];
|
||||||
|
|
||||||
|
/** Output dimensions; <= 0 uses the sequence's dimensions. */
|
||||||
|
int video_width;
|
||||||
|
int video_height;
|
||||||
|
/** Output frame rate; <= 0 uses the sequence's frame rate. */
|
||||||
|
int frame_rate_num;
|
||||||
|
int frame_rate_den;
|
||||||
|
/** Pixel aspect ratio; <= 0 uses the sequence's ratio. */
|
||||||
|
int pixel_aspect_num;
|
||||||
|
int pixel_aspect_den;
|
||||||
|
/** olive::VideoParams::Interlacing; -1 = the sequence's mode. */
|
||||||
|
int interlacing;
|
||||||
|
/** Render pixel format as olive::core::PixelFormat::Format;
|
||||||
|
* -1 = the OfflinePixelFormat config default. */
|
||||||
|
int pixel_format;
|
||||||
|
/** Scaling method (EncodingParams::VideoScalingMethod):
|
||||||
|
* -1 = fit (default), 0 = fit, 1 = stretch, 2 = crop. */
|
||||||
|
int scaling_method;
|
||||||
|
/** olive::VideoParams::ColorRange; -1 = limited (default). */
|
||||||
|
int color_range;
|
||||||
|
/** Encoder thread count; <= 0 = auto. */
|
||||||
|
int video_threads;
|
||||||
|
/** 1 = write an image sequence (still-image codecs); default 0. */
|
||||||
|
int is_image_sequence;
|
||||||
|
} oak_export_options_ex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Extended synchronous export with full EncodingParams coverage.
|
||||||
|
*
|
||||||
|
* Assembles the engine's EncodingParams from `opts` and runs the same
|
||||||
|
* synchronous ExportTask path as oakengine_export_render() (progress
|
||||||
|
* callback, conform prewarm, event-loop drive). PNG sequences accept a
|
||||||
|
* filename template ("-%04d" is inserted before the extension when
|
||||||
|
* absent), same as the simple entry point.
|
||||||
|
*
|
||||||
|
* @return OAKENGINE_OK on success; OAKENGINE_E_INVALID for bad options;
|
||||||
|
* OAKENGINE_E_STATE without OAKENGINE_INIT_RENDER; OAKENGINE_E_FAILED for
|
||||||
|
* render/encode failures; OAKENGINE_E_CANCELLED after
|
||||||
|
* oakengine_export_cancel().
|
||||||
|
*/
|
||||||
|
OAKENGINE_API int oakengine_export_render_ex(OakEngineSequence *seq,
|
||||||
|
const char *path,
|
||||||
|
const oak_export_options_ex *opts);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set an encoder-specific video option (key/value strings, e.g.
|
||||||
|
* "crf" = "18") applied by subsequent oakengine_export_render()/_ex()
|
||||||
|
* calls on this thread (mirrors EncodingParams::set_video_option()).
|
||||||
|
* Repeated calls accumulate; NULL value clears all options.
|
||||||
|
*/
|
||||||
|
OAKENGINE_API void oakengine_export_set_video_option(const char *key,
|
||||||
|
const char *value);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+362
-62
@@ -51,6 +51,14 @@ thread_local QString g_last_error;
|
|||||||
thread_local oakengine_export_progress_fn g_progress_fn = nullptr;
|
thread_local oakengine_export_progress_fn g_progress_fn = nullptr;
|
||||||
thread_local void *g_progress_userdata = nullptr;
|
thread_local void *g_progress_userdata = nullptr;
|
||||||
|
|
||||||
|
// Encoder-specific video options (per thread), applied to the next
|
||||||
|
// assembled EncodingParams.
|
||||||
|
thread_local QHash<QString, QString> g_video_options;
|
||||||
|
|
||||||
|
// The export currently driven by oakengine_export_render()/_ex(), for
|
||||||
|
// cross-thread cancellation (one at a time per process).
|
||||||
|
std::atomic<olive::ExportTask *> g_current_export{ nullptr };
|
||||||
|
|
||||||
void set_error(const QString &error)
|
void set_error(const QString &error)
|
||||||
{
|
{
|
||||||
g_last_error = error;
|
g_last_error = error;
|
||||||
@@ -117,16 +125,17 @@ bool layout_for_channels(int channels, uint64_t *layout)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For PNG sequences: make sure the filename carries a frame placeholder
|
// For image sequences: make sure the filename carries the engine's frame
|
||||||
// ("-%04d"), inserting one before the extension when absent.
|
// placeholder ("[#####]", the same bracketed-hash form the application
|
||||||
|
// uses), inserting one before the extension when absent.
|
||||||
QString image_sequence_filename(const QString &path)
|
QString image_sequence_filename(const QString &path)
|
||||||
{
|
{
|
||||||
if (olive::Encoder::filename_contains_digit_placeholder(path)) {
|
if (olive::Encoder::filename_contains_digit_placeholder(path)) {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
const QFileInfo fi(path);
|
const QFileInfo fi(path);
|
||||||
return fi.dir().filePath(fi.completeBaseName() + QStringLiteral("-%04d.") +
|
return fi.dir().filePath(fi.completeBaseName() +
|
||||||
fi.suffix());
|
QStringLiteral("_[#####].") + fi.suffix());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-generate audio conforms for every footage with audio streams in the
|
// Pre-generate audio conforms for every footage with audio streams in the
|
||||||
@@ -195,6 +204,287 @@ bool prewarm_conforms(olive::Project *project,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drive a fully-assembled EncodingParams through the ExportTask on a
|
||||||
|
// worker thread while this thread pumps events (see the comment in the
|
||||||
|
// body). Shared by oakengine_export_render() and _render_ex().
|
||||||
|
int render_internal(olive::Sequence *sequence, olive::Project *project,
|
||||||
|
olive::EncodingParams ¶ms, bool prewarm_audio,
|
||||||
|
const olive::AudioParams &prewarm_params)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Pre-generate the audio conforms the render is about to need (the
|
||||||
|
// headless equivalent of the application's preview-warmed cache).
|
||||||
|
// A timeout here is not fatal: the render's own conform wait is the
|
||||||
|
// fallback path.
|
||||||
|
if (prewarm_audio) {
|
||||||
|
QString prewarm_error;
|
||||||
|
prewarm_conforms(project, prewarm_params, &prewarm_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
olive::ExportTask task(sequence, project->color_manager(), params);
|
||||||
|
g_current_export.store(&task);
|
||||||
|
// The progress signal is emitted on the task thread, so the callback
|
||||||
|
// (installed for the calling thread) is captured by value -- reading
|
||||||
|
// the thread_local on the task thread would see NULL.
|
||||||
|
const oakengine_export_progress_fn progress_fn = g_progress_fn;
|
||||||
|
void *const progress_userdata = g_progress_userdata;
|
||||||
|
if (progress_fn) {
|
||||||
|
QObject::connect(&task, &olive::ExportTask::progress_changed,
|
||||||
|
[progress_fn, progress_userdata](double fraction) {
|
||||||
|
progress_fn(fraction, progress_userdata);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drive the task the way the application does: the task runs on a
|
||||||
|
// worker thread while the calling thread keeps its event loop
|
||||||
|
// spinning. A bare synchronous start() deadlocks on audio exports:
|
||||||
|
// audio conforms are delivered to TaskManager via queued calls and
|
||||||
|
// ConformManager::conform_task_finished is queued back to THIS
|
||||||
|
// thread, which must therefore process events while waiting.
|
||||||
|
std::atomic<bool> done{ false };
|
||||||
|
bool result = false;
|
||||||
|
QObject::connect(&task, &olive::ExportTask::finished,
|
||||||
|
[&done, &result](olive::Task *, bool r) {
|
||||||
|
result = r;
|
||||||
|
done.store(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
QThread task_thread;
|
||||||
|
task.moveToThread(&task_thread);
|
||||||
|
task_thread.start();
|
||||||
|
QMetaObject::invokeMethod(&task, "start", Qt::QueuedConnection);
|
||||||
|
while (!done.load()) {
|
||||||
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 20);
|
||||||
|
QThread::msleep(5);
|
||||||
|
}
|
||||||
|
// Move the task back before tearing the thread down (QObjects must
|
||||||
|
// not be destroyed while owned by a dead thread).
|
||||||
|
task.moveToThread(QCoreApplication::instance()->thread());
|
||||||
|
task_thread.quit();
|
||||||
|
task_thread.wait();
|
||||||
|
g_current_export.store(nullptr);
|
||||||
|
|
||||||
|
if (task.is_cancelled()) {
|
||||||
|
set_error(QStringLiteral("export cancelled"));
|
||||||
|
return OAKENGINE_E_CANCELLED;
|
||||||
|
}
|
||||||
|
if (!result) {
|
||||||
|
set_error(task.get_error().isEmpty() ?
|
||||||
|
QStringLiteral("export failed") :
|
||||||
|
task.get_error());
|
||||||
|
return OAKENGINE_E_FAILED;
|
||||||
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
g_current_export.store(nullptr);
|
||||||
|
set_error(QStringLiteral("export failed: %1").arg(e.what()));
|
||||||
|
return OAKENGINE_E_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
return OAKENGINE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble EncodingParams from the extended POD. Returns an error string
|
||||||
|
// on invalid options.
|
||||||
|
QString params_from_ex(const oak_export_options_ex &o,
|
||||||
|
olive::Sequence *sequence, const char *path,
|
||||||
|
olive::EncodingParams *params)
|
||||||
|
{
|
||||||
|
if (o.format < 0 || o.format >= olive::ExportFormat::k_format_count) {
|
||||||
|
return QStringLiteral("unknown export format %1").arg(o.format);
|
||||||
|
}
|
||||||
|
if (o.video_codec < 0 || o.video_codec >= olive::ExportCodec::k_codec_count) {
|
||||||
|
return QStringLiteral("unknown video codec %1").arg(o.video_codec);
|
||||||
|
}
|
||||||
|
if (o.audio_codec < 0 || o.audio_codec >= olive::ExportCodec::k_codec_count) {
|
||||||
|
return QStringLiteral("unknown audio codec %1").arg(o.audio_codec);
|
||||||
|
}
|
||||||
|
if (o.subtitles_enabled &&
|
||||||
|
(o.subtitles_codec < 0 ||
|
||||||
|
o.subtitles_codec >= olive::ExportCodec::k_codec_count)) {
|
||||||
|
return QStringLiteral("unknown subtitle codec %1")
|
||||||
|
.arg(o.subtitles_codec);
|
||||||
|
}
|
||||||
|
|
||||||
|
const olive::ExportCodec::Codec vcodec =
|
||||||
|
static_cast<olive::ExportCodec::Codec>(o.video_codec);
|
||||||
|
const olive::ExportCodec::Codec acodec =
|
||||||
|
static_cast<olive::ExportCodec::Codec>(o.audio_codec);
|
||||||
|
const olive::ExportFormat::Format format =
|
||||||
|
static_cast<olive::ExportFormat::Format>(o.format);
|
||||||
|
|
||||||
|
// Video params: sequence's own, with overrides applied.
|
||||||
|
olive::VideoParams vp = sequence->get_video_params();
|
||||||
|
if (o.video_width > 0) {
|
||||||
|
vp.set_width(o.video_width);
|
||||||
|
}
|
||||||
|
if (o.video_height > 0) {
|
||||||
|
vp.set_height(o.video_height);
|
||||||
|
}
|
||||||
|
if (o.frame_rate_num > 0 && o.frame_rate_den > 0) {
|
||||||
|
vp.set_time_base(olive::Rational(o.frame_rate_den, o.frame_rate_num));
|
||||||
|
}
|
||||||
|
if (o.pixel_aspect_num > 0 && o.pixel_aspect_den > 0) {
|
||||||
|
vp.set_pixel_aspect_ratio(
|
||||||
|
olive::Rational(o.pixel_aspect_num, o.pixel_aspect_den));
|
||||||
|
}
|
||||||
|
if (o.interlacing >= 0) {
|
||||||
|
vp.set_interlacing(
|
||||||
|
static_cast<olive::VideoParams::Interlacing>(o.interlacing));
|
||||||
|
}
|
||||||
|
if (o.color_range >= 0) {
|
||||||
|
vp.set_color_range(
|
||||||
|
static_cast<olive::VideoParams::ColorRange>(o.color_range));
|
||||||
|
}
|
||||||
|
if (o.pixel_format >= 0) {
|
||||||
|
vp.set_format(
|
||||||
|
static_cast<olive::PixelFormat::Format>(o.pixel_format));
|
||||||
|
}
|
||||||
|
if (vp.frame_rate().isNull() || vp.frame_rate().isNaN()) {
|
||||||
|
return QStringLiteral("sequence has no valid frame rate");
|
||||||
|
}
|
||||||
|
if (vp.width() <= 0 || vp.height() <= 0) {
|
||||||
|
return QStringLiteral("sequence has no valid video dimensions");
|
||||||
|
}
|
||||||
|
|
||||||
|
params->set_format(format);
|
||||||
|
QString filename = QString::fromUtf8(path);
|
||||||
|
if (o.is_image_sequence) {
|
||||||
|
params->set_video_is_image_sequence(true);
|
||||||
|
filename = image_sequence_filename(filename);
|
||||||
|
}
|
||||||
|
params->set_filename(filename);
|
||||||
|
|
||||||
|
if (o.video_enabled) {
|
||||||
|
params->enable_video(vp, vcodec);
|
||||||
|
if (o.video_bit_rate > 0) {
|
||||||
|
params->set_video_bit_rate(o.video_bit_rate);
|
||||||
|
}
|
||||||
|
if (o.video_threads > 0) {
|
||||||
|
params->set_video_threads(o.video_threads);
|
||||||
|
}
|
||||||
|
// Encoded pixel format by index into the codec's supported list.
|
||||||
|
if (o.video_pix_fmt >= 0) {
|
||||||
|
olive::FFmpegEncoder probe{ olive::EncodingParams() };
|
||||||
|
const QStringList pix_fmts =
|
||||||
|
probe.get_pixel_formats_for_codec(vcodec);
|
||||||
|
if (o.video_pix_fmt >= pix_fmts.size()) {
|
||||||
|
return QStringLiteral(
|
||||||
|
"pixel format index %1 out of range for codec %2")
|
||||||
|
.arg(o.video_pix_fmt)
|
||||||
|
.arg(o.video_codec);
|
||||||
|
}
|
||||||
|
if (!pix_fmts.isEmpty()) {
|
||||||
|
params->set_video_pix_fmt(
|
||||||
|
pix_fmts.at(qMax(0, o.video_pix_fmt)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.audio_enabled) {
|
||||||
|
olive::AudioParams ap = sequence->get_audio_params();
|
||||||
|
const int sample_rate =
|
||||||
|
o.audio_sample_rate > 0 ? o.audio_sample_rate : ap.sample_rate();
|
||||||
|
const uint64_t layout = o.audio_channel_layout != 0 ?
|
||||||
|
o.audio_channel_layout :
|
||||||
|
ap.channel_layout();
|
||||||
|
const olive::core::SampleFormat::Format sample_format =
|
||||||
|
o.audio_sample_format > 0 ?
|
||||||
|
static_cast<olive::core::SampleFormat::Format>(
|
||||||
|
o.audio_sample_format) :
|
||||||
|
olive::core::SampleFormat::f32_p;
|
||||||
|
if (sample_rate <= 0) {
|
||||||
|
return QStringLiteral("sequence has no valid audio sample rate");
|
||||||
|
}
|
||||||
|
ap = olive::AudioParams(sample_rate, layout, sample_format);
|
||||||
|
params->enable_audio(ap, acodec);
|
||||||
|
if (o.audio_bit_rate > 0) {
|
||||||
|
params->set_audio_bit_rate(o.audio_bit_rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.subtitles_enabled) {
|
||||||
|
if (o.subtitles_sidecar) {
|
||||||
|
params->enable_sidecar_subtitles(
|
||||||
|
static_cast<olive::ExportFormat::Format>(o.subtitles_format),
|
||||||
|
static_cast<olive::ExportCodec::Codec>(o.subtitles_codec));
|
||||||
|
} else {
|
||||||
|
params->enable_subtitles(
|
||||||
|
static_cast<olive::ExportCodec::Codec>(o.subtitles_codec));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Range.
|
||||||
|
const olive::Rational tb = vp.frame_rate().flipped();
|
||||||
|
switch (o.range_mode) {
|
||||||
|
case OAKENGINE_EXPORT_RANGE_CUSTOM: {
|
||||||
|
if (o.range_in_ts < 0 || o.range_out_ts <= o.range_in_ts) {
|
||||||
|
return QStringLiteral("invalid custom range");
|
||||||
|
}
|
||||||
|
const olive::Rational in_time =
|
||||||
|
olive::core::Timecode::timestamp_to_time(o.range_in_ts, tb);
|
||||||
|
const olive::Rational out_time =
|
||||||
|
olive::core::Timecode::timestamp_to_time(o.range_out_ts, tb);
|
||||||
|
params->set_custom_range(olive::TimeRange(in_time, out_time));
|
||||||
|
params->set_export_length(out_time - in_time);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case OAKENGINE_EXPORT_RANGE_STILL: {
|
||||||
|
if (o.still_time_ts < 0) {
|
||||||
|
return QStringLiteral("invalid still time");
|
||||||
|
}
|
||||||
|
const olive::Rational t =
|
||||||
|
olive::core::Timecode::timestamp_to_time(o.still_time_ts, tb);
|
||||||
|
params->set_custom_range(olive::TimeRange(t, t + tb));
|
||||||
|
params->set_export_length(tb);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
params->set_export_length(sequence->get_length());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scaling and color.
|
||||||
|
if (o.scaling_method >= 0) {
|
||||||
|
params->set_video_scaling_method(
|
||||||
|
static_cast<olive::EncodingParams::VideoScalingMethod>(
|
||||||
|
o.scaling_method));
|
||||||
|
}
|
||||||
|
switch (o.color_transform) {
|
||||||
|
case OAKENGINE_EXPORT_COLOR_REC709_OETF:
|
||||||
|
params->set_color_transform(
|
||||||
|
olive::ColorTransform(QStringLiteral("Rec.709 OETF")));
|
||||||
|
break;
|
||||||
|
case OAKENGINE_EXPORT_COLOR_REFERENCE:
|
||||||
|
params->set_color_transform(olive::ColorTransform());
|
||||||
|
break;
|
||||||
|
case OAKENGINE_EXPORT_COLOR_BT1886_EOTF:
|
||||||
|
params->set_color_transform(
|
||||||
|
olive::ColorTransform(QStringLiteral("BT.1886 EOTF")));
|
||||||
|
break;
|
||||||
|
case OAKENGINE_EXPORT_COLOR_CUSTOM:
|
||||||
|
if (o.color_transform_name[0] == '\0') {
|
||||||
|
return QStringLiteral(
|
||||||
|
"color_transform is CUSTOM but color_transform_name is empty");
|
||||||
|
}
|
||||||
|
params->set_color_transform(olive::ColorTransform(
|
||||||
|
QString::fromUtf8(o.color_transform_name)));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Same default output transform as the application's export dialog.
|
||||||
|
params->set_color_transform(
|
||||||
|
olive::ColorTransform(QStringLiteral("sRGB OETF")));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encoder-specific video options accumulated for this thread.
|
||||||
|
for (auto it = g_video_options.cbegin(); it != g_video_options.cend();
|
||||||
|
++it) {
|
||||||
|
params->set_video_option(it.key(), it.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
@@ -333,68 +623,11 @@ int oakengine_export_render(OakEngineSequence *seq, const char *path,
|
|||||||
olive::ColorTransform(QStringLiteral("sRGB OETF")));
|
olive::ColorTransform(QStringLiteral("sRGB OETF")));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Pre-generate the audio conforms the render is about to need (the
|
return render_internal(sequence, project, params, audio_enabled, ap);
|
||||||
// headless equivalent of the application's preview-warmed cache).
|
|
||||||
// A timeout here is not fatal: the render's own conform wait is the
|
|
||||||
// fallback path.
|
|
||||||
if (audio_enabled) {
|
|
||||||
QString prewarm_error;
|
|
||||||
prewarm_conforms(project, ap, &prewarm_error);
|
|
||||||
}
|
|
||||||
|
|
||||||
olive::ExportTask task(sequence, project->color_manager(), params);
|
|
||||||
// The progress signal is emitted on the task thread, so the callback
|
|
||||||
// (installed for the calling thread) is captured by value -- reading
|
|
||||||
// the thread_local on the task thread would see NULL.
|
|
||||||
const oakengine_export_progress_fn progress_fn = g_progress_fn;
|
|
||||||
void *const progress_userdata = g_progress_userdata;
|
|
||||||
if (progress_fn) {
|
|
||||||
QObject::connect(&task, &olive::ExportTask::progress_changed,
|
|
||||||
[progress_fn, progress_userdata](double fraction) {
|
|
||||||
progress_fn(fraction, progress_userdata);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drive the task the way the application does: the task runs on a
|
|
||||||
// worker thread while the calling thread keeps its event loop
|
|
||||||
// spinning. A bare synchronous start() deadlocks on audio exports:
|
|
||||||
// audio conforms are delivered to TaskManager via queued calls and
|
|
||||||
// ConformManager::conform_task_finished is queued back to THIS
|
|
||||||
// thread, which must therefore process events while waiting.
|
|
||||||
std::atomic<bool> done{ false };
|
|
||||||
bool result = false;
|
|
||||||
QObject::connect(&task, &olive::ExportTask::finished,
|
|
||||||
[&done, &result](olive::Task *, bool r) {
|
|
||||||
result = r;
|
|
||||||
done.store(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
QThread task_thread;
|
|
||||||
task.moveToThread(&task_thread);
|
|
||||||
task_thread.start();
|
|
||||||
QMetaObject::invokeMethod(&task, "start", Qt::QueuedConnection);
|
|
||||||
while (!done.load()) {
|
|
||||||
QCoreApplication::processEvents(QEventLoop::AllEvents, 20);
|
|
||||||
QThread::msleep(5);
|
|
||||||
}
|
|
||||||
// Move the task back before tearing the thread down (QObjects must
|
|
||||||
// not be destroyed while owned by a dead thread).
|
|
||||||
task.moveToThread(QCoreApplication::instance()->thread());
|
|
||||||
task_thread.quit();
|
|
||||||
task_thread.wait();
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
set_error(task.get_error().isEmpty() ?
|
|
||||||
QStringLiteral("export failed") :
|
|
||||||
task.get_error());
|
|
||||||
return OAKENGINE_E_FAILED;
|
|
||||||
}
|
|
||||||
} catch (const std::exception &e) {
|
} catch (const std::exception &e) {
|
||||||
set_error(QStringLiteral("export failed: %1").arg(e.what()));
|
set_error(QStringLiteral("export failed: %1").arg(e.what()));
|
||||||
return OAKENGINE_E_FAILED;
|
return OAKENGINE_E_FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
return OAKENGINE_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int oakengine_export_last_error(char *buf, int buf_size)
|
int oakengine_export_last_error(char *buf, int buf_size)
|
||||||
@@ -402,6 +635,73 @@ int oakengine_export_last_error(char *buf, int buf_size)
|
|||||||
return string_to_buf(g_last_error, buf, buf_size);
|
return string_to_buf(g_last_error, buf, buf_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int oakengine_export_render_ex(OakEngineSequence *seq, const char *path,
|
||||||
|
const oak_export_options_ex *opts)
|
||||||
|
{
|
||||||
|
set_error(QString());
|
||||||
|
olive::Sequence *sequence = reinterpret_cast<olive::Sequence *>(seq);
|
||||||
|
if (!sequence || !path || !opts) {
|
||||||
|
set_error(QStringLiteral("invalid arguments"));
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
if (!olive::RenderManager::instance()) {
|
||||||
|
set_error(QStringLiteral("engine not initialized with "
|
||||||
|
"OAKENGINE_INIT_RENDER"));
|
||||||
|
return OAKENGINE_E_STATE;
|
||||||
|
}
|
||||||
|
olive::Project *project = olive::Project::get_project_from_object(sequence);
|
||||||
|
if (!project) {
|
||||||
|
set_error(QStringLiteral("sequence is not part of a project"));
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
oak_export_options_ex o = *opts;
|
||||||
|
if (o.video_enabled == 0 && o.audio_enabled == 0 &&
|
||||||
|
o.subtitles_enabled == 0) {
|
||||||
|
// Preserve the simple entry point's defaults: video+audio enabled.
|
||||||
|
o.video_enabled = 1;
|
||||||
|
o.audio_enabled = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
olive::EncodingParams params;
|
||||||
|
const QString error = params_from_ex(o, sequence, path, ¶ms);
|
||||||
|
if (!error.isEmpty()) {
|
||||||
|
set_error(error);
|
||||||
|
return OAKENGINE_E_INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const bool prewarm_audio = o.audio_enabled != 0;
|
||||||
|
return render_internal(sequence, project, params, prewarm_audio,
|
||||||
|
params.audio_enabled() ?
|
||||||
|
params.audio_params() :
|
||||||
|
sequence->get_audio_params());
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
set_error(QStringLiteral("export failed: %1").arg(e.what()));
|
||||||
|
return OAKENGINE_E_FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void oakengine_export_cancel(void)
|
||||||
|
{
|
||||||
|
if (olive::ExportTask *task = g_current_export.load()) {
|
||||||
|
task->cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void oakengine_export_set_video_option(const char *key, const char *value)
|
||||||
|
{
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
g_video_options.insert(QString::fromUtf8(key),
|
||||||
|
QString::fromUtf8(value));
|
||||||
|
} else {
|
||||||
|
g_video_options.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int oakengine_export_has_video_codec(int codec)
|
int oakengine_export_has_video_codec(int codec)
|
||||||
{
|
{
|
||||||
if (codec == OAKENGINE_EXPORT_VIDEO_PNG_SEQUENCE) {
|
if (codec == OAKENGINE_EXPORT_VIDEO_PNG_SEQUENCE) {
|
||||||
|
|||||||
@@ -39,6 +39,9 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
@@ -365,6 +368,143 @@ int main(void)
|
|||||||
oakengine_footage_free(tone);
|
oakengine_footage_free(tone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- render_ex option validation (no GL needed) -------------------------
|
||||||
|
{
|
||||||
|
oak_export_options_ex bad;
|
||||||
|
memset(&bad, 0, sizeof(bad));
|
||||||
|
bad.format = 2; // mp4
|
||||||
|
bad.video_enabled = 1;
|
||||||
|
bad.video_codec = 1; // h264
|
||||||
|
bad.audio_enabled = 0;
|
||||||
|
char out_bad[4096];
|
||||||
|
snprintf(out_bad, sizeof(out_bad), "%s/bad.mp4", g_tmpdir);
|
||||||
|
|
||||||
|
bad.format = -1;
|
||||||
|
assert(oakengine_export_render_ex(seq, out_bad, &bad) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
bad.format = 99;
|
||||||
|
assert(oakengine_export_render_ex(seq, out_bad, &bad) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
bad.format = 2;
|
||||||
|
|
||||||
|
bad.video_codec = 99;
|
||||||
|
assert(oakengine_export_render_ex(seq, out_bad, &bad) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
bad.video_codec = 1;
|
||||||
|
|
||||||
|
bad.range_mode = OAKENGINE_EXPORT_RANGE_CUSTOM;
|
||||||
|
bad.range_in_ts = 30;
|
||||||
|
bad.range_out_ts = 30;
|
||||||
|
assert(oakengine_export_render_ex(seq, out_bad, &bad) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
bad.range_mode = OAKENGINE_EXPORT_RANGE_STILL;
|
||||||
|
bad.still_time_ts = -1;
|
||||||
|
assert(oakengine_export_render_ex(seq, out_bad, &bad) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
|
||||||
|
bad.range_mode = OAKENGINE_EXPORT_RANGE_ENTIRE;
|
||||||
|
bad.color_transform = OAKENGINE_EXPORT_COLOR_CUSTOM;
|
||||||
|
bad.color_transform_name[0] = '\0';
|
||||||
|
assert(oakengine_export_render_ex(seq, out_bad, &bad) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
|
||||||
|
bad.color_transform = OAKENGINE_EXPORT_COLOR_SRGB_OETF;
|
||||||
|
bad.video_pix_fmt = 99;
|
||||||
|
assert(oakengine_export_render_ex(seq, out_bad, &bad) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
|
||||||
|
assert(oakengine_export_render_ex(NULL, out_bad, &bad) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_export_render_ex(seq, NULL, &bad) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
assert(oakengine_export_render_ex(seq, out_bad, NULL) ==
|
||||||
|
OAKENGINE_E_INVALID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- render_ex: real exports ---------------------------------------------
|
||||||
|
{
|
||||||
|
// H.264 + AAC over a custom 20-frame range of the same sequence.
|
||||||
|
char out3[4096];
|
||||||
|
snprintf(out3, sizeof(out3), "%s/ex_custom.mp4", g_tmpdir);
|
||||||
|
oak_export_options_ex o3;
|
||||||
|
memset(&o3, 0, sizeof(o3));
|
||||||
|
o3.range_mode = OAKENGINE_EXPORT_RANGE_CUSTOM;
|
||||||
|
o3.range_in_ts = 5;
|
||||||
|
o3.range_out_ts = 25;
|
||||||
|
o3.format = 2; // ExportFormat::k_format_mpe_g4_video
|
||||||
|
o3.video_enabled = 1;
|
||||||
|
o3.video_codec = 1; // ExportCodec::k_codec_h264
|
||||||
|
o3.audio_enabled = 1;
|
||||||
|
o3.audio_codec = 11; // ExportCodec::k_codec_aac
|
||||||
|
o3.audio_sample_rate = 48000;
|
||||||
|
o3.audio_channel_layout = 0x3;
|
||||||
|
o3.video_width = 320;
|
||||||
|
o3.video_height = 180;
|
||||||
|
rc = oakengine_export_render_ex(seq, out3, &o3);
|
||||||
|
if (rc != OAKENGINE_OK) {
|
||||||
|
fprintf(stderr, "ex custom export failed (%d): %s\n", rc,
|
||||||
|
oakengine_export_last_error(err, sizeof(err)) > 0 ?
|
||||||
|
err :
|
||||||
|
"(no error)");
|
||||||
|
}
|
||||||
|
assert(rc == OAKENGINE_OK);
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"ffprobe -v error -show_entries stream=codec_type,duration "
|
||||||
|
"-of csv=p=0 \"%s\"",
|
||||||
|
out3);
|
||||||
|
FILE *p3 = popen(cmd, "r");
|
||||||
|
assert(p3 != NULL);
|
||||||
|
char p3_out[512] = { 0 };
|
||||||
|
const size_t p3_len = fread(p3_out, 1, sizeof(p3_out) - 1, p3);
|
||||||
|
(void)p3_len;
|
||||||
|
assert(pclose(p3) == 0);
|
||||||
|
assert(strstr(p3_out, "video") != NULL);
|
||||||
|
assert(strstr(p3_out, "audio") != NULL);
|
||||||
|
// 20 frames at 30000/1001 ~= 0.667 s.
|
||||||
|
assert(strstr(p3_out, "0.6") != NULL);
|
||||||
|
|
||||||
|
// PNG sequence over 5 frames (image-sequence flag auto-fills the
|
||||||
|
// frame placeholder).
|
||||||
|
char out4[4096];
|
||||||
|
snprintf(out4, sizeof(out4), "%s/seqout.png", g_tmpdir);
|
||||||
|
oak_export_options_ex o4 = o3;
|
||||||
|
o4.range_in_ts = 0;
|
||||||
|
o4.range_out_ts = 5;
|
||||||
|
o4.format = 5; // ExportFormat::k_format_png
|
||||||
|
o4.video_codec = 5; // ExportCodec::k_codec_png
|
||||||
|
o4.audio_enabled = 0;
|
||||||
|
o4.is_image_sequence = 1;
|
||||||
|
rc = oakengine_export_render_ex(seq, out4, &o4);
|
||||||
|
if (rc != OAKENGINE_OK) {
|
||||||
|
fprintf(stderr, "ex png export failed (%d): %s\n", rc,
|
||||||
|
oakengine_export_last_error(err, sizeof(err)) > 0 ?
|
||||||
|
err :
|
||||||
|
"(no error)");
|
||||||
|
}
|
||||||
|
assert(rc == OAKENGINE_OK);
|
||||||
|
char png0[4096];
|
||||||
|
snprintf(png0, sizeof(png0), "%s/seqout_00000.png", g_tmpdir);
|
||||||
|
assert(access(png0, F_OK) == 0);
|
||||||
|
snprintf(cmd, sizeof(cmd), "file \"%s\"", png0);
|
||||||
|
assert_probe_matches(cmd, "PNG image data");
|
||||||
|
|
||||||
|
// Cancellation from another thread: either the export finishes
|
||||||
|
// first (idempotent) or reports E_CANCELLED; the exporter must stay
|
||||||
|
// usable afterwards.
|
||||||
|
oak_export_options_ex o5 = o3;
|
||||||
|
snprintf(out3, sizeof(out3), "%s/ex_cancel.mp4", g_tmpdir);
|
||||||
|
std::thread canceller([]() {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
|
oakengine_export_cancel();
|
||||||
|
});
|
||||||
|
const int cancel_rc = oakengine_export_render_ex(seq, out3, &o5);
|
||||||
|
canceller.join();
|
||||||
|
assert(cancel_rc == OAKENGINE_OK ||
|
||||||
|
cancel_rc == OAKENGINE_E_CANCELLED);
|
||||||
|
snprintf(out3, sizeof(out3), "%s/ex_after.mp4", g_tmpdir);
|
||||||
|
assert(oakengine_export_render_ex(seq, out3, &o5) == OAKENGINE_OK);
|
||||||
|
}
|
||||||
|
|
||||||
oakengine_project_free(project);
|
oakengine_project_free(project);
|
||||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user