From 40a336276f5077f9fee9fb7452bfbe1476933320 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Fri, 7 Aug 2026 14:40:12 +0800 Subject: [PATCH] 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 --- include/codec/encoder.h | 31 + include/node/node.h | 45 ++ include/node/project.h | 6 + include/render/cache.h | 14 + include/render/copier.h | 65 +++ include/render/ticket.h | 156 +++++ include/task/project.h | 12 + src/codec/c_api/encoder.cpp | 41 ++ src/node/c_api/node.cpp | 102 ++++ src/node/c_api/project.cpp | 15 + src/render/c_api/CMakeLists.txt | 2 + src/render/c_api/cache.cpp | 34 ++ src/render/c_api/copier.cpp | 85 +++ src/render/c_api/ticket.cpp | 350 +++++++++++ src/render/tests/CMakeLists.txt | 1 + src/render/tests/ticket_test.cpp | 57 ++ src/task/c_api/project.cpp | 29 + src/task/src/export/export.cpp | 547 ++++++++++++++++++ src/task/src/export/export.h | 96 +++ src/task/src/precache/precachetask.cpp | 202 +++++++ .../src/precache/precachetask.cpp.pending | 113 ---- ...{precachetask.h.pending => precachetask.h} | 22 +- .../src/project/load/loadbasetask.cpp.pending | 34 -- .../src/project/load/loadbasetask.h.pending | 63 -- src/task/src/render/render.cpp | 409 +++++++++++++ src/task/src/render/render.cpp.pending | 352 ----------- src/task/src/render/render.h | 189 ++++++ src/task/src/render/render.h.pending | 154 ----- src/task/tests/task_test.cpp | 48 ++ 29 files changed, 2548 insertions(+), 726 deletions(-) create mode 100644 include/render/copier.h create mode 100644 include/render/ticket.h create mode 100644 src/render/c_api/copier.cpp create mode 100644 src/render/c_api/ticket.cpp create mode 100644 src/render/tests/ticket_test.cpp create mode 100644 src/task/src/export/export.cpp create mode 100644 src/task/src/export/export.h create mode 100644 src/task/src/precache/precachetask.cpp delete mode 100644 src/task/src/precache/precachetask.cpp.pending rename src/task/src/precache/{precachetask.h.pending => precachetask.h} (73%) delete mode 100644 src/task/src/project/load/loadbasetask.cpp.pending delete mode 100644 src/task/src/project/load/loadbasetask.h.pending create mode 100644 src/task/src/render/render.cpp delete mode 100644 src/task/src/render/render.cpp.pending create mode 100644 src/task/src/render/render.h delete mode 100644 src/task/src/render/render.h.pending diff --git a/include/codec/encoder.h b/include/codec/encoder.h index cd5403b20..8606541c1 100644 --- a/include/codec/encoder.h +++ b/include/codec/encoder.h @@ -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 diff --git a/include/node/node.h b/include/node/node.h index 7a64f8c65..d94b30afd 100644 --- a/include/node/node.h +++ b/include/node/node.h @@ -23,6 +23,7 @@ #include +#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 diff --git a/include/node/project.h b/include/node/project.h index ff82813a6..73c11143c 100644 --- a/include/node/project.h +++ b/include/node/project.h @@ -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, diff --git a/include/render/cache.h b/include/render/cache.h index 655a2f3ef..eb33bf49f 100644 --- a/include/render/cache.h +++ b/include/render/cache.h @@ -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)). diff --git a/include/render/copier.h b/include/render/copier.h new file mode 100644 index 000000000..947a13011 --- /dev/null +++ b/include/render/copier.h @@ -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 . + +***/ + +#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 diff --git a/include/render/ticket.h b/include/render/ticket.h new file mode 100644 index 000000000..24271b1e7 --- /dev/null +++ b/include/render/ticket.h @@ -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 . + +***/ + +#ifndef OAK_EDITOR_RENDER_TICKET_H +#define OAK_EDITOR_RENDER_TICKET_H + +#include + +#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 diff --git a/include/task/project.h b/include/task/project.h index 6993c56d6..a370cf0bb 100644 --- a/include/task/project.h +++ b/include/task/project.h @@ -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). diff --git a/src/codec/c_api/encoder.cpp b/src/codec/c_api/encoder.cpp index e81a708f6..4a2314ca9 100644 --- a/src/codec/c_api/encoder.cpp +++ b/src/codec/c_api/encoder.cpp @@ -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(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 matrix = olive::EncodingParams::generate_matrix( + static_cast(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; +} diff --git a/src/node/c_api/node.cpp b/src/node/c_api/node.cpp index 07e5f726e..176b5d956 100644 --- a/src/node/c_api/node.cpp +++ b/src/node/c_api/node.cpp @@ -20,6 +20,9 @@ #include "node/node.h" +#include "common/videoparams.h" +#include "olive/core/oakcore/audioparams.h" + #include #include @@ -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( + 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( + 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(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(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; + } +} diff --git a/src/node/c_api/project.cpp b/src/node/c_api/project.cpp index 7b8f802da..3d73d42b1 100644 --- a/src/node/c_api/project.cpp +++ b/src/node/c_api/project.cpp @@ -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(to_cpp(src)), to_cpp(dst)); + return OAKNODE_OK; + } catch (...) { + return OAKNODE_E_FAILED; + } +} diff --git a/src/render/c_api/CMakeLists.txt b/src/render/c_api/CMakeLists.txt index 182e9a97c..c7b14aa9b 100644 --- a/src/render/c_api/CMakeLists.txt +++ b/src/render/c_api/CMakeLists.txt @@ -2,6 +2,8 @@ target_sources(oakrender PRIVATE renderer.cpp cache.cpp cancelatom.cpp + ticket.cpp + copier.cpp color.cpp manager.cpp ) diff --git a/src/render/c_api/cache.cpp b/src/render/c_api/cache.cpp index 4c8927adb..69c0fc461 100644 --- a/src/render/c_api/cache.cpp +++ b/src/render/c_api/cache.cpp @@ -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; + } +} diff --git a/src/render/c_api/copier.cpp b/src/render/c_api/copier.cpp new file mode 100644 index 000000000..e41754269 --- /dev/null +++ b/src/render/c_api/copier.cpp @@ -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 . + +***/ + +#include "render/copier.h" + +#include + +#include "../src/projectcopier.h" + +namespace +{ + +olive::ProjectCopier *impl(OakRenderProjectCopier *h) +{ + return reinterpret_cast(h); +} + +} // namespace + +OakRenderProjectCopier *oakrender_project_copier_create(void) +{ + try { + return reinterpret_cast( + 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(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( + impl(copier)->get_copy(reinterpret_cast(original))); +} + +OakNodeProject *oakrender_project_copier_get_copied_project( + OakRenderProjectCopier *copier) +{ + if (!copier) { + return NULL; + } + return reinterpret_cast( + impl(copier)->get_copied_project()); +} diff --git a/src/render/c_api/ticket.cpp b/src/render/c_api/ticket.cpp new file mode 100644 index 000000000..35f777786 --- /dev/null +++ b/src/render/c_api/ticket.cpp @@ -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 . + +***/ + +#include "render/ticket.h" + +#include + +#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(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(h); +} + +olive::Node *to_node(OakNodeNode *n) +{ + return reinterpret_cast(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(params->color_manager), + static_cast(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( + 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( + 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(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()) { + return OAKRENDER_E_FAILED; + } + + olive::FramePtr frame = result.value(); + 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()) { + return OAKRENDER_E_FAILED; + } + + olive::SampleBuffer samples = result.value(); + *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; +} diff --git a/src/render/tests/CMakeLists.txt b/src/render/tests/CMakeLists.txt index 45dd9418f..69adc5485 100644 --- a/src/render/tests/CMakeLists.txt +++ b/src/render/tests/CMakeLists.txt @@ -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 diff --git a/src/render/tests/ticket_test.cpp b/src/render/tests/ticket_test.cpp new file mode 100644 index 000000000..c92985ccc --- /dev/null +++ b/src/render/tests/ticket_test.cpp @@ -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 . + +***/ + +#include + +#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(¶ms, 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); +} diff --git a/src/task/c_api/project.cpp b/src/task/c_api/project.cpp index c4effede4..56862ef91 100644 --- a/src/task/c_api/project.cpp +++ b/src/task/c_api/project.cpp @@ -22,6 +22,8 @@ #include +#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; + } +} diff --git a/src/task/src/export/export.cpp b/src/task/src/export/export.cpp new file mode 100644 index 000000000..275e678a2 --- /dev/null +++ b/src/task/src/export/export.cpp @@ -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 . + +***/ + +#include "export.h" + +#include +#include + +#include + +#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(oakrender_codec_frame_const_data(render_frame)); + uint8_t *dst = static_cast(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 ¶ms) + : 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(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(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(¶ms_); + 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(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 interleaved(sample_count * (size_t)channels); + // SampleBuffer is planar; interleave for the encoder + std::vector planar((size_t)channels); + oakcore_samplebuffer_to_raw_ptrs(samples, + const_cast(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; +} + +} diff --git a/src/task/src/export/export.h b/src/task/src/export/export.h new file mode 100644 index 000000000..5d500b08b --- /dev/null +++ b/src/task/src/export/export.h @@ -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 . + +***/ + +#ifndef OAK_EXPORTTASK_H +#define OAK_EXPORTTASK_H + +#include + +#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 ¶ms); + + 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 time_map_; + + struct TimeRangeLess { + bool operator()(const TimeRange &a, const TimeRange &b) const + { + return a.in() < b.in(); + } + }; + std::map 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 diff --git a/src/task/src/precache/precachetask.cpp b/src/task/src/precache/precachetask.cpp new file mode 100644 index 000000000..717d4471c --- /dev/null +++ b/src/task/src/precache/precachetask.cpp @@ -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 . + +***/ + +#include "precachetask.h" + +#include + +#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( + oaknode_block_from_node(footage_copy)); + if (!footage_) { + footage_ = reinterpret_cast(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(cache), + intersection.in().numerator(), intersection.in().denominator(), + intersection.out().numerator(), intersection.out().denominator(), + nullptr, 0); + + TimeRangeList video_range; + if (range_count > 0) { + std::vector flat(size_t(range_count) * 4); + oakrender_cache_get_invalidated_ranges( + reinterpret_cast(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; +} + +} diff --git a/src/task/src/precache/precachetask.cpp.pending b/src/task/src/precache/precachetask.cpp.pending deleted file mode 100644 index 42241e432..000000000 --- a/src/task/src/precache/precachetask.cpp.pending +++ /dev/null @@ -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 . - -***/ - -#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->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; -} - -} diff --git a/src/task/src/precache/precachetask.h.pending b/src/task/src/precache/precachetask.h similarity index 73% rename from src/task/src/precache/precachetask.h.pending rename to src/task/src/precache/precachetask.h index d082e20bf..424fd2cfd 100644 --- a/src/task/src/precache/precachetask.h.pending +++ b/src/task/src/precache/precachetask.h @@ -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 ×) 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_; }; } diff --git a/src/task/src/project/load/loadbasetask.cpp.pending b/src/task/src/project/load/loadbasetask.cpp.pending deleted file mode 100644 index c8a54b3b5..000000000 --- a/src/task/src/project/load/loadbasetask.cpp.pending +++ /dev/null @@ -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 . - -***/ - -#include "loadbasetask.h" - -namespace olive -{ - -ProjectLoadBaseTask::ProjectLoadBaseTask(const QString &filename) - : project_(nullptr) - , filename_(filename) -{ - set_title(tr("Loading '%1'").arg(filename)); -} - -} diff --git a/src/task/src/project/load/loadbasetask.h.pending b/src/task/src/project/load/loadbasetask.h.pending deleted file mode 100644 index 411093546..000000000 --- a/src/task/src/project/load/loadbasetask.h.pending +++ /dev/null @@ -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 . - -***/ - -#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 diff --git a/src/task/src/render/render.cpp b/src/task/src/render/render.cpp new file mode 100644 index 000000000..f9b51b290 --- /dev/null +++ b/src/task/src/render/render.cpp @@ -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 . + +***/ + +#include "render.h" + +#include + +#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( + ¶ms, + [](OakRenderTicket *t, void *userdata) { + static_cast(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(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 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(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 block_indexes(size_t(track_count), 0); + std::vector 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 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; +} + +} diff --git a/src/task/src/render/render.cpp.pending b/src/task/src/render/render.cpp.pending deleted file mode 100644 index 4028f5fe4..000000000 --- a/src/task/src/render/render.cpp.pending +++ /dev/null @@ -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 . - -***/ - -#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(viewer_)) { - TrackList *list = sequence->track_list(Track::k_subtitle); - QVector block_indexes(list->get_track_count(), 0); - - QVector 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(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(); - - if (ticket_type == RenderManager::k_type_audio) { - TimeRange range = watcher->property("range").value(); - - if (!audio_downloaded(range, - watcher->get().value())) { - 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(), - watcher->property("time").value())) { - 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(), - watcher->property("time").value())) { - 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(); -} - -} diff --git a/src/task/src/render/render.h b/src/task/src/render/render.h new file mode 100644 index 000000000..f3ce6083b --- /dev/null +++ b/src/task/src/render/render.h @@ -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 . + +***/ + +#ifndef OAK_RENDERTASK_H +#define OAK_RENDERTASK_H + +#include +#include +#include +#include + +#include + +#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 finished_tickets_; + std::vector running_ticket_list_; + int running_tickets_; + + bool native_progress_signalling_; + + int64_t total_number_of_frames_; +}; + +} + +#endif // OAK_RENDERTASK_H diff --git a/src/task/src/render/render.h.pending b/src/task/src/render/render.h.pending deleted file mode 100644 index f16204eec..000000000 --- a/src/task/src/render/render.h.pending +++ /dev/null @@ -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 . - -***/ - -#ifndef OAK_RENDERTASK_H -#define OAK_RENDERTASK_H - -#include - -#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 running_watchers_; - std::list 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 diff --git a/src/task/tests/task_test.cpp b/src/task/tests/task_test.cpp index 43ec8a493..c9f5b3e73 100644 --- a/src/task/tests/task_test.cpp +++ b/src/task/tests/task_test.cpp @@ -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, ¶ms), 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, ¶ms); + 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); +}