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