refactor(task,render): RenderTask family over a new oakrender ticket C ABI

- oakrender: new ticket family (render_frame/render_audio with finished
  callback, result frame/samples access, cancel/wait), project copier
  C API, cache get_invalidated_ranges, manager set_aggressive_gc
- oaknode: node_copy_inputs, set_value_hint_track, viewer params
  setters, video frame cache borrowed outlet, project copy_settings
- oakcodec: encoder desired pixel format, export format extension,
  encoding generate_matrix, custom range in encoding params POD
- oaktask: RenderTask orchestrates oakrender tickets (no Qt threads/
  watchers), PreCacheTask and ExportTask rewritten over the oaknode/
  oakrender/oakcodec C ABIs; frames cross from oakrender to oakcodec
  handles by pixel copy (different control blocks)
- the two-step texture/download render path collapses into single-step
  (tickets always yield frames); subtitle sidecar kept
- tests: 105 in build-oaktask (export/precache factory paths,
  construction, conform end-to-end); regressions all green
This commit is contained in:
2026-08-07 14:40:12 +08:00
parent 6ded1d2d63
commit 40a336276f
29 changed files with 2548 additions and 726 deletions
+31
View File
@@ -116,6 +116,14 @@ typedef struct oakcodec_encoding_params {
int export_length_num; /**< Export length in seconds (rational). */
int export_length_den;
/** Custom export range (seconds, rational pairs); used when
* has_custom_range != 0. */
int has_custom_range;
int64_t custom_range_in_num;
int64_t custom_range_in_den;
int64_t custom_range_out_num;
int64_t custom_range_out_den;
} oakcodec_encoding_params;
/**
@@ -185,6 +193,29 @@ OAKCODEC_API int oakcodec_encoder_flush(OakEncoder encoder);
*/
OAKCODEC_API int oakcodec_encoder_last_error(OakEncoder encoder, char *buf, int buf_size);
/**
* @brief The pixel format the encoder wants frames in
* (Encoder::get_desired_pixel_format()), as int; -1 when
* unknown/invalid encoder.
*/
OAKCODEC_API int oakcodec_encoder_get_desired_pixel_format(OakEncoder encoder);
/**
* @brief File extension for an export format
* (ExportFormat::get_extension()), two-stage string getter.
*/
OAKCODEC_API int oakcodec_export_format_get_extension(int format, char *buf,
int buf_size);
/**
* @brief Scaling matrix for a scaling method
* (EncodingParams::generate_matrix()), row-major 4x4 into
* out_matrix[16].
*/
OAKCODEC_API int oakcodec_encoding_generate_matrix(int method, int src_width,
int src_height, int dst_width,
int dst_height, double *out_matrix);
#ifdef __cplusplus
}
#endif
+45
View File
@@ -23,6 +23,7 @@
#include <stdint.h>
#include "common/videoparams.h"
#include "node/error.h"
#include "undo/undocommand.h"
@@ -110,6 +111,16 @@ typedef struct OakNodeProject OakNodeProject;
typedef struct OakNodeMarkerList OakNodeMarkerList;
typedef struct OakNodeWorkArea OakNodeWorkArea;
/**
* @brief Opaque borrowed handle to a node's video frame cache
* (olive::FrameHashCache in oakrender). oakrender reinterprets this into
* its own handle types.
*/
typedef struct OakNodeFrameCache OakNodeFrameCache;
/* oakcore handles used by the viewer setters. */
typedef struct OakAudioParams OakAudioParams;
/**
* @brief Number of live owned objects created through this API
* (nodes from oaknode_factory_create_from_id()/oaknode_node_create_copy(),
@@ -543,6 +554,40 @@ int oaknode_node_get_markers(const OakNodeNode *node,
int oaknode_node_get_work_area(const OakNodeNode *node,
OakNodeWorkArea **out);
/**
* @brief Borrowed video frame cache of a node (NULL when the node has
* none or for NULL input).
*/
int oaknode_node_get_video_frame_cache(const OakNodeNode *node,
OakNodeFrameCache **out);
/**
* @brief Copy input values/connections from one node to another
* (Node::copy_inputs()). include_connections != 0 also copies
* input connections.
*/
int oaknode_node_copy_inputs(OakNodeNode *dst, const OakNodeNode *src,
int include_connections);
/**
* @brief Set a track-routing value hint on an input
* (Node::set_value_hint_for_input() with a single texture type
* and a Track::Reference string).
*/
int oaknode_node_set_value_hint_track(OakNodeNode *node,
const char *input_id,
int track_type, int track_index);
/**
* @brief Set a viewer node's video/audio params (ViewerOutput::
* set_video_params/set_audio_params, stream index 0). `params` is an
* oakcommon handle (video) or borrowed oakcore handle (audio).
*/
int oaknode_viewer_set_video_params(OakNodeNode *viewer,
const OakVideoParams *params);
int oaknode_viewer_set_audio_params(OakNodeNode *viewer,
const OakAudioParams *params);
/**
* @brief Create a command that removes a node from its graph together
* with its exclusive dependencies and disconnects its edges
+6
View File
@@ -163,6 +163,12 @@ int oaknode_project_is_new(const OakNodeProject *project);
int oaknode_project_cache_path(const OakNodeProject *project, char *buf,
int buf_size);
/**
* @brief Copy all project settings (Project::copy_settings()).
*/
int oaknode_project_copy_settings(OakNodeProject *dst,
const OakNodeProject *src);
/**
* @brief Cache location setting enum value
* (Project::get_cache_location_setting(): 0 = default location,
+14
View File
@@ -107,6 +107,20 @@ int oakrender_cache_has_validated_ranges(const OakRenderCache *cache);
*/
int oakrender_cache_indicator_height(void);
/**
* @brief The invalidated sub-ranges of [in, out) as flat
* {in_n, in_d, out_n, out_d} quadruples
* (PlaybackCache::get_invalidated_ranges()).
*
* Two-stage: call with ranges == NULL (or max_ranges == 0) to get the
* count; then call with a buffer of max_ranges * 4 int64_t values.
*
* @return Range count (>= 0), or a negative OAKRENDER_E_* code.
*/
int oakrender_cache_get_invalidated_ranges(OakRenderCache *c,
int64_t in_num, int64_t in_den, int64_t out_num, int64_t out_den,
int64_t *ranges, int max_ranges);
/**
* @brief Load a cached frame from disk
* (FrameHashCache::load_cache_frame(cache_path, uuid, ts)).
+65
View File
@@ -0,0 +1,65 @@
/***
Oak Video Editor - 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/>.
***/
#ifndef OAK_EDITOR_RENDER_COPIER_H
#define OAK_EDITOR_RENDER_COPIER_H
#include "node/node.h"
#include "node/project.h"
#include "render/error.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Opaque handle to a project copier (olive::ProjectCopier):
* deep-copies a project graph for background processing
* (export/precache).
*/
typedef struct OakRenderProjectCopier OakRenderProjectCopier;
/**
* @brief Create a copier. The copy is built by
* oakrender_project_copier_set_project().
*/
OakRenderProjectCopier *oakrender_project_copier_create(void);
/** @brief Free the copier AND its copied project. NULL-safe. */
void oakrender_project_copier_free(OakRenderProjectCopier *copier);
/** @brief (Re)build the copy from `project`. */
int oakrender_project_copier_set_project(OakRenderProjectCopier *copier,
OakNodeProject *project);
/** @brief The copied counterpart of an original node (borrowed), NULL
* when the node is not in the copied project. */
OakNodeNode *oakrender_project_copier_get_copy(
OakRenderProjectCopier *copier, OakNodeNode *original);
/** @brief The copied project (borrowed). */
OakNodeProject *oakrender_project_copier_get_copied_project(
OakRenderProjectCopier *copier);
#ifdef __cplusplus
}
#endif
#endif //OAK_EDITOR_RENDER_COPIER_H
+156
View File
@@ -0,0 +1,156 @@
/***
Oak Video Editor - 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/>.
***/
#ifndef OAK_EDITOR_RENDER_TICKET_H
#define OAK_EDITOR_RENDER_TICKET_H
#include <stdint.h>
#include "common/colortransform.h"
#include "common/videoparams.h"
#include "node/colormanager.h"
#include "node/node.h"
#include "olive/core/oakcore/audioparams.h"
#include "olive/core/oakcore/samplebuffer.h"
#include "render/error.h"
#include "render/color.h"
#include "render/renderer.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Opaque handle to a render ticket (olive::RenderTicketWatcher).
*
* Created by oakrender_ticket_render_frame() /
* oakrender_ticket_render_audio(); free with oakrender_ticket_free().
*/
typedef struct OakRenderTicket OakRenderTicket;
/**
* @brief Finished callback (async command return channel, 01 §4
* exception). Fires on the ticket's finishing thread, exactly
* once (cancelled tickets fire with a NULL result).
*/
typedef void (*oakrender_ticket_finished_fn)(OakRenderTicket *ticket,
void *userdata);
/** @brief Ticket types (RenderManager::TicketType). */
enum OakRenderTicketType {
OAKRENDER_TICKET_VIDEO = 0,
OAKRENDER_TICKET_AUDIO = 1
};
/**
* @brief Parameters for a video frame ticket
* (RenderManager::RenderVideoParams).
*/
typedef struct oakrender_video_ticket_params {
OakNodeNode *output_node; /**< Connected texture output node. */
OakVideoParams video_params; /**< By value (oakcommon handle). */
OakAudioParams *audio_params; /**< Borrowed oakcore handle, may be NULL. */
int64_t time_num; /**< Frame timestamp as rational. */
int64_t time_den;
OakNodeColorManager *color_manager; /**< Borrowed, may be NULL. */
int mode; /**< olive::RenderMode::Mode as int. */
int force_width; /**< 0/0 = off. */
int force_height;
double force_matrix[16]; /**< Used when has_force_matrix != 0. */
int has_force_matrix;
int force_format; /**< PixelFormat as int, -1 = off. */
int force_channel_count; /**< 0 = off. */
OakColorProcessor *force_color_output; /**< May be NULL. */
OakColorTransform force_color_transform; /**< By value; empty ctx = default. */
OakNodeFrameCache *cache; /**< Borrowed frame cache, may be NULL. */
} oakrender_video_ticket_params;
/**
* @brief Submit a video frame render ticket.
*
* @return Ticket handle (caller frees), or NULL on failure. The finished
* callback fires exactly once; NULL `cb` is allowed (poll with
* oakrender_ticket_wait()/oakrender_ticket_is_finished()).
*/
OakRenderTicket *oakrender_ticket_render_frame(
const oakrender_video_ticket_params *params,
oakrender_ticket_finished_fn cb, void *userdata);
/**
* @brief Submit an audio render ticket (RenderManager::render_audio()).
*
* @param output_node Connected sample output node.
* @param params Audio params (borrowed oakcore handle).
*/
OakRenderTicket *oakrender_ticket_render_audio(
OakNodeNode *output_node, int64_t in_num, int64_t in_den,
int64_t out_num, int64_t out_den, const OakAudioParams *params,
int mode, oakrender_ticket_finished_fn cb, void *userdata);
int oakrender_ticket_is_finished(OakRenderTicket *ticket);
/** @brief Block until the ticket finishes. */
int oakrender_ticket_wait(OakRenderTicket *ticket);
int oakrender_ticket_cancel(OakRenderTicket *ticket);
/** @brief OAKRENDER_TICKET_* or negative error. */
int oakrender_ticket_get_type(OakRenderTicket *ticket);
/** @brief Ticket timestamp (video tickets). */
int oakrender_ticket_get_time(OakRenderTicket *ticket, int64_t *out_num,
int64_t *out_den);
/** @brief Ticket time range (audio tickets). */
int oakrender_ticket_get_range(OakRenderTicket *ticket, int64_t *in_num,
int64_t *in_den, int64_t *out_num,
int64_t *out_den);
/**
* @brief The resulting frame (video tickets). *out receives a retained
* OakCodecFrame (release with oakrender_codec_frame_free()).
* OAKRENDER_E_STATE when unfinished, OAKRENDER_E_FAILED when the
* ticket has no frame result.
*/
int oakrender_ticket_get_frame(OakRenderTicket *ticket,
OakCodecFrame **out);
/**
* @brief The resulting samples (audio tickets). *out receives a copy
* (release with oakcore_samplebuffer_free()).
*/
int oakrender_ticket_get_samples(OakRenderTicket *ticket,
OakSampleBuffer **out);
/** @brief Free the ticket handle (safe on finished tickets; cancels and
* waits on running ones). No-op on NULL. */
void oakrender_ticket_free(OakRenderTicket *ticket);
/**
* @brief Toggle aggressive garbage collection on the render manager
* (RenderManager::set_aggressive_garbage_collection()).
*/
int oakrender_manager_set_aggressive_gc(int enabled);
#ifdef __cplusplus
}
#endif
#endif //OAK_EDITOR_RENDER_TICKET_H
+12
View File
@@ -21,9 +21,12 @@
#ifndef OAK_EDITOR_TASK_PROJECT_H
#define OAK_EDITOR_TASK_PROJECT_H
#include "codec/encoder.h"
#include "node/colormanager.h"
#include "node/footage.h"
#include "node/node.h"
#include "node/project.h"
#include "node/sequence.h"
#include "task/task.h"
#include "undo/undocommand.h"
@@ -67,6 +70,15 @@ int oaktask_import_invalid_count(OakTaskTask *t);
int oaktask_import_invalid_at(OakTaskTask *t, int index, char *buf,
int buf_size);
/** @brief olive::PreCacheTask. */
OakTaskTask *oaktask_create_precache(OakNodeFootage *footage, int index,
OakNodeSequence *sequence);
/** @brief olive::ExportTask (params POD from codec/encoder.h). */
OakTaskTask *oaktask_create_export(OakNodeNode *viewer,
OakNodeColorManager *color_manager,
const oakcodec_encoding_params *params);
/**
* @brief Image-sequence confirmation callback (facade/UI concern;
* olive::ProjectImportTask::set_image_sequence_confirm_callback).
+41
View File
@@ -119,6 +119,15 @@ olive::EncodingParams to_native(const oakcodec_encoding_params *p)
Rational(p->export_length_num, p->export_length_den));
}
if (p->has_custom_range && p->custom_range_in_den != 0 &&
p->custom_range_out_den != 0) {
n.set_custom_range(TimeRange(
Rational(int(p->custom_range_in_num),
int(p->custom_range_in_den)),
Rational(int(p->custom_range_out_num),
int(p->custom_range_out_den))));
}
return n;
}
@@ -270,3 +279,35 @@ int oakcodec_encoder_last_error(OakEncoder encoder, char *buf, int buf_size)
return string_out(b->encoder ? b->encoder->get_error() : std::string(),
buf, buf_size);
}
int oakcodec_encoder_get_desired_pixel_format(OakEncoder encoder)
{
EncoderBox *b = box(encoder.ctx);
if (!b || !b->encoder)
return OAKCODEC_E_INVALID;
return int(b->encoder->get_desired_pixel_format());
}
int oakcodec_export_format_get_extension(int format, char *buf, int buf_size)
{
return string_out(
olive::ExportFormat::get_extension(
static_cast<olive::ExportFormat::Format>(format)),
buf, buf_size);
}
int oakcodec_encoding_generate_matrix(int method, int src_width,
int src_height, int dst_width,
int dst_height, double *out_matrix)
{
if (!out_matrix)
return OAKCODEC_E_INVALID;
std::array<float, 16> matrix = olive::EncodingParams::generate_matrix(
static_cast<olive::EncodingParams::VideoScalingMethod>(method),
src_width, src_height, dst_width, dst_height);
for (int i = 0; i < 16; i++) {
out_matrix[i] = matrix[size_t(i)];
}
return OAKCODEC_OK;
}
+102
View File
@@ -20,6 +20,9 @@
#include "node/node.h"
#include "common/videoparams.h"
#include "olive/core/oakcore/audioparams.h"
#include <atomic>
#include <string>
@@ -1230,3 +1233,102 @@ int oaknode_node_get_work_area(const OakNodeNode *node,
}
return OAKNODE_OK;
}
int oaknode_node_get_video_frame_cache(const OakNodeNode *node,
OakNodeFrameCache **out)
{
if (!node || !out) {
return OAKNODE_E_INVALID;
}
*out = reinterpret_cast<OakNodeFrameCache *>(
to_node(node)->video_frame_cache());
return OAKNODE_OK;
}
int oaknode_node_copy_inputs(OakNodeNode *dst, const OakNodeNode *src,
int include_connections)
{
if (!dst || !src) {
return OAKNODE_E_INVALID;
}
try {
olive::Node::copy_inputs(to_node(src), to_node(dst),
include_connections != 0);
return OAKNODE_OK;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
int oaknode_node_set_value_hint_track(OakNodeNode *node,
const char *input_id,
int track_type, int track_index)
{
if (!node || !input_id) {
return OAKNODE_E_INVALID;
}
try {
to_node(node)->set_value_hint_for_input(
input_id,
olive::Node::ValueHint({ olive::NodeValue::k_texture },
olive::Track::Reference(
static_cast<olive::Track::Type>(
track_type),
track_index)
.to_string()));
return OAKNODE_OK;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
int oaknode_viewer_set_video_params(OakNodeNode *viewer,
const OakVideoParams *params)
{
if (!viewer || !params || !params->ctx) {
return OAKNODE_E_INVALID;
}
const olive::VideoParams *native =
oakcommon_videoparams_get_native(*params);
if (!native) {
return OAKNODE_E_INVALID;
}
olive::ViewerOutput *v =
dynamic_cast<olive::ViewerOutput *>(to_node(viewer));
if (!v) {
return OAKNODE_E_INVALID;
}
try {
v->set_video_params(*native);
return OAKNODE_OK;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
int oaknode_viewer_set_audio_params(OakNodeNode *viewer,
const OakAudioParams *params)
{
if (!viewer || !params) {
return OAKNODE_E_INVALID;
}
olive::ViewerOutput *v =
dynamic_cast<olive::ViewerOutput *>(to_node(viewer));
if (!v) {
return OAKNODE_E_INVALID;
}
try {
v->set_audio_params(olive::AudioParams::from_handle(
oakcore_audioparams_copy(params)));
return OAKNODE_OK;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
+15
View File
@@ -371,3 +371,18 @@ OakNodeNode *oaknode_project_node_at(const OakNodeProject *project, int index)
return NULL;
}
}
int oaknode_project_copy_settings(OakNodeProject *dst,
const OakNodeProject *src)
{
if (!dst || !src) {
return OAKNODE_E_INVALID;
}
try {
olive::Project::copy_settings(const_cast<olive::Project *>(to_cpp(src)), to_cpp(dst));
return OAKNODE_OK;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
+2
View File
@@ -2,6 +2,8 @@ target_sources(oakrender PRIVATE
renderer.cpp
cache.cpp
cancelatom.cpp
ticket.cpp
copier.cpp
color.cpp
manager.cpp
)
+34
View File
@@ -206,3 +206,37 @@ void oakrender_frame_cache_save(OakRenderCache *cache, const char *path,
} catch (...) {
}
}
int oakrender_cache_get_invalidated_ranges(OakRenderCache *cache,
int64_t in_num, int64_t in_den, int64_t out_num, int64_t out_den,
int64_t *ranges, int max_ranges)
{
if (!cache || max_ranges < 0) {
return OAKRENDER_E_INVALID;
}
try {
olive::core::TimeRangeList list = impl(cache)->get_invalidated_ranges(
olive::core::TimeRange(
olive::core::Rational(int(in_num), int(in_den)),
olive::core::Rational(int(out_num), int(out_den))));
int count = int(list.size());
if (ranges) {
int written = 0;
for (const olive::core::TimeRange &r : list) {
if (written >= max_ranges) {
break;
}
ranges[written * 4 + 0] = r.in().numerator();
ranges[written * 4 + 1] = r.in().denominator();
ranges[written * 4 + 2] = r.out().numerator();
ranges[written * 4 + 3] = r.out().denominator();
written++;
}
}
return count;
} catch (...) {
return OAKRENDER_E_FAILED;
}
}
+85
View File
@@ -0,0 +1,85 @@
/***
Oak Video Editor - 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/>.
***/
#include "render/copier.h"
#include <new>
#include "../src/projectcopier.h"
namespace
{
olive::ProjectCopier *impl(OakRenderProjectCopier *h)
{
return reinterpret_cast<olive::ProjectCopier *>(h);
}
} // namespace
OakRenderProjectCopier *oakrender_project_copier_create(void)
{
try {
return reinterpret_cast<OakRenderProjectCopier *>(
new olive::ProjectCopier());
} catch (...) {
return NULL;
}
}
void oakrender_project_copier_free(OakRenderProjectCopier *copier)
{
delete impl(copier);
}
int oakrender_project_copier_set_project(OakRenderProjectCopier *copier,
OakNodeProject *project)
{
if (!copier || !project) {
return OAKRENDER_E_INVALID;
}
try {
impl(copier)->set_project(
reinterpret_cast<olive::Project *>(project));
return OAKRENDER_OK;
} catch (...) {
return OAKRENDER_E_FAILED;
}
}
OakNodeNode *oakrender_project_copier_get_copy(
OakRenderProjectCopier *copier, OakNodeNode *original)
{
if (!copier || !original) {
return NULL;
}
return reinterpret_cast<OakNodeNode *>(
impl(copier)->get_copy(reinterpret_cast<olive::Node *>(original)));
}
OakNodeProject *oakrender_project_copier_get_copied_project(
OakRenderProjectCopier *copier)
{
if (!copier) {
return NULL;
}
return reinterpret_cast<OakNodeProject *>(
impl(copier)->get_copied_project());
}
+350
View File
@@ -0,0 +1,350 @@
/***
Oak Video Editor - 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/>.
***/
#include "render/ticket.h"
#include <new>
#include "../src/colorprocessor.h"
#include "../src/framehashcache.h"
#include "../src/rendermanager.h"
#include "../src/renderticket.h"
#include "internalhandles.h"
namespace
{
struct TicketHandle {
olive::RenderTicketWatcher *watcher;
olive::RenderManager::TicketType type;
olive::core::Rational time;
olive::core::TimeRange range;
};
TicketHandle *impl(OakRenderTicket *t)
{
return reinterpret_cast<TicketHandle *>(t);
}
OakRenderTicket *wrap(olive::RenderTicketWatcher *w,
olive::RenderManager::TicketType type)
{
if (!w) {
return NULL;
}
TicketHandle *h = new (std::nothrow) TicketHandle{
w, type, olive::core::Rational(), olive::core::TimeRange()
};
if (!h) {
delete w;
return NULL;
}
return reinterpret_cast<OakRenderTicket *>(h);
}
olive::Node *to_node(OakNodeNode *n)
{
return reinterpret_cast<olive::Node *>(n);
}
} // namespace
OakRenderTicket *oakrender_ticket_render_frame(
const oakrender_video_ticket_params *params,
oakrender_ticket_finished_fn cb, void *userdata)
{
if (!params || !params->output_node) {
return NULL;
}
olive::RenderManager *manager = olive::RenderManager::instance();
if (!manager) {
return NULL;
}
try {
const olive::VideoParams *vp =
params->video_params.ctx
? oakcommon_videoparams_get_native(params->video_params)
: nullptr;
if (!vp) {
return NULL;
}
olive::RenderManager::RenderVideoParams rvp(
to_node(params->output_node), *vp,
params->audio_params
? olive::AudioParams::from_handle(
oakcore_audioparams_copy(params->audio_params))
: olive::AudioParams(),
olive::core::Rational(int(params->time_num),
int(params->time_den)),
reinterpret_cast<olive::ColorManager *>(params->color_manager),
static_cast<olive::RenderMode::Mode>(params->mode));
rvp.force_size =
olive::FrameSize(params->force_width, params->force_height);
if (params->has_force_matrix) {
olive::Matrix4x4 matrix;
for (int row = 0; row < 4; row++) {
for (int col = 0; col < 4; col++) {
matrix(row, col) =
float(params->force_matrix[row * 4 + col]);
}
}
rvp.force_matrix = matrix;
}
rvp.force_format = static_cast<olive::core::PixelFormat::Format>(
params->force_format);
rvp.force_channel_count = params->force_channel_count;
rvp.force_color_output =
params->force_color_output ? params->force_color_output->ptr
: nullptr;
if (params->force_color_transform.ctx) {
const olive::ColorTransform *ct =
oakcommon_colortransform_get_native(
params->force_color_transform);
if (ct) {
rvp.force_color_transform = *ct;
}
}
if (params->cache) {
rvp.add_cache(reinterpret_cast<olive::FrameHashCache *>(
params->cache));
}
auto *watcher = new olive::RenderTicketWatcher();
watcher->set_property("type", olive::Variant::from_value(
int(olive::RenderManager::k_type_video)));
watcher->set_property(
"time", olive::Variant::from_value(olive::core::Rational(
int(params->time_num), int(params->time_den))));
OakRenderTicket *handle =
wrap(watcher, olive::RenderManager::k_type_video);
if (!handle) {
return NULL;
}
impl(handle)->time = olive::core::Rational(
int(params->time_num), int(params->time_den));
if (cb) {
watcher->set_finished_callback(
[handle, cb, userdata](olive::RenderTicketWatcher *) {
cb(handle, userdata);
});
}
watcher->set_ticket(manager->render_frame(rvp));
return handle;
} catch (...) {
return NULL;
}
}
OakRenderTicket *oakrender_ticket_render_audio(
OakNodeNode *output_node, int64_t in_num, int64_t in_den,
int64_t out_num, int64_t out_den, const OakAudioParams *params,
int mode, oakrender_ticket_finished_fn cb, void *userdata)
{
if (!output_node || !params) {
return NULL;
}
olive::RenderManager *manager = olive::RenderManager::instance();
if (!manager) {
return NULL;
}
try {
olive::core::Rational range_in((int)in_num, (int)in_den);
olive::core::Rational range_out((int)out_num, (int)out_den);
olive::core::TimeRange range(range_in, range_out);
olive::RenderManager::RenderAudioParams rap(
to_node(output_node), range,
olive::AudioParams::from_handle(
oakcore_audioparams_copy(params)),
static_cast<olive::RenderMode::Mode>(mode));
auto *watcher = new olive::RenderTicketWatcher();
watcher->set_property("type", olive::Variant::from_value(
int(olive::RenderManager::k_type_audio)));
watcher->set_property("range", olive::Variant::from_value(range));
OakRenderTicket *handle =
wrap(watcher, olive::RenderManager::k_type_audio);
if (!handle) {
return NULL;
}
impl(handle)->range = range;
if (cb) {
watcher->set_finished_callback(
[handle, cb, userdata](olive::RenderTicketWatcher *) {
cb(handle, userdata);
});
}
watcher->set_ticket(manager->render_audio(rap));
return handle;
} catch (...) {
return NULL;
}
}
int oakrender_ticket_is_finished(OakRenderTicket *ticket)
{
if (!ticket) {
return OAKRENDER_E_INVALID;
}
return impl(ticket)->watcher->is_running() ? 0 : 1;
}
int oakrender_ticket_wait(OakRenderTicket *ticket)
{
if (!ticket) {
return OAKRENDER_E_INVALID;
}
impl(ticket)->watcher->wait_for_finished();
return OAKRENDER_OK;
}
int oakrender_ticket_cancel(OakRenderTicket *ticket)
{
if (!ticket) {
return OAKRENDER_E_INVALID;
}
impl(ticket)->watcher->cancel();
return OAKRENDER_OK;
}
int oakrender_ticket_get_type(OakRenderTicket *ticket)
{
if (!ticket) {
return OAKRENDER_E_INVALID;
}
return int(impl(ticket)->type);
}
int oakrender_ticket_get_time(OakRenderTicket *ticket, int64_t *out_num,
int64_t *out_den)
{
if (!ticket || !out_num || !out_den) {
return OAKRENDER_E_INVALID;
}
*out_num = impl(ticket)->time.numerator();
*out_den = impl(ticket)->time.denominator();
return OAKRENDER_OK;
}
int oakrender_ticket_get_range(OakRenderTicket *ticket, int64_t *in_num,
int64_t *in_den, int64_t *out_num,
int64_t *out_den)
{
if (!ticket || !in_num || !in_den || !out_num || !out_den) {
return OAKRENDER_E_INVALID;
}
*in_num = impl(ticket)->range.in().numerator();
*in_den = impl(ticket)->range.in().denominator();
*out_num = impl(ticket)->range.out().numerator();
*out_den = impl(ticket)->range.out().denominator();
return OAKRENDER_OK;
}
int oakrender_ticket_get_frame(OakRenderTicket *ticket, OakCodecFrame **out)
{
if (!ticket || !out) {
return OAKRENDER_E_INVALID;
}
*out = NULL;
TicketHandle *h = impl(ticket);
if (h->watcher->is_running()) {
return OAKRENDER_E_STATE;
}
olive::Variant result = h->watcher->get();
if (!result.can_convert<olive::FramePtr>()) {
return OAKRENDER_E_FAILED;
}
olive::FramePtr frame = result.value<olive::FramePtr>();
if (!frame) {
return OAKRENDER_E_FAILED;
}
OakCodecFrame *handle = new (std::nothrow) OakCodecFrame;
if (!handle) {
return OAKRENDER_E_NOMEM;
}
handle->ptr = frame;
*out = handle;
return OAKRENDER_OK;
}
int oakrender_ticket_get_samples(OakRenderTicket *ticket,
OakSampleBuffer **out)
{
if (!ticket || !out) {
return OAKRENDER_E_INVALID;
}
*out = NULL;
TicketHandle *h = impl(ticket);
if (h->watcher->is_running()) {
return OAKRENDER_E_STATE;
}
olive::Variant result = h->watcher->get();
if (!result.can_convert<olive::SampleBuffer>()) {
return OAKRENDER_E_FAILED;
}
olive::SampleBuffer samples = result.value<olive::SampleBuffer>();
*out = oakcore_samplebuffer_copy(samples.handle());
return *out ? OAKRENDER_OK : OAKRENDER_E_NOMEM;
}
void oakrender_ticket_free(OakRenderTicket *ticket)
{
if (!ticket) {
return;
}
TicketHandle *h = impl(ticket);
if (h->watcher->is_running()) {
h->watcher->cancel();
h->watcher->wait_for_finished();
}
delete h->watcher;
delete h;
}
int oakrender_manager_set_aggressive_gc(int enabled)
{
olive::RenderManager *manager = olive::RenderManager::instance();
if (!manager) {
return OAKRENDER_E_STATE;
}
manager->set_aggressive_garbage_collection(enabled != 0);
return OAKRENDER_OK;
}
+1
View File
@@ -18,6 +18,7 @@ endif()
add_executable(oakrender-gtest
cache_test.cpp
cancelatom_test.cpp
ticket_test.cpp
color_test.cpp
manager_test.cpp
renderer_test.cpp
+57
View File
@@ -0,0 +1,57 @@
/***
Oak Video Editor - 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/>.
***/
#include <gtest/gtest.h>
#include "render/ticket.h"
TEST(OakRenderTicket, NullAndInvalidArgs)
{
EXPECT_EQ(oakrender_ticket_render_frame(nullptr, nullptr, nullptr),
nullptr);
oakrender_video_ticket_params params = {};
EXPECT_EQ(oakrender_ticket_render_frame(&params, nullptr, nullptr),
nullptr);
EXPECT_EQ(oakrender_ticket_is_finished(nullptr), OAKRENDER_E_INVALID);
EXPECT_EQ(oakrender_ticket_wait(nullptr), OAKRENDER_E_INVALID);
EXPECT_EQ(oakrender_ticket_cancel(nullptr), OAKRENDER_E_INVALID);
EXPECT_EQ(oakrender_ticket_get_type(nullptr), OAKRENDER_E_INVALID);
EXPECT_EQ(oakrender_ticket_get_time(nullptr, nullptr, nullptr),
OAKRENDER_E_INVALID);
OakCodecFrame *frame = nullptr;
EXPECT_EQ(oakrender_ticket_get_frame(nullptr, &frame),
OAKRENDER_E_INVALID);
OakSampleBuffer *samples = nullptr;
EXPECT_EQ(oakrender_ticket_get_samples(nullptr, &samples),
OAKRENDER_E_INVALID);
oakrender_ticket_free(nullptr); // no-op
}
TEST(OakRenderTicket, AggressiveGcRequiresManager)
{
// The standalone test binary never initializes the render manager
int result = oakrender_manager_set_aggressive_gc(1);
EXPECT_TRUE(result == OAKRENDER_OK || result == OAKRENDER_E_STATE);
}
+29
View File
@@ -22,6 +22,8 @@
#include <vector>
#include "../src/export/export.h"
#include "../src/precache/precachetask.h"
#include "../src/project/import/import.h"
#include "../src/project/load/load.h"
#include "../src/project/save/save.h"
@@ -178,3 +180,30 @@ void oaktask_import_set_image_sequence_confirm_cb(
return fn(filename.c_str(), userdata) != 0;
});
}
OakTaskTask *oaktask_create_precache(OakNodeFootage *footage, int index,
OakNodeSequence *sequence)
{
if (!footage || !sequence) {
return NULL;
}
try {
return wrap(new olive::PreCacheTask(footage, index, sequence));
} catch (...) {
return NULL;
}
}
OakTaskTask *oaktask_create_export(OakNodeNode *viewer,
OakNodeColorManager *color_manager,
const oakcodec_encoding_params *params)
{
if (!viewer || !params) {
return NULL;
}
try {
return wrap(new olive::ExportTask(viewer, color_manager, *params));
} catch (...) {
return NULL;
}
}
+547
View File
@@ -0,0 +1,547 @@
/***
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 "export.h"
#include <cstring>
#include <filesystem>
#include <cstdint>
#include "common/filefunctions.h"
#include "node/sequence.h"
#include "olive/core/util/timecodefunctions.h"
#include "render/color.h"
namespace olive
{
namespace
{
std::string node_label(OakNodeNode *node)
{
int needed = oaknode_node_get_label(node, nullptr, 0);
if (needed <= 0) {
return std::string();
}
std::string label(size_t(needed), 0);
oaknode_node_get_label(node, label.data(), needed);
label.resize(size_t(needed) - 1);
return label;
}
std::string encoder_error(OakEncoder encoder)
{
char buf[512];
if (oakcodec_encoder_last_error(encoder, buf, sizeof(buf)) <= 0) {
return std::string();
}
return buf;
}
/**
* @brief Copy an oakrender frame into an oakcodec frame (pixel copy;
* the two handles wrap different control blocks).
*
* Returns an empty OakFrame (ctx == NULL) on failure.
*/
OakFrame copy_frame_to_codec(OakCodecFrame *render_frame)
{
OakFrame empty = {};
if (!render_frame) {
return empty;
}
oakrender_video_params rp = {};
if (oakrender_codec_frame_get_params(render_frame, &rp) !=
OAKRENDER_OK) {
return empty;
}
OakVideoParams vp = oakcommon_videoparams_init();
if (!vp.ctx) {
return empty;
}
oakcommon_videoparams_set_width(vp, rp.width);
oakcommon_videoparams_set_height(vp, rp.height);
oakcommon_videoparams_set_time_base(vp, rp.time_base_num,
rp.time_base_den);
oakcommon_videoparams_set_frame_rate(vp, rp.time_base_den,
rp.time_base_num);
oakcommon_videoparams_set_format(vp, rp.format);
oakcommon_videoparams_set_pixel_aspect_ratio(vp, rp.pixel_aspect_num,
rp.pixel_aspect_den);
oakcommon_videoparams_set_interlacing(vp, rp.interlacing);
oakcommon_videoparams_set_color_range(vp, rp.color_range);
oakcommon_videoparams_set_divider(vp, rp.divider);
oakcommon_videoparams_set_video_type(vp, rp.video_type);
oakcommon_videoparams_set_premultiplied_alpha(vp,
rp.premultiplied_alpha);
OakFrame out = oakcodec_frame_init_with_params(vp);
oakcommon_videoparams_free(&vp);
if (!out.ctx) {
return empty;
}
if (oakcodec_frame_allocate(out) != OAKCODEC_OK) {
oakcodec_frame_free(&out);
return empty;
}
// Copy scanlines
const uint8_t *src =
static_cast<const uint8_t *>(oakrender_codec_frame_const_data(render_frame));
uint8_t *dst = static_cast<uint8_t *>(oakcodec_frame_data(out));
int src_linesize = oakrender_codec_frame_linesize_bytes(render_frame);
int dst_linesize = oakcodec_frame_linesize_bytes(out);
int copy_bytes = src_linesize < dst_linesize ? src_linesize
: dst_linesize;
if (!src || !dst || copy_bytes <= 0) {
oakcodec_frame_free(&out);
return empty;
}
for (int y = 0; y < rp.height; y++) {
memcpy(dst + size_t(y) * dst_linesize,
src + size_t(y) * src_linesize, size_t(copy_bytes));
}
return out;
}
} // namespace
ExportTask::ExportTask(OakNodeNode *viewer_node,
OakNodeColorManager *color_manager,
const oakcodec_encoding_params &params)
: copier_(nullptr)
, color_manager_(nullptr)
, params_(params)
, encoder_({})
, subtitle_encoder_({})
, color_processor_(nullptr)
, frame_time_(0)
, null_frame_streak_(0)
, audio_time_(0)
{
(void)color_manager;
// Create a copy of the project
OakNodeProject *source_project = nullptr;
oaknode_node_get_project(viewer_node, &source_project);
copier_ = oakrender_project_copier_create();
if (copier_ && source_project) {
oakrender_project_copier_set_project(copier_, source_project);
}
set_viewer(oakrender_project_copier_get_copy(copier_, viewer_node));
OakNodeProject *copied_project =
oakrender_project_copier_get_copied_project(copier_);
color_manager_ = oaknode_colormanager_init(copied_project);
// Adjust video params to have no divider
OakVideoParams vp = {};
oaknode_sequence_get_video_params(
reinterpret_cast<OakNodeSequence *>(viewer_node), 0, &vp);
oakcommon_videoparams_set_divider(vp, 1);
oakcommon_videoparams_set_time_base(vp, params_.video_time_base_num,
params_.video_time_base_den);
oakcommon_videoparams_set_frame_rate(vp, params_.video_time_base_den,
params_.video_time_base_num);
set_video_params(vp);
oakcommon_videoparams_free(&vp);
OakAudioParams *audio_params = nullptr;
oaknode_sequence_get_audio_params(
reinterpret_cast<OakNodeSequence *>(viewer_node), 0,
&audio_params);
set_audio_params(audio_params);
set_title("Exporting \"" + node_label(viewer_node) + "\"");
set_native_progress_signalling_enabled(false);
}
ExportTask::~ExportTask()
{
if (encoder_.ctx) {
oakcodec_encoder_free(&encoder_);
}
if (subtitle_encoder_.ctx) {
oakcodec_encoder_free(&subtitle_encoder_);
}
if (color_processor_) {
oakrender_color_processor_free(color_processor_);
}
if (color_manager_) {
oaknode_colormanager_free(color_manager_);
}
oakrender_project_copier_free(copier_);
if (audio_params()) {
oakcore_audioparams_free(audio_params());
}
}
bool ExportTask::run()
{
// For safety, if we're overwriting, we save to a temporary filename and then only overwrite it
// at the end
std::string real_filename = params_.filename;
OakFileFunctions filefuncs = oakcommon_filefunctions_init();
if (std::filesystem::exists(real_filename)) {
// Generate a filename that definitely doesn't exist
char buf[1024];
if (oakcommon_filefunctions_get_safe_temporary_filename(
filefuncs, real_filename.c_str(), buf, sizeof(buf)) > 0) {
strncpy(params_.filename, buf, sizeof(params_.filename) - 1);
params_.filename[sizeof(params_.filename) - 1] = 0;
}
}
// If we're exporting to a sidecar subtitle file, disable the subtitles in the main encoder
bool subtitles_enabled = params_.subtitles_enabled != 0;
oakcodec_encoding_params sidecar_params = params_;
if (subtitles_enabled && params_.subtitles_are_sidecar) {
params_.subtitles_enabled = 0;
}
encoder_ = oakcodec_encoder_init(&params_);
if (!encoder_.ctx) {
set_error("Failed to create encoder");
return false;
}
if (oakcodec_encoder_open(encoder_) != OAKCODEC_OK) {
set_error("Failed to open file: " + encoder_error(encoder_));
return false;
}
if (subtitles_enabled && params_.subtitles_are_sidecar) {
// Construct sidecar params
sidecar_params.video_enabled = 0;
sidecar_params.audio_enabled = 0;
std::filesystem::path fi(real_filename);
std::string sidecar_filename = fi.stem().string();
char ext[64];
if (oakcodec_export_format_get_extension(
sidecar_params.subtitles_sidecar_format, ext,
sizeof(ext)) > 0) {
sidecar_filename += ".";
sidecar_filename += ext;
}
sidecar_filename =
(fi.parent_path() / sidecar_filename).string();
strncpy(sidecar_params.filename, sidecar_filename.c_str(),
sizeof(sidecar_params.filename) - 1);
sidecar_params.format = sidecar_params.subtitles_sidecar_format;
subtitle_encoder_ = oakcodec_encoder_init(&sidecar_params);
if (!subtitle_encoder_.ctx) {
set_error("Failed to create subtitle encoder");
return false;
}
if (oakcodec_encoder_open(subtitle_encoder_) != OAKCODEC_OK) {
set_error("Failed to open subtitle sidecar file: " +
sidecar_filename);
return false;
}
} else {
subtitle_encoder_ = encoder_;
}
if (params_.has_custom_range) {
// Render custom range only
export_range_ = TimeRange(
Rational(int(params_.custom_range_in_num),
int(params_.custom_range_in_den)),
Rational(int(params_.custom_range_out_num),
int(params_.custom_range_out_den)));
} else {
// Render entire sequence
int len_n = 0, len_d = 1;
oaknode_sequence_get_length(
reinterpret_cast<OakNodeSequence *>(viewer()), &len_n, &len_d);
export_range_ =
TimeRange(Rational(0), Rational(len_n, len_d));
}
frame_time_ = 0;
ForceParams force;
if (params_.video_enabled) {
// If a transformation matrix is applied to this video, create it here
int src_w = 0, src_h = 0;
oakcommon_videoparams_get_width(video_params(), &src_w);
oakcommon_videoparams_get_height(video_params(), &src_h);
if (src_w != params_.video_width || src_h != params_.video_height) {
force.width = params_.video_width;
force.height = params_.video_height;
if (params_.video_scaling_method != 0 /* k_stretch */) {
force.has_matrix = true;
oakcodec_encoding_generate_matrix(
params_.video_scaling_method, src_w, src_h,
params_.video_width, params_.video_height,
force.matrix);
}
} else {
// Disables forcing size in the renderer
force.width = 0;
force.height = 0;
}
// Create color processor
char reference_space[256];
if (oaknode_colormanager_get_reference_color_space(
color_manager_, reference_space,
sizeof(reference_space)) > 0) {
color_processor_ = oakrender_color_processor_create(
reference_space, params_.color_transform_output, 0);
}
force.format =
oakcodec_encoder_get_desired_pixel_format(encoder_);
force.channel_count = 4; /* RGBA */
force.color_output = color_processor_;
}
// Start render process
TimeRangeList video_range, audio_range;
TimeRange subtitle_range;
if (params_.video_enabled) {
if (export_range_.in() > 0) {
int tb_num = 0, tb_den = 1;
oakcommon_videoparams_frame_rate_as_time_base(video_params(),
&tb_num, &tb_den);
export_range_.set_in(Timecode::snap_time_to_timebase(
export_range_.in(), Rational(tb_num, tb_den)));
}
video_range = { export_range_ };
}
if (params_.audio_enabled) {
audio_range = { export_range_ };
}
if (subtitles_enabled) {
subtitle_range = export_range_;
}
render(color_manager_, video_range, audio_range, subtitle_range,
0 /* RenderMode::k_online */, nullptr, force);
bool success = true;
oakcodec_encoder_flush(encoder_);
std::string err = encoder_error(encoder_);
if (!err.empty()) {
set_error(err);
success = false;
}
if (subtitle_encoder_.ctx != encoder_.ctx) {
oakcodec_encoder_flush(subtitle_encoder_);
err = encoder_error(subtitle_encoder_);
if (!err.empty()) {
set_error(err);
success = false;
}
}
// If cancelled, delete the file we made, which is always a file we created since we write to a
// temp file during the actual encoding process
if (is_cancelled()) {
std::error_code ec;
std::filesystem::remove(params_.filename, ec);
} else if (real_filename != params_.filename) {
// If we were writing to a temp file, overwrite now
int renamed = 0;
if (oakcommon_filefunctions_rename_file_allow_overwrite(
filefuncs, params_.filename, real_filename.c_str(),
&renamed) != OAKCOMMON_OK ||
!renamed) {
set_error("Failed to overwrite \"" + real_filename +
"\". Export has been saved as \"" +
std::string(params_.filename) + "\" instead.");
success = false;
}
}
oakcommon_filefunctions_free(&filefuncs);
return success;
}
bool ExportTask::frame_downloaded(OakCodecFrame *f, const Rational &time)
{
// The worker pool finishes tickets without a result when no worker is
// available (or every worker crashed). Grinding through the whole
// timeline at several seconds per dead worker looks like a hang, so
// fail the export after a short streak of missing frames.
if (!f) {
if (++null_frame_streak_ >= 8) {
set_error("Render workers failed to deliver " +
std::to_string(null_frame_streak_) +
" consecutive frames; aborting export");
return false;
}
} else {
null_frame_streak_ = 0;
}
Rational actual_time = time - export_range_.in();
time_map_.insert({ actual_time, f });
while (!is_cancelled()) {
int tb_num = 0, tb_den = 1;
oakcommon_videoparams_frame_rate_as_time_base(video_params(),
&tb_num, &tb_den);
Rational real_time = Timecode::timestamp_to_time(
frame_time_, Rational(tb_num, tb_den));
auto it = time_map_.find(real_time);
if (it == time_map_.end()) {
break;
}
// Unfortunately this can't be done in another thread since the frames need to be sent
// one after the other chronologically.
OakFrame codec_frame = copy_frame_to_codec(it->second);
bool written = codec_frame.ctx &&
oakcodec_encoder_write_video(encoder_, codec_frame) ==
OAKCODEC_OK;
if (codec_frame.ctx) {
oakcodec_frame_free(&codec_frame);
}
if (!written) {
set_error(encoder_error(encoder_));
return false;
}
if (it->second) {
oakrender_codec_frame_free(it->second);
}
time_map_.erase(it);
frame_time_++;
emit_progress(double(frame_time_) /
double(get_total_number_of_frames()));
}
return true;
}
bool ExportTask::audio_downloaded(const TimeRange &range,
OakSampleBuffer *samples)
{
TimeRange adjusted_range = range - export_range_.in();
if (adjusted_range.in() == audio_time_) {
if (!write_audio_loop(adjusted_range, samples)) {
return false;
}
} else {
audio_map_.insert({ adjusted_range, samples });
}
return true;
}
bool ExportTask::encode_subtitle(OakNodeBlock *sub)
{
// The subtitle block's text is its standard "text" input
char text[8192];
if (oaknode_node_get_input_string(oaknode_block_as_node(sub), "text",
text, sizeof(text)) < 0) {
text[0] = 0;
}
int in_n = 0, in_d = 1, out_n = 0, out_d = 1;
oaknode_block_get_in(sub, &in_n, &in_d);
oaknode_block_get_out(sub, &out_n, &out_d);
double in_seconds = in_d ? double(in_n) / in_d : 0.0;
double out_seconds = out_d ? double(out_n) / out_d : 0.0;
if (oakcodec_encoder_write_subtitle(subtitle_encoder_, text,
in_seconds, out_seconds) !=
OAKCODEC_OK) {
set_error(encoder_error(subtitle_encoder_));
return false;
}
return true;
}
bool ExportTask::write_audio_loop(const TimeRange &time,
OakSampleBuffer *samples)
{
int channels = oakcore_samplebuffer_channel_count(samples);
size_t sample_count = oakcore_samplebuffer_sample_count(samples);
std::vector<float> interleaved(sample_count * (size_t)channels);
// SampleBuffer is planar; interleave for the encoder
std::vector<const float *> planar((size_t)channels);
oakcore_samplebuffer_to_raw_ptrs(samples,
const_cast<float **>(planar.data()));
for (size_t i = 0; i < sample_count; i++) {
for (int c = 0; c < channels; c++) {
interleaved[i * size_t(channels) + size_t(c)] =
planar[size_t(c)][i];
}
}
if (oakcodec_encoder_write_audio(encoder_, interleaved.data(),
int(sample_count)) != OAKCODEC_OK) {
set_error(encoder_error(encoder_));
return false;
}
audio_time_ = time.out();
for (auto it = audio_map_.begin(); it != audio_map_.end(); it++) {
TimeRange t = it->first;
OakSampleBuffer *s = it->second;
if (t.in() == audio_time_) {
// Erase from audio map since we're just about to write it
audio_map_.erase(it);
// Call recursively to write the next sample buffer
if (!write_audio_loop(t, s)) {
return false;
}
// Break out of loop
break;
}
}
return true;
}
}
+96
View File
@@ -0,0 +1,96 @@
/***
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/>.
***/
#ifndef OAK_EXPORTTASK_H
#define OAK_EXPORTTASK_H
#include <map>
#include "codec/encoder.h"
#include "node/colormanager.h"
#include "node/node.h"
#include "render/copier.h"
#include "../render/render.h"
namespace olive
{
/**
* @brief Export (encode) task over the oakcodec/oakrender C ABIs
*
* Takes a flat oakcodec_encoding_params POD instead of the C++
* EncodingParams class.
*/
class ExportTask : public RenderTask {
public:
ExportTask(OakNodeNode *viewer_node, OakNodeColorManager *color_manager,
const oakcodec_encoding_params &params);
virtual ~ExportTask() override;
protected:
virtual bool run() override;
virtual bool frame_downloaded(OakCodecFrame *frame,
const Rational &time) override;
virtual bool audio_downloaded(const TimeRange &range,
OakSampleBuffer *samples) override;
virtual bool encode_subtitle(OakNodeBlock *sub) override;
private:
bool write_audio_loop(const TimeRange &time, OakSampleBuffer *samples);
OakRenderProjectCopier *copier_;
std::map<Rational, OakCodecFrame *> time_map_;
struct TimeRangeLess {
bool operator()(const TimeRange &a, const TimeRange &b) const
{
return a.in() < b.in();
}
};
std::map<TimeRange, OakSampleBuffer *, TimeRangeLess> audio_map_;
OakNodeColorManager *color_manager_;
oakcodec_encoding_params params_;
OakEncoder encoder_;
OakEncoder subtitle_encoder_;
OakColorProcessor *color_processor_;
int64_t frame_time_;
int null_frame_streak_;
Rational audio_time_;
TimeRange export_range_;
};
}
#endif // OAK_EXPORTTASK_H
+202
View File
@@ -0,0 +1,202 @@
/***
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 "precachetask.h"
#include <vector>
#include "node/factory.h"
#include "node/node.h"
#include "node/project.h"
#include "render/cache.h"
#include "node/track.h"
#include "rendermodes.h"
#include "timeline/workarea.h"
namespace olive
{
namespace
{
const char *k_viewer_output_id = "org.olivevideoeditor.Olive.vieweroutput";
} // namespace
PreCacheTask::PreCacheTask(OakNodeFootage *footage, int index,
OakNodeSequence *sequence)
: project_(nullptr)
, footage_(nullptr)
, audio_params_(nullptr)
{
// Set video and audio params
OakVideoParams video_params = {};
if (oaknode_sequence_get_video_params(sequence, 0, &video_params) ==
OAKNODE_OK) {
set_video_params(video_params);
oakcommon_videoparams_free(&video_params);
}
oaknode_sequence_get_audio_params(sequence, 0, &audio_params_);
set_audio_params(audio_params_);
// Create new project
project_ = oaknode_project_init();
// Create viewer with same parameters as the sequence
set_viewer(oaknode_factory_create_from_id(k_viewer_output_id));
oaknode_project_add_node(project_, viewer());
if (video_params.ctx) {
oaknode_viewer_set_video_params(viewer(), &video_params);
}
if (audio_params_) {
oaknode_viewer_set_audio_params(viewer(), audio_params_);
}
// Copy project config nodes
OakNodeProject *source_project = nullptr;
oaknode_node_get_project(oaknode_footage_as_node(footage),
&source_project);
if (source_project) {
oaknode_project_copy_settings(project_, source_project);
}
// Copy footage node so it can precache without any modifications from the user screwing it up
OakNodeNode *footage_copy =
oaknode_node_create_copy(oaknode_footage_as_node(footage));
footage_ = reinterpret_cast<OakNodeFootage *>(
oaknode_block_from_node(footage_copy));
if (!footage_) {
footage_ = reinterpret_cast<OakNodeFootage *>(footage_copy);
}
oaknode_project_add_node(project_, footage_copy);
oaknode_node_copy_inputs(footage_copy, oaknode_footage_as_node(footage),
0);
oaknode_node_connect(footage_copy, viewer(),
OAKNODE_SEQUENCE_TEXTURE_INPUT);
oaknode_node_set_value_hint_track(viewer(),
OAKNODE_SEQUENCE_TEXTURE_INPUT,
OAKNODE_TRACK_TYPE_VIDEO, index);
char filename[1024];
if (oaknode_footage_filename(footage, filename, sizeof(filename)) <=
0) {
filename[0] = 0;
}
set_title("Pre-caching " + std::string(filename) + ":" +
std::to_string(index));
}
PreCacheTask::~PreCacheTask()
{
// This should delete the footage we copied and the viewer we created
oaknode_project_free(project_);
if (audio_params_) {
oakcore_audioparams_free(audio_params_);
}
}
bool PreCacheTask::run()
{
// Get list of invalidated ranges
TimeRange intersection;
OakTimelineWorkArea *workarea =
oaktimeline_workarea_of(oaknode_footage_as_node(footage_));
int64_t len_n = 0, len_d = 1;
oaknode_footage_get_video_length(footage_, &len_n, &len_d);
Rational video_length((int)len_n, (int)len_d);
int wa_enabled = 0;
int wa_in = 0, wa_ind = 1, wa_out = 0, wa_outd = 1;
if (workarea) {
oaktimeline_workarea_get(workarea, &wa_in, &wa_ind, &wa_out,
&wa_outd, &wa_enabled);
}
if (workarea && wa_enabled) {
// If we're caching only in-out, limit the range to that
intersection = TimeRange(Rational(wa_in, wa_ind),
Rational(wa_out, wa_outd));
} else {
// Otherwise use full length
intersection = TimeRange(Rational(0), video_length);
}
OakNodeFrameCache *cache = nullptr;
oaknode_node_get_video_frame_cache(viewer(), &cache);
int range_count = oakrender_cache_get_invalidated_ranges(
reinterpret_cast<OakRenderCache *>(cache),
intersection.in().numerator(), intersection.in().denominator(),
intersection.out().numerator(), intersection.out().denominator(),
nullptr, 0);
TimeRangeList video_range;
if (range_count > 0) {
std::vector<int64_t> flat(size_t(range_count) * 4);
oakrender_cache_get_invalidated_ranges(
reinterpret_cast<OakRenderCache *>(cache),
intersection.in().numerator(), intersection.in().denominator(),
intersection.out().numerator(), intersection.out().denominator(),
flat.data(), range_count);
for (int i = 0; i < range_count; i++) {
video_range.insert(TimeRange(
Rational(int(flat[i * 4 + 0]), int(flat[i * 4 + 1])),
Rational(int(flat[i * 4 + 2]), int(flat[i * 4 + 3]))));
}
}
OakNodeColorManager *color_manager =
oaknode_colormanager_init(project_);
render(color_manager, video_range, TimeRangeList(), TimeRange(),
0 /* RenderMode::k_online */, cache, ForceParams());
oaknode_colormanager_free(color_manager);
return true;
}
bool PreCacheTask::frame_downloaded(OakCodecFrame *frame,
const Rational &time)
{
// Do nothing. Pre-cache essentially just creates more frames in the cache, it doesn't need to do
// anything else.
(void)frame;
(void)time;
return true;
}
bool PreCacheTask::audio_downloaded(const TimeRange &range,
OakSampleBuffer *samples)
{
// Pre-cache doesn't cache any audio
(void)range;
(void)samples;
return true;
}
}
@@ -1,113 +0,0 @@
/***
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 "precachetask.h"
#include "node/project.h"
namespace olive
{
PreCacheTask::PreCacheTask(Footage *footage, int index, Sequence *sequence)
{
// Set video and audio params
set_video_params(sequence->get_video_params());
set_audio_params(sequence->get_audio_params());
// Create new project
project_ = new Project();
// Create viewer with same parameters as the sequence
set_viewer(new ViewerOutput());
viewer()->setParent(project_);
viewer()->set_video_params(sequence->get_video_params());
viewer()->set_audio_params(sequence->get_audio_params());
// Copy project config nodes
Project::copy_settings(footage->project(), project_);
// Copy footage node so it can precache without any modifications from the user screwing it up
footage_ = static_cast<Footage *>(footage->copy());
footage_->setParent(project_);
Node::copy_inputs(footage, footage_, false);
Node::connect_edge(footage_,
NodeInput(viewer(), ViewerOutput::k_texture_input));
viewer()->set_value_hint_for_input(
ViewerOutput::k_texture_input,
Node::ValueHint({ NodeValue::k_texture },
Track::Reference(Track::k_video, index).to_string()));
set_title(tr("Pre-caching %1:%2")
.arg(footage_->filename(), QString::number(index)));
}
PreCacheTask::~PreCacheTask()
{
// This should delete the footage we copied and the viewer we created
delete project_;
}
bool PreCacheTask::run()
{
// Get list of invalidated ranges
TimeRange intersection;
if (footage_->get_work_area()->enabled()) {
// If we're caching only in-out, limit the range to that
intersection = footage_->get_work_area()->range();
} else {
// Otherwise use full length
intersection = TimeRange(0, footage_->get_video_length());
}
TimeRangeList video_range =
viewer()->video_frame_cache()->get_invalidated_ranges(intersection);
render(project_->color_manager(), video_range, TimeRangeList(), TimeRange(),
RenderMode::k_online, viewer()->video_frame_cache());
return true;
}
bool PreCacheTask::frame_downloaded(FramePtr frame, const Rational &time)
{
// Do nothing. Pre-cache essentially just creates more frames in the cache, it doesn't need to do
// anything else.
Q_UNUSED(frame)
Q_UNUSED(time)
return true;
}
bool PreCacheTask::audio_downloaded(const TimeRange &range,
const SampleBuffer &samples)
{
// Pre-cache doesn't cache any audio
Q_UNUSED(range)
Q_UNUSED(samples)
return true;
}
}
@@ -22,33 +22,35 @@
#ifndef OAK_PRECACHETASK_H
#define OAK_PRECACHETASK_H
#include "node/project/footage/footage.h"
#include "node/project/sequence/sequence.h"
#include "task/render/render.h"
#include "node/footage.h"
#include "node/sequence.h"
#include "render/render.h"
namespace olive
{
class PreCacheTask : public RenderTask {
Q_OBJECT
public:
PreCacheTask(Footage *footage, int index, Sequence *sequence);
PreCacheTask(OakNodeFootage *footage, int index,
OakNodeSequence *sequence);
virtual ~PreCacheTask() override;
protected:
virtual bool run() override;
virtual bool frame_downloaded(FramePtr frame,
const Rational &times) override;
virtual bool frame_downloaded(OakCodecFrame *frame,
const Rational &time) override;
virtual bool audio_downloaded(const TimeRange &range,
const SampleBuffer &samples) override;
OakSampleBuffer *samples) override;
private:
Project *project_;
OakNodeProject *project_;
Footage *footage_;
OakNodeFootage *footage_;
OakAudioParams *audio_params_;
};
}
@@ -1,34 +0,0 @@
/***
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 "loadbasetask.h"
namespace olive
{
ProjectLoadBaseTask::ProjectLoadBaseTask(const QString &filename)
: project_(nullptr)
, filename_(filename)
{
set_title(tr("Loading '%1'").arg(filename));
}
}
@@ -1,63 +0,0 @@
/***
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/>.
***/
#ifndef OAK_PROJECTLOADBASETASK_H
#define OAK_PROJECTLOADBASETASK_H
#include "node/project.h"
#include "node/project/serializer/serializedlayoutinfo.h"
#include "task/task.h"
namespace olive
{
class ProjectLoadBaseTask : public Task {
Q_OBJECT
public:
ProjectLoadBaseTask(const QString &filename);
Project *get_loaded_project() const
{
return project_;
}
const QString &get_filename() const
{
return filename_;
}
const SerializedLayoutInfo &get_loaded_layout() const
{
return layout_;
}
protected:
Project *project_;
SerializedLayoutInfo layout_;
private:
QString filename_;
};
}
#endif // LOADBASETASK_H
+409
View File
@@ -0,0 +1,409 @@
/***
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 "render.h"
#include <thread>
#include "node/block.h"
#include "node/sequence.h"
#include "node/track.h"
namespace olive
{
namespace
{
Rational task_block_in(OakNodeBlock *b)
{
int n = 0, d = 1;
oaknode_block_get_in(b, &n, &d);
return Rational(n, d);
}
Rational task_block_out(OakNodeBlock *b)
{
int n = 0, d = 1;
oaknode_block_get_out(b, &n, &d);
return Rational(n, d);
}
} // namespace
RenderTask::RenderTask()
: viewer_(nullptr)
, video_params_({})
, audio_params_(nullptr)
, running_tickets_(0)
, native_progress_signalling_(true)
, total_number_of_frames_(0)
{
}
RenderTask::~RenderTask()
{
if (video_params_.ctx) {
oakcommon_videoparams_free(&video_params_);
}
}
void RenderTask::on_ticket_finished(OakRenderTicket *ticket)
{
finished_mutex_.lock();
finished_tickets_.push_back(ticket);
finished_wait_cond_.notify_all();
running_tickets_--;
finished_mutex_.unlock();
}
bool RenderTask::start_video_ticket(OakNodeColorManager *manager,
const Rational &time, int mode,
OakNodeFrameCache *cache,
const ForceParams &force)
{
OakNodeNode *output_node = nullptr;
oaknode_node_input_get_connected_node(
viewer_, OAKNODE_SEQUENCE_TEXTURE_INPUT, &output_node);
if (!output_node) {
return false;
}
oakrender_video_ticket_params params = {};
params.output_node = output_node;
params.video_params = video_params_;
params.audio_params = audio_params_;
params.time_num = time.numerator();
params.time_den = time.denominator();
params.color_manager = manager;
params.mode = mode;
params.force_width = force.width;
params.force_height = force.height;
params.has_force_matrix = force.has_matrix ? 1 : 0;
for (int i = 0; i < 16; i++) {
params.force_matrix[i] = force.matrix[i];
}
params.force_format = force.format;
params.force_channel_count = force.channel_count;
params.force_color_output = force.color_output;
params.force_color_transform = force.color_transform;
params.cache = cache;
OakRenderTicket *ticket = oakrender_ticket_render_frame(
&params,
[](OakRenderTicket *t, void *userdata) {
static_cast<RenderTask *>(userdata)->on_ticket_finished(t);
},
this);
if (!ticket) {
return false;
}
finished_mutex_.lock();
running_ticket_list_.push_back(ticket);
running_tickets_++;
finished_mutex_.unlock();
return true;
}
bool RenderTask::render(OakNodeColorManager *manager,
const TimeRangeList &video_range,
const TimeRangeList &audio_range,
const TimeRange &subtitle_range, int render_mode,
OakNodeFrameCache *cache, const ForceParams &force)
{
oakrender_manager_set_aggressive_gc(1);
double progress_counter = 0;
double total_length = 0;
// Queue audio jobs
for (const TimeRange &range : audio_range) {
OakNodeNode *output_node = nullptr;
oaknode_node_input_get_connected_node(
viewer_, OAKNODE_SEQUENCE_SAMPLES_INPUT, &output_node);
if (!output_node) {
continue;
}
OakRenderTicket *ticket = oakrender_ticket_render_audio(
output_node, range.in().numerator(), range.in().denominator(),
range.out().numerator(), range.out().denominator(),
audio_params_, render_mode,
[](OakRenderTicket *t, void *userdata) {
static_cast<RenderTask *>(userdata)->on_ticket_finished(t);
},
this);
if (ticket) {
finished_mutex_.lock();
running_ticket_list_.push_back(ticket);
running_tickets_++;
finished_mutex_.unlock();
}
}
// Frame timestamps
Rational timebase;
{
int tb_num = 0, tb_den = 1;
oakcommon_videoparams_frame_rate_as_time_base(video_params_,
&tb_num, &tb_den);
timebase = Rational(tb_num, tb_den);
}
std::vector<Rational> frame_times;
for (const TimeRange &range : video_range) {
for (Rational t = range.in(); t < range.out(); t += timebase) {
frame_times.push_back(t);
}
}
total_number_of_frames_ = int64_t(frame_times.size());
total_length += double(total_number_of_frames_);
if (total_length <= 0) {
total_length = 1;
}
// Start a limited number of renders, then start one more for each
// that finishes, so rendered frames don't stack up in memory
const int maximum_rendered_frames =
std::max(1, int(std::thread::hardware_concurrency()));
size_t next_frame_index = 0;
for (int i = 0;
i < maximum_rendered_frames && next_frame_index < frame_times.size();
i++, next_frame_index++) {
start_video_ticket(manager, frame_times[next_frame_index],
render_mode, cache, force);
}
bool result = true;
// Subtitle loop, loops over all blocks in sequence on all tracks
if (!subtitle_range.length().isNull()) {
OakNodeSequence *sequence =
reinterpret_cast<OakNodeSequence *>(viewer_);
OakNodeTrackList *list = nullptr;
oaknode_sequence_get_track_list(
sequence, OAKNODE_TRACK_TYPE_SUBTITLE, &list);
if (list) {
int track_count = 0;
oaknode_tracklist_get_track_count(list, &track_count);
std::vector<int> block_indexes(size_t(track_count), 0);
std::vector<int> tracks_to_push;
do {
tracks_to_push.clear();
for (int i = 0; i < track_count; i++) {
OakNodeTrack *this_track = nullptr;
oaknode_tracklist_get_track_at(list, i, &this_track);
if (!this_track) {
continue;
}
int muted = 0;
oaknode_track_get_muted(this_track, &muted);
if (muted) {
continue;
}
int this_block_count = 0;
oaknode_track_get_block_count(this_track,
&this_block_count);
int &this_block_index = block_indexes[size_t(i)];
if (this_block_index >= this_block_count) {
continue;
}
OakNodeBlock *this_block = nullptr;
oaknode_track_get_block_at(this_track,
this_block_index,
&this_block);
OakNodeTrack *compare_track = nullptr;
if (!tracks_to_push.empty()) {
oaknode_tracklist_get_track_at(
list, tracks_to_push.front(), &compare_track);
}
OakNodeBlock *compare_block = nullptr;
if (compare_track) {
oaknode_track_get_block_at(
compare_track,
block_indexes[size_t(
tracks_to_push.front())],
&compare_block);
}
if (!compare_track ||
task_block_out(compare_block) >= task_block_in(this_block)) {
if (compare_track &&
task_block_in(compare_block) !=
task_block_in(this_block)) {
tracks_to_push.clear();
}
tracks_to_push.push_back(i);
}
}
for (int i : tracks_to_push) {
OakNodeTrack *this_track = nullptr;
oaknode_tracklist_get_track_at(list, i, &this_track);
OakNodeBlock *this_block = nullptr;
oaknode_track_get_block_at(
this_track, block_indexes[size_t(i)], &this_block);
int kind = OAKNODE_BLOCK_OTHER;
if (this_block) {
oaknode_block_get_kind(this_block, &kind);
}
if (this_block && kind != OAKNODE_BLOCK_GAP) {
int enabled = 0;
oaknode_block_get_enabled(this_block, &enabled);
if (enabled) {
if (!encode_subtitle(this_block)) {
result = false;
break;
}
}
}
block_indexes[size_t(i)]++;
}
} while (!tracks_to_push.empty());
}
}
std::unique_lock<std::mutex> loop_lock(finished_mutex_);
while (result && !is_cancelled()) {
while (!finished_tickets_.empty() && !is_cancelled() && result) {
OakRenderTicket *ticket = finished_tickets_.front();
finished_tickets_.pop_front();
loop_lock.unlock();
int type = oakrender_ticket_get_type(ticket);
if (type == OAKRENDER_TICKET_AUDIO) {
Rational range_in;
Rational range_out;
{
int64_t rn = 0, rd = 1, ro_n = 0, ro_d = 1;
oakrender_ticket_get_range(ticket, &rn, &rd, &ro_n,
&ro_d);
range_in = Rational((int)rn, (int)rd);
range_out = Rational((int)ro_n, (int)ro_d);
}
TimeRange range(range_in, range_out);
OakSampleBuffer *samples = nullptr;
if (oakrender_ticket_get_samples(ticket, &samples) ==
OAKRENDER_OK) {
if (!audio_downloaded(range, samples)) {
result = false;
}
oakcore_samplebuffer_free(samples);
} else {
result = false;
}
} else {
int64_t tn, td;
oakrender_ticket_get_time(ticket, &tn, &td);
Rational time((int)tn, (int)td);
OakCodecFrame *frame = nullptr;
oakrender_ticket_get_frame(ticket, &frame);
if (two_step_frame_rendering() &&
!download_frame(frame, time)) {
result = false;
} else if (!frame_downloaded(frame, time)) {
result = false;
}
if (native_progress_signalling_) {
double progress_to_add = 1.0;
if (two_step_frame_rendering()) {
progress_to_add *= 0.5;
}
progress_counter += progress_to_add;
emit_progress(progress_counter / total_length);
}
if (frame) {
oakrender_codec_frame_free(frame);
}
if (next_frame_index < frame_times.size()) {
start_video_ticket(manager,
frame_times[next_frame_index++],
render_mode, cache, force);
}
}
oakrender_ticket_free(ticket);
loop_lock.lock();
}
if (is_cancelled() || !result) {
break;
}
if (running_tickets_ > 0) {
finished_wait_cond_.wait(loop_lock);
} else {
break;
}
}
loop_lock.unlock();
if (is_cancelled() || !result) {
// Cancel every ticket we created
for (OakRenderTicket *ticket : running_ticket_list_) {
oakrender_ticket_cancel(ticket);
oakrender_ticket_wait(ticket);
oakrender_ticket_free(ticket);
}
}
oakrender_manager_set_aggressive_gc(0);
return result;
}
bool RenderTask::download_frame(OakCodecFrame *frame, const Rational &time)
{
(void)frame;
(void)time;
// NOTE: Doesn't reflect the actual return result of SaveFrameToCache
return true;
}
bool RenderTask::encode_subtitle(OakNodeBlock *subtitle)
{
(void)subtitle;
return true;
}
}
-352
View File
@@ -1,352 +0,0 @@
/***
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 "render.h"
#include "node/project/sequence/sequence.h"
#include "render/rendermanager.h"
namespace olive
{
RenderTask::RenderTask()
: running_tickets_(0)
, native_progress_signalling_(true)
{
}
RenderTask::~RenderTask()
{
}
bool RenderTask::render(ColorManager *manager, const TimeRangeList &video_range,
const TimeRangeList &audio_range,
const TimeRange &subtitle_range, RenderMode::Mode mode,
FrameHashCache *cache, const QSize &force_size,
const QMatrix4x4 &force_matrix,
PixelFormat force_format, int force_channel_count,
ColorProcessorPtr force_color_output,
const ColorTransform &force_color_transform)
{
QMetaObject::invokeMethod(RenderManager::instance(),
"SetAggressiveGarbageCollection",
Q_ARG(bool, true));
// Run watchers in another thread so they can accept signals even while this thread is blocked
QThread watcher_thread;
watcher_thread.start();
double progress_counter = 0;
double total_length = 0;
// Store real time before any rendering takes place
// Queue audio jobs
for (const TimeRange &range : audio_range) {
// Don't count audio progress, since it's generally a lot faster than video and is weighted at
// 50%, which makes the progress bar look weird to the uninitiated
//total_length += r.length().toDouble();
RenderManager::RenderAudioParams rap(
viewer_->get_connected_sample_output(), range, audio_params_,
RenderMode::k_online);
RenderTicketWatcher *watcher = new RenderTicketWatcher();
watcher->setProperty("range", QVariant::fromValue(range));
prepare_watcher(watcher, &watcher_thread);
increment_running_tickets();
watcher->set_ticket(RenderManager::instance()->render_audio(rap));
}
// Look up hashes
TimeRangeListFrameIterator iterator(
video_range, video_params().frame_rate_as_time_base());
total_number_of_frames_ = iterator.size();
total_length += total_number_of_frames_;
// Start a render of a limited amount, and then render one frame for each frame that gets
// finished. This prevents rendered frames from stacking up in memory indefinitely while the
// encoder is processing them. The amount is kind of arbitrary, but we use the thread count so
// each of the system's threads are utilized as memory allows.
const int maximum_rendered_frames = QThread::idealThreadCount();
Rational next_frame;
for (int i = 0;
i < maximum_rendered_frames && iterator.get_next(&next_frame); i++) {
start_ticket(&watcher_thread, manager, next_frame, mode, cache,
force_size, force_matrix, force_format, force_channel_count,
force_color_output, force_color_transform);
}
bool result = true;
// Subtitle loop, loops over all blocks in sequence on all tracks
if (!subtitle_range.length().isNull()) {
if (Sequence *sequence = dynamic_cast<Sequence *>(viewer_)) {
TrackList *list = sequence->track_list(Track::k_subtitle);
QVector<int> block_indexes(list->get_track_count(), 0);
QVector<int> tracks_to_push;
do {
tracks_to_push.clear();
for (int i = 0; i < block_indexes.size(); i++) {
Track *this_track = list->get_track_at(i);
if (this_track->is_muted()) {
continue;
}
int &this_block_index = block_indexes[i];
if (this_block_index >= this_track->blocks().size()) {
continue;
}
Block *this_block =
this_track->blocks().at(this_block_index);
Track *compare_track =
tracks_to_push.isEmpty() ?
nullptr :
list->get_track_at(tracks_to_push.first());
const int &compare_block_index =
tracks_to_push.isEmpty() ?
-1 :
block_indexes.at(tracks_to_push.first());
Block *compare_block =
compare_track ?
compare_track->blocks().at(compare_block_index) :
nullptr;
if (!compare_track ||
compare_block->in() >= this_block->in()) {
if (compare_track &&
compare_block->in() != this_block->in()) {
tracks_to_push.clear();
}
tracks_to_push.append(i);
}
}
for (int i = 0; i < tracks_to_push.size(); i++) {
Track *this_track = list->get_track_at(tracks_to_push.at(i));
Block *this_block = this_track->blocks().at(
block_indexes.at(tracks_to_push.at(i)));
if (const SubtitleBlock *sub =
dynamic_cast<const SubtitleBlock *>(this_block)) {
if (sub->is_enabled()) {
if (!encode_subtitle(sub)) {
result = false;
break;
}
}
}
block_indexes[tracks_to_push.at(i)]++;
}
} while (!tracks_to_push.isEmpty());
}
}
finished_watcher_mutex_.lock();
while (result && !is_cancelled()) {
while (!finished_watchers_.empty() && !is_cancelled() && result) {
RenderTicketWatcher *watcher = finished_watchers_.front();
finished_watchers_.pop_front();
finished_watcher_mutex_.unlock();
// Analyze watcher here
RenderManager::TicketType ticket_type =
watcher->get_ticket()
->property("type")
.value<RenderManager::TicketType>();
if (ticket_type == RenderManager::k_type_audio) {
TimeRange range = watcher->property("range").value<TimeRange>();
if (!audio_downloaded(range,
watcher->get().value<SampleBuffer>())) {
result = false;
}
// Don't count audio progress, since it's generally a lot faster than video and is weighted at
// 50%, which makes the progress bar look weird to the uninitiated
//progress_counter += range.length().toDouble();
//emit ProgressChanged(progress_counter / total_length);
} else if (ticket_type == RenderManager::k_type_video &&
two_step_frame_rendering()) {
if (!download_frame(
&watcher_thread, watcher->get().value<FramePtr>(),
watcher->property("time").value<Rational>())) {
result = false;
}
if (native_progress_signalling_) {
progress_counter += 0.5;
emit progress_changed(progress_counter / total_length);
}
} else {
// Assume single-step video or video download ticket
if (!frame_downloaded(
watcher->get().value<FramePtr>(),
watcher->property("time").value<Rational>())) {
result = false;
}
if (native_progress_signalling_) {
double progress_to_add = 1.0;
if (two_step_frame_rendering()) {
progress_to_add *= 0.5;
}
progress_counter += progress_to_add;
emit progress_changed(progress_counter / total_length);
}
if (iterator.get_next(&next_frame)) {
start_ticket(&watcher_thread, manager, next_frame, mode,
cache, force_size, force_matrix, force_format,
force_channel_count, force_color_output,
force_color_transform);
}
}
delete watcher;
running_watchers_.removeOne(watcher);
finished_watcher_mutex_.lock();
}
if (is_cancelled() || !result) {
break;
}
// Run out of finished watchers. If we still have running tickets, wait for the next one to finish.
if (running_tickets_ > 0) {
finished_watcher_wait_cond_.wait(&finished_watcher_mutex_);
} else {
// No more running tickets or finished tickets, wem ust be
break;
}
}
finished_watcher_mutex_.unlock();
if (is_cancelled() || !result) {
// Cancel every watcher we created
foreach (RenderTicketWatcher *watcher, running_watchers_) {
watcher->cancel();
disconnect(watcher, &RenderTicketWatcher::finished, this,
&RenderTask::ticket_done);
RenderManager::instance()->remove_ticket(watcher->get_ticket());
}
foreach (RenderTicketWatcher *watcher, running_watchers_) {
watcher->wait_for_finished();
}
}
watcher_thread.quit();
watcher_thread.wait();
QMetaObject::invokeMethod(RenderManager::instance(),
"SetAggressiveGarbageCollection",
Q_ARG(bool, false));
return result;
}
bool RenderTask::download_frame(QThread *thread, FramePtr frame,
const Rational &time)
{
//RenderTicketWatcher* watcher = new RenderTicketWatcher();
//PrepareWatcher(watcher, thread);
//IncrementRunningTickets();
//watcher->SetTicket(RenderManager::instance()->SaveFrameToCache(viewer_->video_frame_cache(), frame, time));
// NOTE: Doesn't reflect the actual return result of SaveFrameToCache
return true;
}
bool RenderTask::encode_subtitle(const SubtitleBlock *subtitle)
{
Q_UNUSED(subtitle)
return true;
}
void RenderTask::prepare_watcher(RenderTicketWatcher *watcher, QThread *thread)
{
watcher->moveToThread(thread);
connect(watcher, &RenderTicketWatcher::finished, this,
&RenderTask::ticket_done, Qt::DirectConnection);
running_watchers_.append(watcher);
}
void RenderTask::increment_running_tickets()
{
finished_watcher_mutex_.lock();
running_tickets_++;
finished_watcher_mutex_.unlock();
}
void RenderTask::start_ticket(QThread *watcher_thread, ColorManager *manager,
const Rational &time, RenderMode::Mode mode,
FrameHashCache *cache, const QSize &force_size,
const QMatrix4x4 &force_matrix,
PixelFormat force_format, int force_channel_count,
ColorProcessorPtr force_color_output,
const ColorTransform &force_color_transform)
{
RenderManager::RenderVideoParams rvp(viewer_->get_connected_texture_output(),
video_params_, audio_params_, time,
manager, mode);
rvp.force_size = force_size;
rvp.force_matrix = force_matrix;
rvp.force_format = force_format;
rvp.force_color_output = force_color_output;
rvp.force_color_transform = force_color_transform;
rvp.force_channel_count = force_channel_count;
if (cache) {
rvp.add_cache(cache);
}
RenderTicketWatcher *watcher = new RenderTicketWatcher();
watcher->setProperty("time", QVariant::fromValue(time));
prepare_watcher(watcher, watcher_thread);
increment_running_tickets();
watcher->set_ticket(RenderManager::instance()->render_frame(rvp));
}
void RenderTask::ticket_done(RenderTicketWatcher *watcher)
{
finished_watcher_mutex_.lock();
finished_watchers_.push_back(watcher);
finished_watcher_wait_cond_.wakeAll();
running_tickets_--;
finished_watcher_mutex_.unlock();
}
}
+189
View File
@@ -0,0 +1,189 @@
/***
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/>.
***/
#ifndef OAK_RENDERTASK_H
#define OAK_RENDERTASK_H
#include <condition_variable>
#include <deque>
#include <mutex>
#include <vector>
#include <olive/core/core.h>
#include "common/colortransform.h"
#include "common/videoparams.h"
#include "node/block.h"
#include "node/colormanager.h"
#include "node/node.h"
#include "render/ticket.h"
#include "task.h"
namespace olive
{
using namespace core;
/**
* @brief Task that renders a set of video/audio ranges through
* oakrender tickets
*
* De-Qt version: drives the oakrender ticket C API; the finished
* callback is the ticket's return channel. The two-step
* texture-then-download rendering distinction is gone (tickets always
* produce frames), so every video ticket counts full progress.
*/
class RenderTask : public Task {
public:
RenderTask();
virtual ~RenderTask() override;
protected:
struct ForceParams {
int width = 0; /**< 0/0 = off */
int height = 0;
double matrix[16] = { 0 };
bool has_matrix = false;
int format = -1; /**< PixelFormat as int, -1 = off */
int channel_count = 0; /**< 0 = off */
OakColorProcessor *color_output = nullptr; /**< borrowed */
OakColorTransform color_transform = {}; /**< empty ctx = default */
};
bool render(OakNodeColorManager *manager,
const TimeRangeList &video_range,
const TimeRangeList &audio_range,
const TimeRange &subtitle_range, int render_mode,
OakNodeFrameCache *cache,
const ForceParams &force);
virtual bool download_frame(OakCodecFrame *frame, const Rational &time);
virtual bool frame_downloaded(OakCodecFrame *frame,
const Rational &time) = 0;
virtual bool audio_downloaded(const TimeRange &range,
OakSampleBuffer *samples) = 0;
virtual bool encode_subtitle(OakNodeBlock *subtitle);
OakNodeNode *viewer() const
{
return viewer_;
}
void set_viewer(OakNodeNode *v)
{
viewer_ = v;
}
OakVideoParams video_params() const
{
return video_params_;
}
void set_video_params(const OakVideoParams &video_params)
{
if (video_params_.ctx == video_params.ctx && video_params_.ctx) {
return;
}
if (video_params_.ctx) {
oakcommon_videoparams_free(&video_params_);
}
video_params_ = video_params;
if (video_params_.ctx) {
video_params_.addref(video_params_.ctx);
}
}
OakAudioParams *audio_params() const
{
return audio_params_;
}
void set_audio_params(OakAudioParams *audio_params)
{
audio_params_ = audio_params;
}
/**
* @brief Kept for API parity; with ticket-based rendering every video
* ticket produces a frame, so the two-step texture/download
* split no longer exists and this is always false.
*/
virtual bool two_step_frame_rendering() const
{
return false;
}
virtual void cancel_event() override
{
finished_mutex_.lock();
finished_wait_cond_.notify_all();
finished_mutex_.unlock();
}
void set_native_progress_signalling_enabled(bool e)
{
native_progress_signalling_ = e;
}
/**
* @brief Only valid after render() is called
*/
int64_t get_total_number_of_frames() const
{
return total_number_of_frames_;
}
private:
struct VideoTicketRequest {
Rational time;
};
void on_ticket_finished(OakRenderTicket *ticket);
bool start_video_ticket(OakNodeColorManager *manager,
const Rational &time, int mode,
OakNodeFrameCache *cache,
const ForceParams &force);
OakNodeNode *viewer_;
OakVideoParams video_params_;
OakAudioParams *audio_params_;
std::mutex finished_mutex_;
std::condition_variable finished_wait_cond_;
std::deque<OakRenderTicket *> finished_tickets_;
std::vector<OakRenderTicket *> running_ticket_list_;
int running_tickets_;
bool native_progress_signalling_;
int64_t total_number_of_frames_;
};
}
#endif // OAK_RENDERTASK_H
-154
View File
@@ -1,154 +0,0 @@
/***
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/>.
***/
#ifndef OAK_RENDERTASK_H
#define OAK_RENDERTASK_H
#include <QtConcurrent/QtConcurrent>
#include "node/block/subtitle/subtitle.h"
#include "node/color/colormanager/colormanager.h"
#include "node/output/viewer/viewer.h"
#include "task/task.h"
#include "render/renderticket.h"
namespace olive
{
class RenderTask : public Task {
Q_OBJECT
public:
RenderTask();
virtual ~RenderTask() override;
protected:
bool render(ColorManager *manager, const TimeRangeList &video_range,
const TimeRangeList &audio_range,
const TimeRange &subtitle_range, RenderMode::Mode mode,
FrameHashCache *cache, const QSize &force_size = QSize(0, 0),
const QMatrix4x4 &force_matrix = QMatrix4x4(),
PixelFormat force_format = PixelFormat::invalid,
int force_channel_count = 0,
ColorProcessorPtr force_color_output = nullptr,
const ColorTransform &force_color_transform = ColorTransform());
virtual bool download_frame(QThread *thread, FramePtr frame,
const Rational &time);
virtual bool frame_downloaded(FramePtr frame, const Rational &time) = 0;
virtual bool audio_downloaded(const TimeRange &range,
const SampleBuffer &samples) = 0;
virtual bool encode_subtitle(const SubtitleBlock *subtitle);
ViewerOutput *viewer() const
{
return viewer_;
}
void set_viewer(ViewerOutput *v)
{
viewer_ = v;
}
const VideoParams &video_params() const
{
return video_params_;
}
void set_video_params(const VideoParams &video_params)
{
video_params_ = video_params;
}
const AudioParams &audio_params() const
{
return audio_params_;
}
void set_audio_params(const AudioParams &audio_params)
{
audio_params_ = audio_params;
}
virtual void CancelEvent() override
{
finished_watcher_mutex_.lock();
finished_watcher_wait_cond_.wakeAll();
finished_watcher_mutex_.unlock();
}
virtual bool two_step_frame_rendering() const
{
return true;
}
void set_native_progress_signalling_enabled(bool e)
{
native_progress_signalling_ = e;
}
/**
* @brief Only valid after Render() is called
*/
int64_t get_total_number_of_frames() const
{
return total_number_of_frames_;
}
private:
void prepare_watcher(RenderTicketWatcher *watcher, QThread *thread);
void increment_running_tickets();
void start_ticket(QThread *watcher_thread, ColorManager *manager,
const Rational &time, RenderMode::Mode mode,
FrameHashCache *cache, const QSize &force_size,
const QMatrix4x4 &force_matrix, PixelFormat force_format,
int force_channel_count,
ColorProcessorPtr force_color_output,
const ColorTransform &force_color_transform);
ViewerOutput *viewer_;
VideoParams video_params_;
AudioParams audio_params_;
QVector<RenderTicketWatcher *> running_watchers_;
std::list<RenderTicketWatcher *> finished_watchers_;
int running_tickets_;
QMutex finished_watcher_mutex_;
QWaitCondition finished_watcher_wait_cond_;
bool native_progress_signalling_;
int64_t total_number_of_frames_;
private slots:
void ticket_done(RenderTicketWatcher *watcher);
};
}
#endif // OAK_RENDERTASK_H
+48
View File
@@ -314,3 +314,51 @@ TEST(OakTaskConform, SubmittedConformProducesPcm)
}
} // namespace
// ---- render family factories ----------------------------------------------
TEST(OakTaskRenderFamily, FactoryErrorPaths)
{
EXPECT_EQ(oaktask_create_precache(nullptr, 0, nullptr), nullptr);
oakcodec_encoding_params params = {};
EXPECT_EQ(oaktask_create_export(nullptr, nullptr, &params), nullptr);
EXPECT_EQ(oaktask_create_export(nullptr, nullptr, nullptr), nullptr);
}
TEST(OakTaskRenderFamily, ExportTaskConstruction)
{
OakNodeProject *project = oaknode_project_init();
ASSERT_NE(project, nullptr);
ASSERT_EQ(oaknode_project_initialize(project), OAKNODE_OK);
// A sequence gives us a viewer with tracks; the factory should wrap
// the task successfully (no render manager needed for construction)
OakNodeSequence *sequence = oaknode_sequence_create();
ASSERT_NE(sequence, nullptr);
ASSERT_EQ(oaknode_project_add_node(project,
oaknode_sequence_as_node(sequence)),
OAKNODE_OK);
OakNodeColorManager *cm = oaknode_colormanager_init(project);
ASSERT_NE(cm, nullptr);
oakcodec_encoding_params params = {};
strncpy(params.filename, "/tmp/oaktask_export_test.mp4",
sizeof(params.filename) - 1);
params.video_enabled = 1;
OakTaskTask *t = oaktask_create_export(
oaknode_sequence_as_node(sequence), cm, &params);
ASSERT_NE(t, nullptr);
char title[128];
EXPECT_GT(oaktask_task_title(t, title, sizeof(title)), 0);
// Running needs a render manager; not available in this binary
oaktask_task_free(t);
oaknode_colormanager_free(cm);
oaknode_project_free(project);
EXPECT_EQ(oaktask_debug_alive_count(), 0);
}