refactor(task): de-Qt oaktask base + project tasks, wire codec task submitter

- Task base: Qt signals become lifecycle listeners (the async-command
  callback exception), cancellation uses oakrender's OakCancelAtom C
  handle; TaskManager runs std::thread workers, no signals
- ConformTask/ProxyTask implement oakcodec's submit-callback contract
  (synchronous interim); register_codec_task_submitter() closes the
  conform/proxy loop - conform of demo.mp4 produces pcm caches in tests
- ProjectImportTask/ProjectLoadTask/ProjectSaveTask over the oaknode
  C ABI; image-sequence confirmation becomes a facade callback
  (default: not a sequence)
- C API additions needed by oaktask: oaknode serializer file-level
  save/load (auto-initializing), footage video-params/cancel-atom/
  as-node, folder add-child command factory; oakcodec decoder
  conform_audio + image-sequence helpers; oakrender cancelatom
  get_native
- fix double-ownership: submitting a task to the manager transfers
  ownership, freeing its handle only releases the wrapper
- C ABI in include/task (task/manager/project, OakTaskTask opaque
  handle + lifecycle subscribe), every function tested (103 cases in
  build-oaktask incl. regressions)
- RenderTask family and OTIO tasks follow in separate commits
This commit is contained in:
2026-08-07 14:01:23 +08:00
parent 66831a870e
commit 6ded1d2d63
54 changed files with 5937 additions and 1 deletions
+33
View File
@@ -25,6 +25,7 @@
#include "error.h"
#include "frame.h"
#include "render/cancelatom.h"
#ifdef __cplusplus
extern "C" {
@@ -199,6 +200,38 @@ OAKCODEC_API int oakcodec_decoder_decode_audio(OakDecoder decoder, int in_num, i
uint64_t channel_layout, float *buf,
int buf_frames);
/**
* @brief Conform the open stream's audio into per-channel pcm cache files
* (Decoder::conform_audio()).
*
* `output_filenames` is an array of `filename_count` final per-channel
* paths. `sample_format` is olive::core::SampleFormat::Format as int.
* `cancelled` may be an empty OakCancelAtom (ctx == NULL).
*
* @return OAKCODEC_OK on success, OAKCODEC_E_STATE when no stream is
* open, OAKCODEC_E_CANCELLED when cancelled, OAKCODEC_E_FAILED
* otherwise.
*/
OAKCODEC_API int oakcodec_decoder_conform_audio(OakDecoder decoder,
const char *const *output_filenames, int filename_count,
int sample_rate, uint64_t channel_layout, int sample_format,
OakCancelAtom cancelled);
/**
* @brief Image-sequence filename heuristics (Decoder::get_image_sequence_*).
*
* digit_count: number of trailing digits in the filename stem (0 = not an
* image sequence filename). index: the numeric value of those digits (-1
* when none). transform: substitute `number` into the digit field,
* two-stage string getter.
*/
OAKCODEC_API int oakcodec_decoder_get_image_sequence_digit_count(
const char *filename);
OAKCODEC_API int64_t oakcodec_decoder_get_image_sequence_index(
const char *filename);
OAKCODEC_API int oakcodec_decoder_transform_image_sequence_file_name(
const char *filename, int64_t number, char *buf, int buf_size);
/**
* @brief Human-readable detail of the last error on this decoder
* (buf/size string getter convention).
+14
View File
@@ -22,6 +22,7 @@
#define OAK_EDITOR_NODE_FOLDER_H
#include "node/error.h"
#include "undo/undocommand.h"
#include "node/project.h"
#ifdef __cplusplus
@@ -73,6 +74,19 @@ OakNodeNode *oaknode_folder_child_at(const OakNodeFolder *folder, int index);
*/
int oaknode_folder_add_child(OakNodeFolder *folder, OakNodeNode *child);
/**
* @brief Borrowed cast from a folder handle to its node handle.
* NULL for NULL.
*/
OakNodeNode *oaknode_folder_as_node(OakNodeFolder *folder);
/**
* @brief Create an undoable FolderAddChild command. Owned; free with
* oakundo_command_free().
*/
OakUndoCommand *oaknode_command_create_folder_add_child(
OakNodeFolder *folder, OakNodeNode *child);
/**
* @brief Remove `child` from `folder` without deleting it (live,
* non-undoable; executes Folder::RemoveElementCommand::redo()).
+41
View File
@@ -23,7 +23,12 @@
#include <stdint.h>
#include "common/videoparams.h"
#include "node/error.h"
// NOTE: quoted-relative to bypass the "render/cancelatom.h" transition
// bridge (oakrender's C++ olive::CancelAtom) that shadows the C ABI
// header on oaknode's include path.
#include "../../include/render/cancelatom.h"
#include "node/project.h"
#ifdef __cplusplus
@@ -60,6 +65,12 @@ typedef struct OakNodeFootage OakNodeFootage;
OakNodeFootage *oaknode_footage_create(OakNodeProject *project,
const char *filename);
/**
* @brief Borrowed cast from a footage handle to its node handle.
* NULL for NULL.
*/
OakNodeNode *oaknode_footage_as_node(OakNodeFootage *footage);
/**
* @brief Current media path (Footage::filename()). Two-stage string getter.
*
@@ -192,6 +203,36 @@ int oaknode_footage_set_proxy(OakNodeFootage *footage, const char *path,
*/
int oaknode_footage_clear_proxy(OakNodeFootage *footage);
/**
* @brief Video stream parameters as an oakcommon video-params handle
* (ViewerOutput::get_video_params()). `out` receives a handle with
* reference count 1 (release with oakcommon_videoparams_free()).
* OAKNODE_E_NOT_FOUND for an out-of-range index.
*/
int oaknode_footage_get_video_params(OakNodeFootage *footage, int index,
OakVideoParams *out);
/**
* @brief Set a video stream's parameters from an oakcommon handle
* (ViewerOutput::set_video_params()).
*/
int oaknode_footage_set_video_params(OakNodeFootage *footage, int index,
const OakVideoParams *params);
/**
* @brief Video length as a rational pair (ViewerOutput::get_video_length()).
*/
int oaknode_footage_get_video_length(OakNodeFootage *footage,
int64_t *out_num, int64_t *out_den);
/**
* @brief Set the footage's cancellation atom used during probing
* (Footage::set_cancel_pointer()). `atom` may be an empty OakCancelAtom
* (ctx == NULL) to clear.
*/
int oaknode_footage_set_cancel_atom(OakNodeFootage *footage,
OakCancelAtom atom);
#ifdef __cplusplus
}
#endif
+41
View File
@@ -242,3 +242,44 @@ int oaknode_serializer_loaddata_connection_at(
#endif
#endif //OAK_EDITOR_NODE_SERIALIZER_H
/**
* @brief Result codes for file-level save/load (mirror
* ProjectSerializer::ResultCode; pinned by test).
*/
enum OakNodeSerializerResultCode {
OAKNODE_SERIALIZER_RESULT_SUCCESS = 0,
OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_OLD = 1,
OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_NEW = 2,
OAKNODE_SERIALIZER_RESULT_UNKNOWN_VERSION = 3,
OAKNODE_SERIALIZER_RESULT_FILE_ERROR = 4,
OAKNODE_SERIALIZER_RESULT_XML_ERROR = 5,
OAKNODE_SERIALIZER_RESULT_OVERWRITE_ERROR = 6,
OAKNODE_SERIALIZER_RESULT_NO_DATA = 7
};
/**
* @brief Save a project to a file (ProjectSerializer::save(), project
* type, optional OVEC compression). Layout data is not serialized
* through this API (app-layer concern, see oakstorage/M10).
*
* @param out_code Receives an OakNodeSerializerResultCode (may be NULL).
* @param details Optional two-stage buffer for the result details
* string (e.g. the fallback filename on overwrite errors).
* @return OAKNODE_OK when the result code is
* OAKNODE_SERIALIZER_RESULT_SUCCESS, OAKNODE_E_FAILED otherwise
* (details in out_code/details), OAKNODE_E_INVALID for NULL args.
*/
int oaknode_serializer_save_to_file(OakNodeProject *project,
const char *filename, int use_compression, int *out_code,
char *details, int details_size);
/**
* @brief Load a project from a file into `project`
* (ProjectSerializer::load(), project type).
*
* Same return/out-param convention as oaknode_serializer_save_to_file().
*/
int oaknode_serializer_load_from_file(OakNodeProject *project,
const char *filename, int *out_code, char *details,
int details_size);
+15 -1
View File
@@ -29,6 +29,14 @@
extern "C" {
#endif
#ifdef __cplusplus
} /* extern "C" */
namespace olive { class CancelAtom; }
extern "C" {
#endif
/**
* @brief Current ABI version stamped into every oakrender handle.
*
@@ -108,8 +116,14 @@ int oakrender_cancelatom_is_cancelled(OakCancelAtom atom, int *cancelled);
* NULL out parameter.
*/
int oakrender_cancelatom_heard_cancel(OakCancelAtom atom, int *heard);
#ifdef __cplusplus
/**
* @brief Borrowed access to the underlying C++ atom (C++ only, for
* adapter layers). Valid while the handle is held. NULL-safe.
*/
olive::CancelAtom *oakrender_cancelatom_get_native(OakCancelAtom atom);
}
#endif
+40
View File
@@ -0,0 +1,40 @@
/***
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_TASK_ERROR_H
#define OAK_EDITOR_TASK_ERROR_H
/**
* @brief Status and error codes shared by all oaktask C API families.
*
* Return-code convention (mirrors engine/include/oakengine/init.h):
* 0 (OAKTASK_OK) on success, a negative OAKTASK_E_* error code on
* failure. String getters return the required buffer size in bytes
* (including the terminating NUL) as a non-negative value instead.
*/
#define OAKTASK_OK 0 /**< Success. */
#define OAKTASK_E_INVALID (-1) /**< NULL handle or invalid argument. */
#define OAKTASK_E_STATE (-2) /**< Call not valid in the current state. */
#define OAKTASK_E_FAILED (-3) /**< The underlying operation failed. */
#define OAKTASK_E_NOT_FOUND (-4) /**< Index out of range / entry not found. */
#define OAKTASK_E_NOMEM (-5) /**< Allocation failed. */
#define OAKTASK_E_CANCELLED (-6) /**< The operation was cancelled. */
#endif //OAK_EDITOR_TASK_ERROR_H
+54
View File
@@ -0,0 +1,54 @@
/***
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_TASK_MANAGER_H
#define OAK_EDITOR_TASK_MANAGER_H
#include "task/task.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Task manager singleton lifecycle.
*/
int oaktask_manager_init(void);
void oaktask_manager_shutdown(void);
/**
* @brief Register oaktask as oakcodec's background task submitter
* (olive::register_codec_task_submitter()). Called by
* oaktask_manager_init(); exposed for manual control.
*/
int oaktask_register_codec_submitter(void);
int oaktask_manager_count(void);
/** @brief Borrowed task at index, or NULL when out of range. */
OakTaskTask *oaktask_manager_at(int i);
void oaktask_manager_delete_finished(void);
#ifdef __cplusplus
}
#endif
#endif //OAK_EDITOR_TASK_MANAGER_H
+85
View File
@@ -0,0 +1,85 @@
/***
Oak Video Editor - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#ifndef OAK_EDITOR_TASK_PROJECT_H
#define OAK_EDITOR_TASK_PROJECT_H
#include "node/footage.h"
#include "node/node.h"
#include "node/project.h"
#include "task/task.h"
#include "undo/undocommand.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Project task factories and result accessors (M8 §2.2).
*/
/** @brief olive::ProjectLoadTask. */
OakTaskTask *oaktask_create_project_load(const char *filename);
/** @brief Take the loaded project (ownership transfer). */
OakNodeProject *oaktask_load_take_project(OakTaskTask *t);
/** @brief olive::ProjectSaveTask. `filename_or_NULL` overrides the
* project's own filename. */
OakTaskTask *oaktask_create_project_save(OakNodeProject *project,
const char *filename_or_NULL,
int use_compression);
/** @brief olive::ProjectImportTask. */
OakTaskTask *oaktask_create_project_import(OakNodeFolder *folder,
OakNodeProject *project,
const char *const *urls,
int url_count);
/** @brief Take the import's undo command (ownership transfer). */
OakUndoCommand *oaktask_import_take_command(OakTaskTask *t);
int oaktask_import_footage_count(OakTaskTask *t);
/** @brief Borrowed footage handle at index, NULL when out of range. */
OakNodeFootage *oaktask_import_footage_at(OakTaskTask *t, int index);
int oaktask_import_invalid_count(OakTaskTask *t);
/** @brief Invalid filename at index (two-stage). */
int oaktask_import_invalid_at(OakTaskTask *t, int index, char *buf,
int buf_size);
/**
* @brief Image-sequence confirmation callback (facade/UI concern;
* olive::ProjectImportTask::set_image_sequence_confirm_callback).
* Return non-zero to treat numbered stills as a sequence.
* Default (no callback): not a sequence.
*/
typedef int (*oaktask_image_sequence_confirm_fn)(const char *filename,
void *userdata);
void oaktask_import_set_image_sequence_confirm_cb(
oaktask_image_sequence_confirm_fn fn, void *userdata);
#ifdef __cplusplus
}
#endif
#endif //OAK_EDITOR_TASK_PROJECT_H
+97
View File
@@ -0,0 +1,97 @@
/***
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_TASK_TASK_H
#define OAK_EDITOR_TASK_TASK_H
#include <stdint.h>
#include "task/error.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Opaque handle to a background task (olive::Task).
*
* Tasks are created through the factories in task/project.h (and future
* family headers) and must be released with oaktask_task_free().
*/
typedef struct OakTaskTask OakTaskTask;
/** @brief Lifecycle event ids for oaktask_task_subscribe(). */
enum OakTaskEvent {
OAKTASK_EVENT_STARTED = 0,
OAKTASK_EVENT_PROGRESS = 1,
OAKTASK_EVENT_FINISHED = 2
};
/**
* @brief Event callback (async command return channel, 01 §4 exception).
*
* For OAKTASK_EVENT_FINISHED, `value` is 1.0 on success / 0.0 on failure;
* for OAKTASK_EVENT_PROGRESS it is 0..1; for OAKTASK_EVENT_STARTED it is
* the start time in milliseconds.
*/
typedef void (*oaktask_event_fn)(int event_id, double value,
void *userdata);
/**
* @brief Free a task. No-op on NULL. The task must not be running on the
* manager (oaktask_task_cancel + wait first if it is).
*/
void oaktask_task_free(OakTaskTask *t);
/** @brief Run synchronously in the calling thread. 1 = succeeded. */
int oaktask_task_start_sync(OakTaskTask *t);
/** @brief Run asynchronously on the task manager. */
int oaktask_task_start(OakTaskTask *t);
int oaktask_task_cancel(OakTaskTask *t);
/** @brief Wait for an asynchronously started task. */
int oaktask_task_wait(OakTaskTask *t);
int oaktask_task_is_finished(const OakTaskTask *t);
int oaktask_task_succeeded(const OakTaskTask *t);
/** @brief Two-stage string getters. */
int oaktask_task_title(OakTaskTask *t, char *buf, int buf_size);
int oaktask_task_error(OakTaskTask *t, char *buf, int buf_size);
/**
* @brief Subscribe to lifecycle events (returns a subscription id >= 0,
* or a negative error code). One-shot per event stream: the
* subscription is dropped after OAKTASK_EVENT_FINISHED.
*/
int64_t oaktask_task_subscribe(OakTaskTask *t, oaktask_event_fn fn,
void *userdata);
/** @brief Alive-count for leak assertions in tests. */
int oaktask_debug_alive_count(void);
#ifdef __cplusplus
}
#endif
#endif //OAK_EDITOR_TASK_TASK_H
+1
View File
@@ -5,3 +5,4 @@ add_subdirectory(render)
add_subdirectory(codec)
add_subdirectory(audio)
add_subdirectory(timeline)
add_subdirectory(task)
+69
View File
@@ -27,6 +27,7 @@
#include <sys/stat.h>
#include "common/loopmode.h"
#include "render/cancelatom.h"
#include "decoder.h"
#include "footagedescription.h"
#include "frame.h"
@@ -386,3 +387,71 @@ int oakcodec_decoder_last_error(OakDecoder decoder, char *buf, int buf_size)
return string_out("", buf, buf_size);
return string_out(b->last_error, buf, buf_size);
}
int oakcodec_decoder_conform_audio(OakDecoder decoder,
const char *const *output_filenames, int filename_count,
int sample_rate, uint64_t channel_layout, int sample_format,
OakCancelAtom cancelled)
{
DecoderBox *b = decoder_box(decoder.ctx);
if (!b || (!output_filenames && filename_count > 0) ||
filename_count < 0)
return OAKCODEC_E_INVALID;
if (!b->open || !b->decoder)
return OAKCODEC_E_STATE;
try {
std::vector<std::string> filenames;
filenames.reserve(size_t(filename_count));
for (int i = 0; i < filename_count; i++) {
if (!output_filenames[i])
return OAKCODEC_E_INVALID;
filenames.emplace_back(output_filenames[i]);
}
olive::AudioParams params(
sample_rate, channel_layout,
static_cast<olive::core::SampleFormat::Format>(sample_format));
bool ret = b->decoder->conform_audio(filenames, params,
cancelled.ctx ? &cancelled
: nullptr);
if (!ret) {
int heard = 0;
if (cancelled.ctx &&
oakrender_cancelatom_heard_cancel(cancelled, &heard) ==
OAKRENDER_OK &&
heard) {
return OAKCODEC_E_CANCELLED;
}
return OAKCODEC_E_FAILED;
}
return OAKCODEC_OK;
} catch (...) {
return OAKCODEC_E_FAILED;
}
}
int oakcodec_decoder_get_image_sequence_digit_count(const char *filename)
{
if (!filename)
return OAKCODEC_E_INVALID;
return olive::Decoder::get_image_sequence_digit_count(filename);
}
int64_t oakcodec_decoder_get_image_sequence_index(const char *filename)
{
if (!filename)
return OAKCODEC_E_INVALID;
return olive::Decoder::get_image_sequence_index(filename);
}
int oakcodec_decoder_transform_image_sequence_file_name(
const char *filename, int64_t number, char *buf, int buf_size)
{
if (!filename)
return OAKCODEC_E_INVALID;
return string_out(
olive::Decoder::transform_image_sequence_file_name(filename, number),
buf, buf_size);
}
+29
View File
@@ -20,6 +20,8 @@
#include "node/folder.h"
#include "../../undo/c_api/commandhandle.h"
#include <new>
#include "../src/project.h"
@@ -230,3 +232,30 @@ OakNodeFolder *oaknode_folder_parent_of(const OakNodeNode *node)
return NULL;
}
}
OakNodeNode *oaknode_folder_as_node(OakNodeFolder *folder)
{
return reinterpret_cast<OakNodeNode *>(folder);
}
OakUndoCommand *oaknode_command_create_folder_add_child(
OakNodeFolder *folder, OakNodeNode *child)
{
if (!folder || !child) {
return NULL;
}
try {
OakUndoCommand *handle =
new (std::nothrow) OakUndoCommand{ nullptr, true };
if (!handle) {
return NULL;
}
handle->command = new olive::FolderAddChild(
reinterpret_cast<olive::Folder *>(folder),
reinterpret_cast<olive::Node *>(child));
return handle;
} catch (...) {
return NULL;
}
}
+78
View File
@@ -20,6 +20,9 @@
#include "node/footage.h"
#include "common/videoparams.h"
#include "../../../include/render/cancelatom.h"
#include <cstring>
#include <new>
#include <string>
@@ -320,3 +323,78 @@ int oaknode_footage_clear_proxy(OakNodeFootage *footage)
return OAKNODE_E_FAILED;
}
}
int oaknode_footage_get_video_params(OakNodeFootage *footage, int index,
OakVideoParams *out)
{
olive::Footage *f = to_cpp(footage);
if (!f || !out || index < 0) {
return OAKNODE_E_INVALID;
}
try {
olive::VideoParams vp = f->get_video_params(index);
if (!vp.is_valid() && index >= f->input_array_size(
olive::ViewerOutput::k_video_params_input)) {
return OAKNODE_E_NOT_FOUND;
}
*out = oakcommon_videoparams_init_from_native(&vp);
return out->ctx ? OAKNODE_OK : OAKNODE_E_NOMEM;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
int oaknode_footage_set_video_params(OakNodeFootage *footage, int index,
const OakVideoParams *params)
{
olive::Footage *f = to_cpp(footage);
if (!f || !params || !params->ctx) {
return OAKNODE_E_INVALID;
}
const olive::VideoParams *native =
oakcommon_videoparams_get_native(*params);
if (!native) {
return OAKNODE_E_INVALID;
}
try {
f->set_video_params(*native, index);
return OAKNODE_OK;
} catch (...) {
return OAKNODE_E_FAILED;
}
}
int oaknode_footage_get_video_length(OakNodeFootage *footage,
int64_t *out_num, int64_t *out_den)
{
olive::Footage *f = to_cpp(footage);
if (!f || !out_num || !out_den) {
return OAKNODE_E_INVALID;
}
olive::core::Rational len = f->get_video_length();
*out_num = len.numerator();
*out_den = len.denominator();
return OAKNODE_OK;
}
int oaknode_footage_set_cancel_atom(OakNodeFootage *footage,
OakCancelAtom atom)
{
olive::Footage *f = to_cpp(footage);
if (!f) {
return OAKNODE_E_INVALID;
}
f->set_cancel_pointer(
atom.ctx ? oakrender_cancelatom_get_native(atom) : nullptr);
return OAKNODE_OK;
}
OakNodeNode *oaknode_footage_as_node(OakNodeFootage *footage)
{
return reinterpret_cast<OakNodeNode *>(footage);
}
+75
View File
@@ -355,3 +355,78 @@ int oaknode_serializer_loaddata_connection_at(
return OAKNODE_E_FAILED;
}
}
namespace
{
int report_serializer_result(const olive::ProjectSerializer::Result &result,
int *out_code, char *details, int details_size)
{
if (out_code) {
*out_code = int(result.code());
}
int needed = 0;
if (details || details_size > 0) {
std::string d = result.get_details();
needed = int(d.size()) + 1;
if (details && details_size >= needed) {
memcpy(details, d.c_str(), needed);
}
}
if (result.code() == olive::ProjectSerializer::k_success) {
return OAKNODE_OK;
}
return (details && details_size > 0) ? needed : OAKNODE_E_FAILED;
}
} // namespace
int oaknode_serializer_save_to_file(OakNodeProject *project,
const char *filename, int use_compression, int *out_code,
char *details, int details_size)
{
if (!project || !filename) {
return OAKNODE_E_INVALID;
}
try {
oaknode_serializer_initialize();
olive::ProjectSerializer::SaveData data(
olive::ProjectSerializer::k_project,
reinterpret_cast<olive::Project *>(project), filename);
olive::ProjectSerializer::Result result =
olive::ProjectSerializer::save(data, use_compression != 0);
return report_serializer_result(result, out_code, details,
details_size);
} catch (...) {
return OAKNODE_E_FAILED;
}
}
int oaknode_serializer_load_from_file(OakNodeProject *project,
const char *filename, int *out_code, char *details,
int details_size)
{
if (!project || !filename) {
return OAKNODE_E_INVALID;
}
try {
oaknode_serializer_initialize();
olive::ProjectSerializer::Result result =
olive::ProjectSerializer::load(
reinterpret_cast<olive::Project *>(project), filename,
olive::ProjectSerializer::k_project);
return report_serializer_result(result, out_code, details,
details_size);
} catch (...) {
return OAKNODE_E_FAILED;
}
}
+8
View File
@@ -133,3 +133,11 @@ int oakrender_cancelatom_heard_cancel(OakCancelAtom atom, int *heard)
*heard = c->heard_cancel() ? 1 : 0;
return OAKRENDER_OK;
}
olive::CancelAtom *oakrender_cancelatom_get_native(OakCancelAtom atom)
{
if (!atom.ctx) {
return nullptr;
}
return &static_cast<CancelAtomBox *>(atom.ctx)->impl;
}
+6
View File
@@ -0,0 +1,6 @@
add_subdirectory(src)
add_subdirectory(c_api)
if(BUILD_TESTS)
add_subdirectory(tests)
endif()
+5
View File
@@ -0,0 +1,5 @@
target_sources(oaktask PRIVATE
task.cpp
manager.cpp
project.cpp
)
+73
View File
@@ -0,0 +1,73 @@
/***
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 "task/manager.h"
#include "../src/codecbridge.h"
#include "../src/taskmanager.h"
#include "taskhandle.h"
int oaktask_manager_init(void)
{
if (olive::TaskManager::instance()) {
return OAKTASK_E_STATE;
}
olive::TaskManager::create_instance();
olive::register_codec_task_submitter();
return OAKTASK_OK;
}
void oaktask_manager_shutdown(void)
{
olive::unregister_codec_task_submitter();
olive::TaskManager::destroy_instance();
}
int oaktask_register_codec_submitter(void)
{
olive::register_codec_task_submitter();
return OAKTASK_OK;
}
int oaktask_manager_count(void)
{
if (!olive::TaskManager::instance()) {
return OAKTASK_E_STATE;
}
return olive::TaskManager::instance()->get_task_count();
}
OakTaskTask *oaktask_manager_at(int i)
{
if (!olive::TaskManager::instance()) {
return NULL;
}
olive::Task *task = olive::TaskManager::instance()->get_task_at(i);
// Borrowed wrapper: freeing it does not delete the task.
return oaktask_capi::wrap_borrowed(task);
}
void oaktask_manager_delete_finished(void)
{
if (olive::TaskManager::instance()) {
olive::TaskManager::instance()->delete_finished();
}
}
+180
View File
@@ -0,0 +1,180 @@
/***
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 "task/project.h"
#include <vector>
#include "../src/project/import/import.h"
#include "../src/project/load/load.h"
#include "../src/project/save/save.h"
#include "taskhandle.h"
using namespace oaktask_capi;
namespace
{
olive::ProjectImportTask *import_impl(OakTaskTask *t)
{
if (!t) {
return nullptr;
}
return dynamic_cast<olive::ProjectImportTask *>(impl(t)->task);
}
olive::ProjectLoadBaseTask *load_impl(OakTaskTask *t)
{
if (!t) {
return nullptr;
}
return dynamic_cast<olive::ProjectLoadBaseTask *>(impl(t)->task);
}
} // namespace
OakTaskTask *oaktask_create_project_load(const char *filename)
{
if (!filename) {
return NULL;
}
try {
return wrap(new olive::ProjectLoadTask(filename));
} catch (...) {
return NULL;
}
}
OakNodeProject *oaktask_load_take_project(OakTaskTask *t)
{
olive::ProjectLoadBaseTask *task = load_impl(t);
if (!task) {
return NULL;
}
return task->take_project();
}
OakTaskTask *oaktask_create_project_save(OakNodeProject *project,
const char *filename_or_NULL,
int use_compression)
{
if (!project) {
return NULL;
}
try {
auto *task = new olive::ProjectSaveTask(project,
use_compression != 0);
if (filename_or_NULL) {
task->set_override_filename(filename_or_NULL);
}
return wrap(task);
} catch (...) {
return NULL;
}
}
OakTaskTask *oaktask_create_project_import(OakNodeFolder *folder,
OakNodeProject *project,
const char *const *urls,
int url_count)
{
if (!folder || !project || (!urls && url_count > 0) || url_count < 0) {
return NULL;
}
try {
std::vector<std::string> filenames;
filenames.reserve(size_t(url_count));
for (int i = 0; i < url_count; i++) {
if (!urls[i]) {
return NULL;
}
filenames.emplace_back(urls[i]);
}
return wrap(
new olive::ProjectImportTask(folder, project, filenames));
} catch (...) {
return NULL;
}
}
OakUndoCommand *oaktask_import_take_command(OakTaskTask *t)
{
olive::ProjectImportTask *task = import_impl(t);
if (!task) {
return NULL;
}
return task->take_command();
}
int oaktask_import_footage_count(OakTaskTask *t)
{
olive::ProjectImportTask *task = import_impl(t);
if (!task) {
return OAKTASK_E_INVALID;
}
return int(task->get_imported_footage().size());
}
OakNodeFootage *oaktask_import_footage_at(OakTaskTask *t, int index)
{
olive::ProjectImportTask *task = import_impl(t);
if (!task || index < 0 ||
index >= int(task->get_imported_footage().size())) {
return NULL;
}
return task->get_imported_footage()[size_t(index)];
}
int oaktask_import_invalid_count(OakTaskTask *t)
{
olive::ProjectImportTask *task = import_impl(t);
if (!task) {
return OAKTASK_E_INVALID;
}
return int(task->get_invalid_files().size());
}
int oaktask_import_invalid_at(OakTaskTask *t, int index, char *buf,
int buf_size)
{
olive::ProjectImportTask *task = import_impl(t);
if (!task) {
return OAKTASK_E_INVALID;
}
if (index < 0 || index >= int(task->get_invalid_files().size())) {
return OAKTASK_E_NOT_FOUND;
}
return copy_string(task->get_invalid_files()[size_t(index)], buf,
buf_size);
}
void oaktask_import_set_image_sequence_confirm_cb(
oaktask_image_sequence_confirm_fn fn, void *userdata)
{
if (!fn) {
olive::ProjectImportTask::set_image_sequence_confirm_callback(
nullptr);
return;
}
olive::ProjectImportTask::set_image_sequence_confirm_callback(
[fn, userdata](const std::string &filename) {
return fn(filename.c_str(), userdata) != 0;
});
}
+161
View File
@@ -0,0 +1,161 @@
/***
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 "task/task.h"
#include "../src/taskmanager.h"
#include "taskhandle.h"
using namespace oaktask_capi;
void oaktask_task_free(OakTaskTask *t)
{
if (!t) {
return;
}
TaskHandle *h = impl(t);
if (h->owns_task) {
delete h->task;
}
delete h;
alive()--;
}
int oaktask_task_start_sync(OakTaskTask *t)
{
if (!t) {
return OAKTASK_E_INVALID;
}
try {
return impl(t)->task->start() ? 1 : 0;
} catch (...) {
return OAKTASK_E_FAILED;
}
}
int oaktask_task_start(OakTaskTask *t)
{
if (!t) {
return OAKTASK_E_INVALID;
}
if (!olive::TaskManager::instance()) {
return OAKTASK_E_STATE;
}
TaskHandle *h = impl(t);
if (h->running_on_manager) {
return OAKTASK_E_STATE;
}
olive::TaskManager::instance()->add_task(h->task);
h->running_on_manager = true;
// Ownership transfers to the manager (deleted by delete_finished() /
// shutdown); freeing this handle later only releases the wrapper.
h->owns_task = false;
return OAKTASK_OK;
}
int oaktask_task_cancel(OakTaskTask *t)
{
if (!t) {
return OAKTASK_E_INVALID;
}
impl(t)->task->cancel();
return OAKTASK_OK;
}
int oaktask_task_wait(OakTaskTask *t)
{
if (!t) {
return OAKTASK_E_INVALID;
}
TaskHandle *h = impl(t);
if (h->running_on_manager && olive::TaskManager::instance()) {
olive::TaskManager::instance()->cancel_task_and_wait(h->task);
h->finished = true;
}
return OAKTASK_OK;
}
int oaktask_task_is_finished(const OakTaskTask *t)
{
if (!t) {
return OAKTASK_E_INVALID;
}
return impl(t)->finished.load() ? 1 : 0;
}
int oaktask_task_succeeded(const OakTaskTask *t)
{
if (!t) {
return OAKTASK_E_INVALID;
}
return impl(t)->succeeded.load() ? 1 : 0;
}
int oaktask_task_title(OakTaskTask *t, char *buf, int buf_size)
{
if (!t) {
return OAKTASK_E_INVALID;
}
return copy_string(impl(t)->task->get_title(), buf, buf_size);
}
int oaktask_task_error(OakTaskTask *t, char *buf, int buf_size)
{
if (!t) {
return OAKTASK_E_INVALID;
}
return copy_string(impl(t)->task->get_error(), buf, buf_size);
}
int64_t oaktask_task_subscribe(OakTaskTask *t, oaktask_event_fn fn,
void *userdata)
{
if (!t || !fn) {
return OAKTASK_E_INVALID;
}
try {
impl(t)->task->add_event_listener(
[fn, userdata](olive::Task::EventType type, double value) {
int event_id = OAKTASK_EVENT_PROGRESS;
switch (type) {
case olive::Task::k_event_started:
event_id = OAKTASK_EVENT_STARTED;
break;
case olive::Task::k_event_finished:
event_id = OAKTASK_EVENT_FINISHED;
break;
default:
break;
}
fn(event_id, value, userdata);
});
return 0;
} catch (...) {
return OAKTASK_E_FAILED;
}
}
int oaktask_debug_alive_count(void)
{
return alive().load();
}
+118
View File
@@ -0,0 +1,118 @@
/***
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_TASK_CAPI_TASKHANDLE_H
#define OAK_TASK_CAPI_TASKHANDLE_H
#include <atomic>
#include <cstring>
#include <new>
#include <string>
#include "../src/task.h"
/**
* @brief Internal handle layout shared by oaktask's c_api translation
* units
*/
namespace oaktask_capi
{
struct TaskHandle {
olive::Task *task;
bool owns_task; /**< false for borrowed wrappers (oaktask_manager_at). */
bool running_on_manager;
std::atomic<bool> finished;
std::atomic<bool> succeeded;
};
inline std::atomic<int> &alive()
{
static std::atomic<int> g_alive{ 0 };
return g_alive;
}
inline TaskHandle *impl(OakTaskTask *t)
{
return reinterpret_cast<TaskHandle *>(t);
}
inline const TaskHandle *impl(const OakTaskTask *t)
{
return reinterpret_cast<const TaskHandle *>(t);
}
inline OakTaskTask *wrap(olive::Task *task)
{
if (!task) {
return NULL;
}
TaskHandle *h = new (std::nothrow) TaskHandle{ task, true, false,
false, false };
if (!h) {
delete task;
return NULL;
}
alive()++;
std::atomic<bool> *finished = &h->finished;
std::atomic<bool> *succeeded = &h->succeeded;
task->add_event_listener(
[finished, succeeded](olive::Task::EventType type, double value) {
if (type == olive::Task::k_event_finished) {
succeeded->store(value != 0.0);
finished->store(true);
}
});
return reinterpret_cast<OakTaskTask *>(h);
}
/**
* @brief Wrap a manager-owned (borrowed) task. Freeing the handle does
* NOT delete the task.
*/
inline OakTaskTask *wrap_borrowed(olive::Task *task)
{
if (!task) {
return NULL;
}
TaskHandle *h = new (std::nothrow) TaskHandle{ task, false, true,
task->get_start_time() != 0,
false };
if (!h) {
return NULL;
}
alive()++;
return reinterpret_cast<OakTaskTask *>(h);
}
inline int copy_string(const std::string &value, char *buf, int buf_size)
{
int needed = int(value.size()) + 1;
if (buf && buf_size >= needed) {
memcpy(buf, value.c_str(), needed);
}
return needed;
}
} // namespace oaktask_capi
#endif // OAK_TASK_CAPI_TASKHANDLE_H
+37
View File
@@ -0,0 +1,37 @@
# 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/>.
file(GLOB_RECURSE OAKTASK_SOURCES CONFIGURE_DEPENDS *.cpp)
add_library(oaktask SHARED ${OAKTASK_SOURCES})
target_include_directories(oaktask PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${OAK_REPO_ROOT}/include
${OAK_REPO_ROOT}/src/common/src
${OAK_REPO_ROOT}/src/undo/src
${OAK_REPO_ROOT}/core/include
)
target_link_libraries(oaktask PUBLIC
oaknode
oakrender
oakcodec
oakundo
oakcommon
olivecore
)
+98
View File
@@ -0,0 +1,98 @@
/***
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 "codecbridge.h"
#include <memory>
#include "codec/task.h"
#include "common/config.h"
#include "conform/conform.h"
#include "proxy/proxy.h"
namespace olive
{
namespace
{
oakcodec_proxy_params proxy_params_from_config()
{
oakcodec_proxy_params params = {};
oakcodec_proxy_params_default(&params);
params.width = oakcommon_config_get_int(nullptr, "ProxyWidth",
params.width);
params.height = oakcommon_config_get_int(nullptr, "ProxyHeight",
params.height);
params.divider = oakcommon_config_get_int(nullptr, "ProxyDivider",
params.divider);
params.crf = oakcommon_config_get_int(nullptr, "ProxyCRF", params.crf);
params.include_audio =
oakcommon_config_get_bool(nullptr, "ProxyIncludeAudio",
params.include_audio)
? 1
: 0;
oakcommon_config_get(nullptr, "ProxyPreset", params.preset,
int(sizeof(params.preset)));
oakcommon_config_get(nullptr, "ProxyExtension", params.extension,
int(sizeof(params.extension)));
return params;
}
int submit_codec_task(const OakCodecTaskRequest *req, void *userdata)
{
(void)userdata;
if (!req) {
return OAKCODEC_E_INVALID;
}
std::unique_ptr<Task> task;
switch (req->kind) {
case OAKCODEC_TASK_CONFORM:
task = std::make_unique<ConformTask>(*req);
break;
case OAKCODEC_TASK_PROXY:
task = std::make_unique<ProxyTask>(*req, proxy_params_from_config());
break;
default:
return OAKCODEC_E_INVALID;
}
// Interim contract: submission is synchronous.
return task->start() ? OAKCODEC_OK : OAKCODEC_E_FAILED;
}
} // namespace
void register_codec_task_submitter()
{
oakcodec_set_task_submit_cb(submit_codec_task, nullptr);
}
void unregister_codec_task_submitter()
{
oakcodec_set_task_submit_cb(nullptr, nullptr);
}
}
+45
View File
@@ -0,0 +1,45 @@
/***
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_CODECBRIDGE_H
#define OAK_CODECBRIDGE_H
namespace olive
{
/**
* @brief Register oaktask as oakcodec's background task submitter
*
* Wires oakcodec_set_task_submit_cb() to execute OAKCODEC_TASK_CONFORM /
* OAKCODEC_TASK_PROXY requests as ConformTask/ProxyTask. Submission is
* synchronous (the callback runs the task to completion), matching
* oakcodec's interim contract.
*/
void register_codec_task_submitter();
/**
* @brief Unregister the submitter (oaktask_shutdown).
*/
void unregister_codec_task_submitter();
}
#endif // OAK_CODECBRIDGE_H
+155
View File
@@ -0,0 +1,155 @@
/***
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 "conform.h"
#include <filesystem>
namespace olive
{
namespace
{
int channel_count_from_layout(uint64_t layout)
{
int count = 0;
while (layout) {
count += int(layout & 1);
layout >>= 1;
}
return count;
}
} // namespace
ConformTask::ConformTask(const OakCodecTaskRequest &request)
: input_filename_(request.input_filename ? request.input_filename : "")
, output_filename_(request.output_filename ? request.output_filename
: "")
, stream_index_(request.stream_index)
, sample_rate_(request.sample_rate)
, channel_layout_(request.channel_layout)
, sample_format_(request.sample_format)
{
set_title("Conforming Audio " + input_filename_ + ":" +
std::to_string(stream_index_));
}
bool ConformTask::derive_filenames(const std::string &first_channel_final,
int channel_count,
std::vector<std::string> *final_names,
std::vector<std::string> *working_names)
{
static const std::string k_suffix = ".0.pcm";
if (channel_count < 1 || first_channel_final.size() <= k_suffix.size() ||
first_channel_final.compare(first_channel_final.size() -
k_suffix.size(),
k_suffix.size(), k_suffix) != 0) {
return false;
}
const std::string base =
first_channel_final.substr(0, first_channel_final.size() -
k_suffix.size());
final_names->clear();
working_names->clear();
final_names->reserve(size_t(channel_count));
working_names->reserve(size_t(channel_count));
for (int i = 0; i < channel_count; i++) {
std::string final_name = base + "." + std::to_string(i) + ".pcm";
final_names->push_back(final_name);
working_names->push_back(final_name + ".working");
}
return true;
}
bool ConformTask::run()
{
int channel_count = channel_count_from_layout(channel_layout_);
std::vector<std::string> final_names, working_names;
if (!derive_filenames(output_filename_, channel_count, &final_names,
&working_names)) {
set_error("Invalid conform output filename");
return false;
}
OakDecoder decoder = oakcodec_decoder_init();
if (!decoder.ctx) {
set_error("Failed to create decoder");
return false;
}
bool ret = false;
if (oakcodec_decoder_open(decoder, input_filename_.c_str(),
stream_index_) != OAKCODEC_OK) {
char err[256];
oakcodec_decoder_last_error(decoder, err, sizeof(err));
set_error(std::string("Failed to open decoder for audio conform: ") +
err);
} else {
std::vector<const char *> working_ptrs;
working_ptrs.reserve(working_names.size());
for (const std::string &name : working_names) {
working_ptrs.push_back(name.c_str());
}
int result = oakcodec_decoder_conform_audio(
decoder, working_ptrs.data(), int(working_ptrs.size()),
sample_rate_, channel_layout_, sample_format_,
get_cancel_atom());
oakcodec_decoder_close(decoder);
if (result == OAKCODEC_OK) {
ret = true;
for (size_t i = 0; i < final_names.size() && ret; i++) {
std::error_code ec;
std::filesystem::rename(working_names[i], final_names[i], ec);
if (ec) {
set_error("Failed to move conformed audio into place");
ret = false;
}
}
} else {
// Clean up any partial working files
for (const std::string &name : working_names) {
std::error_code ec;
std::filesystem::remove(name, ec);
}
if (result == OAKCODEC_E_CANCELLED) {
set_error("Audio conform was cancelled");
} else {
set_error("Audio conform failed");
}
}
}
oakcodec_decoder_free(&decoder);
return ret;
}
}
+68
View File
@@ -0,0 +1,68 @@
/***
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_CONFORMTASK_H
#define OAK_CONFORMTASK_H
#include "codec/decoder.h"
#include "codec/task.h"
#include "task.h"
namespace olive
{
/**
* @brief Audio conform task (pcm cache generation)
*
* Executes an OAKCODEC_TASK_CONFORM request: decodes the audio stream
* and writes per-channel pcm cache files (to ".working" names first,
* renamed to the final paths on success).
*/
class ConformTask : public Task {
public:
ConformTask(const OakCodecTaskRequest &request);
/**
* @brief Derive sibling per-channel and ".working" filenames from the
* first channel's final path (".../<base>.0.pcm")
*
* Extracted for testability. `channel_count` must be >= 1.
*/
static bool derive_filenames(const std::string &first_channel_final,
int channel_count,
std::vector<std::string> *final_names,
std::vector<std::string> *working_names);
protected:
virtual bool run() override;
private:
std::string input_filename_;
std::string output_filename_;
int stream_index_;
int sample_rate_;
uint64_t channel_layout_;
int sample_format_;
};
}
#endif // OAK_CONFORMTASK_H
@@ -0,0 +1,60 @@
/***
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 "customcachetask.h"
namespace olive
{
CustomCacheTask::CustomCacheTask(const std::string &sequence_name)
: cancelled_through_finish_(false)
{
set_title("Caching custom range for \"" + sequence_name + "\"");
}
void CustomCacheTask::finish()
{
std::lock_guard<std::mutex> lock(mutex_);
cancelled_through_finish_ = true;
cancel();
}
bool CustomCacheTask::run()
{
std::unique_lock<std::mutex> lock(mutex_);
while (!is_cancelled()) {
wait_cond_.wait(lock);
}
return true;
}
void CustomCacheTask::cancel_event()
{
if (!cancelled_through_finish_ && cancelled_callback_) {
cancelled_callback_();
}
wait_cond_.notify_one();
}
}
@@ -0,0 +1,66 @@
/***
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_CUSTOMCACHETASK_H
#define OAK_CUSTOMCACHETASK_H
#include <condition_variable>
#include <functional>
#include <mutex>
#include "task.h"
namespace olive
{
class CustomCacheTask : public Task {
public:
CustomCacheTask(const std::string &sequence_name);
void finish();
/**
* @brief Called when the task is cancelled by anything other than
* finish() (async return channel, 01 §4 exception)
*/
void set_cancelled_callback(std::function<void()> callback)
{
cancelled_callback_ = std::move(callback);
}
protected:
virtual bool run() override;
virtual void cancel_event() override;
private:
std::mutex mutex_;
std::condition_variable wait_cond_;
bool cancelled_through_finish_;
std::function<void()> cancelled_callback_;
};
}
#endif // OAK_CUSTOMCACHETASK_H
+313
View File
@@ -0,0 +1,313 @@
/***
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 "node/color/colormanager/colormanager.h"
namespace olive
{
ExportTask::ExportTask(ViewerOutput *viewer_node, ColorManager *color_manager,
const EncodingParams &params)
: params_(params)
{
// Create a copy of the project
copier_ = new ProjectCopier(this);
copier_->set_project(viewer_node->project());
set_viewer(copier_->get_copy(viewer_node));
color_manager_ = copier_->get_copied_project()->color_manager();
// Adjust video params to have no divider
VideoParams vp = viewer_node->get_video_params();
vp.set_divider(1);
vp.set_time_base(params.video_params().time_base());
vp.set_frame_rate(params.video_params().frame_rate());
set_video_params(vp);
set_audio_params(viewer_node->get_audio_params());
set_title(tr("Exporting \"%1\"").arg(viewer_node->get_label()));
set_native_progress_signalling_enabled(false);
}
bool ExportTask::run()
{
// For safety, if we're overwriting, we save to a temporary filename and then only overwrite it
// at the end
QString real_filename = params_.filename();
if (QFileInfo::exists(params_.filename())) {
// Generate a filename that definitely doesn't exist
params_.set_filename(
FileFunctions::get_safe_temporary_filename(real_filename));
}
// If we're exporting to a sidecar subtitle file, disable the subtitles in the main encoder
bool subtitles_enabled = params_.subtitles_enabled();
EncodingParams sidecar_params = params_;
if (subtitles_enabled && params_.subtitles_are_sidecar()) {
params_.disable_subtitles();
}
encoder_ = std::shared_ptr<Encoder>(Encoder::create_from_params(params_));
if (!encoder_) {
set_error(tr("Failed to create encoder"));
return false;
}
if (!encoder_->open()) {
set_error(tr("Failed to open file: %1").arg(encoder_->get_error()));
return false;
}
if (subtitles_enabled && params_.subtitles_are_sidecar()) {
// Construct sidecar params
sidecar_params.disable_video();
sidecar_params.disable_audio();
QString sidecar_filename;
{
QFileInfo fi(real_filename);
sidecar_filename = fi.completeBaseName();
sidecar_filename.append('.');
sidecar_filename.append(ExportFormat::get_extension(
sidecar_params.subtitle_sidecar_fmt()));
sidecar_filename = fi.dir().filePath(sidecar_filename);
}
sidecar_params.set_filename(sidecar_filename);
subtitle_encoder_ = std::shared_ptr<Encoder>(Encoder::create_from_format(
sidecar_params.subtitle_sidecar_fmt(), sidecar_params));
if (!subtitle_encoder_) {
set_error(tr("Failed to create subtitle encoder"));
return false;
}
if (!subtitle_encoder_->open()) {
set_error(tr("Failed to open subtitle sidecar file: %1")
.arg(sidecar_filename));
return false;
}
} else {
subtitle_encoder_ = encoder_;
}
if (params_.has_custom_range()) {
// Render custom range only
export_range_ = params_.custom_range();
} else {
// Render entire sequence
export_range_ = TimeRange(0, viewer()->get_length());
}
frame_time_ = 0;
QSize video_force_size;
QMatrix4x4 video_force_matrix;
if (params_.video_enabled()) {
// If a transformation matrix is applied to this video, create it here
if (video_params().width() != params_.video_params().width() ||
video_params().height() != params_.video_params().height()) {
video_force_size = QSize(params_.video_params().width(),
params_.video_params().height());
if (params_.video_scaling_method() != EncodingParams::k_stretch) {
video_force_matrix = EncodingParams::generate_matrix(
params_.video_scaling_method(), video_params().width(),
video_params().height(), params_.video_params().width(),
params_.video_params().height());
}
} else {
// Disables forcing size in the renderer
video_force_size = QSize(0, 0);
}
// Create color processor
color_processor_ = ColorProcessor::create(
color_manager_, color_manager_->get_reference_color_space(),
params_.color_transform());
}
// Start render process
TimeRangeList video_range, audio_range;
TimeRange subtitle_range;
if (params_.video_enabled()) {
if (export_range_.in() > 0) {
export_range_.set_in(Timecode::snap_time_to_timebase(
export_range_.in(), video_params().frame_rate_as_time_base()));
}
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,
RenderMode::k_online, nullptr, video_force_size, video_force_matrix,
encoder_->get_desired_pixel_format(), VideoParams::k_rgba_channel_count,
color_processor_, params_.color_transform());
bool success = true;
encoder_->close();
if (!encoder_->get_error().isEmpty()) {
set_error(encoder_->get_error());
success = false;
}
if (subtitle_encoder_ != encoder_) {
subtitle_encoder_->close();
if (!subtitle_encoder_->get_error().isEmpty()) {
set_error(subtitle_encoder_->get_error());
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()) {
QFile::remove(params_.filename());
} else if (params_.filename() != real_filename) {
// If we were writing to a temp file, overwrite now
if (!FileFunctions::rename_file_allow_overwrite(params_.filename(),
real_filename)) {
set_error(
tr("Failed to overwrite \"%1\". Export has been saved as \"%2\" instead.")
.arg(real_filename, params_.filename()));
success = false;
}
}
return success;
}
bool ExportTask::frame_downloaded(FramePtr 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(tr("Render workers failed to deliver %1 consecutive "
"frames; aborting export")
.arg(null_frame_streak_));
return false;
}
} else {
null_frame_streak_ = 0;
}
Rational actual_time = time - export_range_.in();
time_map_.insert(actual_time, f);
while (!is_cancelled()) {
Rational real_time = Timecode::timestamp_to_time(
frame_time_, video_params().frame_rate_as_time_base());
if (!time_map_.contains(real_time)) {
break;
}
// Unfortunately this can't be done in another thread since the frames need to be sent
// one after the other chronologically.
if (!encoder_->write_frame(time_map_.take(real_time), real_time)) {
set_error(encoder_->get_error());
return false;
}
frame_time_++;
emit progress_changed(double(frame_time_) /
double(get_total_number_of_frames()));
}
return true;
}
bool ExportTask::audio_downloaded(const TimeRange &range,
const SampleBuffer &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(const SubtitleBlock *sub)
{
if (!subtitle_encoder_->write_subtitle(sub)) {
set_error(subtitle_encoder_->get_error());
return false;
} else {
return true;
}
}
bool ExportTask::write_audio_loop(const TimeRange &time,
const SampleBuffer &samples)
{
if (!encoder_->write_audio(samples)) {
set_error(encoder_->get_error());
return false;
}
audio_time_ = time.out();
for (auto it = audio_map_.begin(); it != audio_map_.end(); it++) {
TimeRange t = it.key();
SampleBuffer s = it.value();
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;
}
}
+86
View File
@@ -0,0 +1,86 @@
/***
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 "codec/encoder.h"
#include "node/output/viewer/viewer.h"
#include "render/colorprocessor.h"
#include "render/projectcopier.h"
#include "task/render/render.h"
#include "task/task.h"
namespace olive
{
class ExportTask : public RenderTask {
Q_OBJECT
public:
ExportTask(ViewerOutput *viewer_node, ColorManager *color_manager,
const EncodingParams &params);
protected:
virtual bool run() override;
virtual bool frame_downloaded(FramePtr frame, const Rational &time) override;
virtual bool audio_downloaded(const TimeRange &range,
const SampleBuffer &samples) override;
virtual bool encode_subtitle(const SubtitleBlock *sub) override;
virtual bool two_step_frame_rendering() const override
{
return false;
}
private:
bool write_audio_loop(const TimeRange &time, const SampleBuffer &samples);
ProjectCopier *copier_;
QHash<Rational, FramePtr> time_map_;
QHash<TimeRange, SampleBuffer> audio_map_;
ColorManager *color_manager_;
EncodingParams params_;
std::shared_ptr<Encoder> encoder_;
std::shared_ptr<Encoder> subtitle_encoder_;
ColorProcessorPtr color_processor_;
int64_t frame_time_;
int null_frame_streak_ = 0;
Rational audio_time_;
TimeRange export_range_;
};
}
#endif // OAK_EXPORTTASK_H
@@ -0,0 +1,113 @@
/***
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;
}
}
@@ -0,0 +1,56 @@
/***
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_PRECACHETASK_H
#define OAK_PRECACHETASK_H
#include "node/project/footage/footage.h"
#include "node/project/sequence/sequence.h"
#include "task/render/render.h"
namespace olive
{
class PreCacheTask : public RenderTask {
Q_OBJECT
public:
PreCacheTask(Footage *footage, int index, Sequence *sequence);
virtual ~PreCacheTask() override;
protected:
virtual bool run() override;
virtual bool frame_downloaded(FramePtr frame,
const Rational &times) override;
virtual bool audio_downloaded(const TimeRange &range,
const SampleBuffer &samples) override;
private:
Project *project_;
Footage *footage_;
};
}
#endif // OAK_PRECACHETASK_H
+416
View File
@@ -0,0 +1,416 @@
/***
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 "import.h"
#include <filesystem>
#include "codec/decoder.h"
#include "common/config.h"
#include "common/videoparams.h"
#include "undo/undostack.h"
namespace olive
{
namespace
{
int count_files_recursive(const std::vector<std::string> &paths)
{
int count = 0;
std::error_code ec;
for (const std::string &path : paths) {
if (std::filesystem::is_directory(path, ec)) {
for (const auto &entry :
std::filesystem::recursive_directory_iterator(path, ec)) {
if (entry.is_regular_file(ec)) {
count++;
}
}
} else {
count++;
}
}
return count;
}
std::string basename_of(const std::string &path)
{
return std::filesystem::path(path).filename().string();
}
} // namespace
ProjectImportTask::ImageSequenceConfirmFn ProjectImportTask::confirm_callback_;
ProjectImportTask::ProjectImportTask(
OakNodeFolder *folder, OakNodeProject *project,
const std::vector<std::string> &filenames)
: command_(nullptr)
, folder_(folder)
, project_(project)
, filenames_(filenames)
{
file_count_ = count_files_recursive(filenames_);
if (file_count_ <= 0) {
file_count_ = 1;
}
set_title("Importing " + std::to_string(file_count_) + " file(s)");
}
ProjectImportTask::~ProjectImportTask()
{
if (command_) {
oakundo_command_free(command_);
}
}
const int &ProjectImportTask::get_file_count() const
{
return file_count_;
}
bool ProjectImportTask::run()
{
command_ = oakundo_command_init_multi();
if (!command_) {
set_error("Failed to create import command");
return false;
}
int imported = 0;
import(folder_, filenames_, imported, command_);
if (is_cancelled()) {
oakundo_command_free(command_);
command_ = nullptr;
return false;
}
return true;
}
void ProjectImportTask::import(OakNodeFolder *folder,
const std::vector<std::string> &entries,
int &counter, OakUndoCommand *parent_command)
{
std::vector<std::string> mutable_entries = entries;
for (size_t i = 0; i < mutable_entries.size(); i++) {
if (is_cancelled()) {
break;
}
const std::string &file_path = mutable_entries[i];
std::error_code ec;
if (std::filesystem::is_directory(file_path, ec)) {
// Create a folder corresponding to the directory and recurse
std::vector<std::string> entry_list;
for (const auto &entry :
std::filesystem::directory_iterator(file_path, ec)) {
entry_list.push_back(entry.path().string());
}
if (!entry_list.empty()) {
OakNodeFolder *f = oaknode_folder_create(project_);
if (!f) {
continue;
}
oaknode_node_set_label(oaknode_folder_as_node(f),
basename_of(file_path).c_str());
add_item_to_folder(folder, oaknode_folder_as_node(f),
parent_command);
// Recursively follow this path
import(f, entry_list, counter, parent_command);
}
} else {
OakNodeFootage *footage =
oaknode_footage_create(project_, nullptr);
if (!footage) {
continue;
}
oaknode_footage_set_cancel_atom(footage, get_cancel_atom());
bool ok = oaknode_footage_set_filename(footage,
file_path.c_str()) ==
OAKNODE_OK;
OakCancelAtom empty_atom = {};
oaknode_footage_set_cancel_atom(footage, empty_atom);
if (ok && oaknode_footage_is_valid(footage)) {
oaknode_node_set_label(oaknode_footage_as_node(footage),
basename_of(file_path).c_str());
// See if this footage is an image sequence
validate_image_sequence(footage, mutable_entries, i);
// Create undoable command that adds the items to the model
add_item_to_folder(folder, oaknode_footage_as_node(footage),
parent_command);
// Add to vector
imported_footage_.push_back(footage);
} else {
// Add to list so we can tell the user about it later
invalid_files_.push_back(file_path);
oaknode_project_remove_node(project_,
oaknode_footage_as_node(footage));
oaknode_node_free(oaknode_footage_as_node(footage));
}
counter++;
emit_progress(static_cast<double>(counter) /
static_cast<double>(file_count_));
}
}
}
void ProjectImportTask::validate_image_sequence(
OakNodeFootage *footage, std::vector<std::string> &info_list,
size_t index)
{
char filename[1024];
if (oaknode_footage_filename(footage, filename, sizeof(filename)) <=
0) {
return;
}
// Heuristically determine whether this file is part of an image sequence or not.
// By this point we've established that video contains a single still image stream.
int digit_count = oakcodec_decoder_get_image_sequence_digit_count(filename);
if (digit_count <= 0) {
return;
}
bool ignored = false;
for (const std::string &ignore : image_sequence_ignore_files_) {
if (ignore == filename) {
ignored = true;
break;
}
}
if (ignored) {
return;
}
if (!item_is_still_image_footage_only(footage)) {
return;
}
OakVideoParams video_stream = {};
if (oaknode_footage_get_video_params(footage, 0, &video_stream) !=
OAKNODE_OK) {
return;
}
int width = 0, height = 0;
oakcommon_videoparams_get_width(video_stream, &width);
oakcommon_videoparams_get_height(video_stream, &height);
int64_t ind = oakcodec_decoder_get_image_sequence_index(filename);
// Check if files around exist that follow a sequence
char prev_fn[1024], next_fn[1024];
oakcodec_decoder_transform_image_sequence_file_name(filename, ind - 1,
prev_fn, sizeof(prev_fn));
oakcodec_decoder_transform_image_sequence_file_name(filename, ind + 1,
next_fn, sizeof(next_fn));
OakNodeFootage *previous_file =
oaknode_footage_create(project_, prev_fn);
OakNodeFootage *next_file = oaknode_footage_create(project_, next_fn);
bool prev_matches =
previous_file && oaknode_footage_is_valid(previous_file) &&
compare_still_image_size(previous_file, width, height);
bool next_matches =
next_file && oaknode_footage_is_valid(next_file) &&
compare_still_image_size(next_file, width, height);
if (prev_matches || next_matches) {
// By this point, we've established this file is a still image with a number at the end of
// the filename surrounded by adjacent numbers. It could be a still image! But let's ask the
// user just in case...
bool is_sequence =
confirm_callback_ ? confirm_callback_(filename) : false;
int64_t seq_index = oakcodec_decoder_get_image_sequence_index(filename);
// Heuristic to find the first and last images (users can always override this later in
// FootagePropertiesDialog)
int64_t start_index = get_image_sequence_limit(filename, seq_index,
false);
int64_t end_index = get_image_sequence_limit(filename, seq_index,
true);
// Depending on the user's choice, either remove them from the list or don't ask for
// the remainders
for (int64_t j = start_index; j <= end_index; j++) {
char entry_fn[1024];
oakcodec_decoder_transform_image_sequence_file_name(
filename, j, entry_fn, sizeof(entry_fn));
if (is_sequence) {
// If this is part of the sequence we're importing here, remove it
for (size_t k = index + 1; k < info_list.size(); k++) {
if (info_list[k] == entry_fn) {
info_list.erase(info_list.begin() + k);
break;
}
}
} else {
image_sequence_ignore_files_.push_back(entry_fn);
}
}
if (is_sequence) {
// User has confirmed it is an image sequence, let's set it accordingly.
oakcommon_videoparams_set_video_type(
video_stream, OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE);
char rate_str[64];
int needed = oakcommon_config_get(
nullptr, "DefaultSequenceFrameRate", rate_str,
sizeof(rate_str));
if (needed > 0) {
int num = 0, den = 1;
if (sscanf(rate_str, "%d/%d", &num, &den) == 2 && den != 0) {
oakcommon_videoparams_set_time_base(video_stream, num,
den);
oakcommon_videoparams_set_frame_rate(video_stream, den,
num);
}
}
oakcommon_videoparams_set_start_time(video_stream,
start_index);
oakcommon_videoparams_set_duration(video_stream,
end_index - start_index + 1);
oaknode_footage_set_video_params(footage, 0, &video_stream);
}
}
oakcommon_videoparams_free(&video_stream);
if (previous_file) {
oaknode_project_remove_node(project_,
oaknode_footage_as_node(previous_file));
oaknode_node_free(oaknode_footage_as_node(previous_file));
}
if (next_file) {
oaknode_project_remove_node(project_,
oaknode_footage_as_node(next_file));
oaknode_node_free(oaknode_footage_as_node(next_file));
}
}
void ProjectImportTask::add_item_to_folder(OakNodeFolder *folder,
OakNodeNode *item,
OakUndoCommand *command)
{
OakUndoCommand *child =
oaknode_command_create_folder_add_child(folder, item);
if (child) {
oakundo_command_multi_add_child(command, child);
}
}
bool ProjectImportTask::item_is_still_image_footage_only(
OakNodeFootage *footage)
{
if (oaknode_footage_total_stream_count(footage) != 1) {
// Footage with more than one stream (usually video+audio) most likely isn't an image sequence
return false;
}
OakVideoParams vp = {};
if (oaknode_footage_get_video_params(footage, 0, &vp) != OAKNODE_OK) {
return false;
}
int video_type = 0;
oakcommon_videoparams_get_video_type(vp, &video_type);
int valid = 0;
oakcommon_videoparams_get_is_valid(vp, &valid);
oakcommon_videoparams_free(&vp);
// Footage must be valid and video stream must be a still image to be an image sequence
return valid && video_type == OAKCOMMON_VIDEO_TYPE_STILL;
}
bool ProjectImportTask::compare_still_image_size(OakNodeFootage *footage,
int width, int height)
{
if (!item_is_still_image_footage_only(footage)) {
return false;
}
OakVideoParams stream = {};
if (oaknode_footage_get_video_params(footage, 0, &stream) !=
OAKNODE_OK) {
return false;
}
int w = 0, h = 0;
oakcommon_videoparams_get_width(stream, &w);
oakcommon_videoparams_get_height(stream, &h);
oakcommon_videoparams_free(&stream);
return w == width && h == height;
}
int64_t ProjectImportTask::get_image_sequence_limit(
const std::string &start_fn, int64_t start, bool up)
{
std::error_code ec;
while (true) {
int64_t test_index = up ? start + 1 : start - 1;
char test_filename[1024];
oakcodec_decoder_transform_image_sequence_file_name(
start_fn.c_str(), test_index, test_filename,
sizeof(test_filename));
if (!std::filesystem::exists(test_filename, ec)) {
// Reached end of index
break;
}
start = test_index;
}
return start;
}
}
+128
View File
@@ -0,0 +1,128 @@
/***
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_PROJECTIMPORTMANAGER_H
#define OAK_PROJECTIMPORTMANAGER_H
#include <functional>
#include <string>
#include <vector>
#include "node/folder.h"
#include "node/footage.h"
#include "node/node.h"
#include "task.h"
#include "undo/undocommand.h"
namespace olive
{
class ProjectImportTask : public Task {
public:
ProjectImportTask(OakNodeFolder *folder, OakNodeProject *project,
const std::vector<std::string> &filenames);
~ProjectImportTask() override;
const int &get_file_count() const;
/** Take ownership of the import command. After this call the task no
* longer owns (and will not free) the returned command. */
OakUndoCommand *take_command()
{
OakUndoCommand *c = command_;
command_ = nullptr;
return c;
}
const std::vector<std::string> &get_invalid_files() const
{
return invalid_files_;
}
bool has_invalid_files() const
{
return !invalid_files_.empty();
}
const std::vector<OakNodeFootage *> &get_imported_footage() const
{
return imported_footage_;
}
/**
* @brief Callback asking the user whether numbered stills form an
* image sequence (facade/UI concern). When no callback is
* installed, files are NOT treated as sequences (safest
* default - imports every file individually).
*/
using ImageSequenceConfirmFn = std::function<bool(
const std::string &filename)>;
static void set_image_sequence_confirm_callback(
ImageSequenceConfirmFn callback)
{
confirm_callback_ = std::move(callback);
}
protected:
virtual bool run() override;
private:
void import(OakNodeFolder *folder,
const std::vector<std::string> &entries, int &counter,
OakUndoCommand *parent_command);
void validate_image_sequence(OakNodeFootage *footage,
std::vector<std::string> &info_list,
size_t index);
void add_item_to_folder(OakNodeFolder *folder, OakNodeNode *item,
OakUndoCommand *command);
static bool item_is_still_image_footage_only(OakNodeFootage *footage);
static bool compare_still_image_size(OakNodeFootage *footage, int width,
int height);
static int64_t get_image_sequence_limit(const std::string &start_fn,
int64_t start, bool up);
OakUndoCommand *command_;
OakNodeFolder *folder_;
OakNodeProject *project_;
std::vector<std::string> filenames_;
int file_count_;
std::vector<std::string> invalid_files_;
std::vector<std::string> image_sequence_ignore_files_;
std::vector<OakNodeFootage *> imported_footage_;
static ImageSequenceConfirmFn confirm_callback_;
};
}
#endif // OAK_PROJECTIMPORTMANAGER_H
+100
View File
@@ -0,0 +1,100 @@
/***
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 "load.h"
#include "node/serializer.h"
namespace olive
{
ProjectLoadBaseTask::ProjectLoadBaseTask(const std::string &filename)
: project_(nullptr)
, filename_(filename)
{
set_title("Loading '" + filename + "'");
}
ProjectLoadTask::ProjectLoadTask(const std::string &filename)
: ProjectLoadBaseTask(filename)
{
}
bool ProjectLoadTask::run()
{
project_ = oaknode_project_init();
if (!project_) {
set_error("Failed to create project");
return false;
}
oaknode_project_set_filename(project_, get_filename().c_str());
int code = OAKNODE_SERIALIZER_RESULT_FILE_ERROR;
char details[512];
details[0] = 0;
int result = oaknode_serializer_load_from_file(
project_, get_filename().c_str(), &code, details, sizeof(details));
bool success = false;
switch (code) {
case OAKNODE_SERIALIZER_RESULT_SUCCESS:
success = true;
break;
case OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_OLD:
set_error("This project is from a version of Oak Video Editor that "
"is no longer supported in this version.");
break;
case OAKNODE_SERIALIZER_RESULT_PROJECT_TOO_NEW:
set_error("This project is from a newer version of Oak Video Editor "
"and cannot be opened in this version.");
break;
case OAKNODE_SERIALIZER_RESULT_UNKNOWN_VERSION:
set_error("Failed to determine project version.");
break;
case OAKNODE_SERIALIZER_RESULT_FILE_ERROR:
set_error("Failed to read file \"" + get_filename() +
"\" for reading.");
break;
case OAKNODE_SERIALIZER_RESULT_XML_ERROR:
set_error(std::string(
"Failed to read XML document. File may be corrupt. "
"Error was: ") +
details);
break;
case OAKNODE_SERIALIZER_RESULT_NO_DATA:
set_error("Failed to find any data to parse.");
break;
default:
set_error("Unknown error.");
break;
}
if (result == OAKNODE_OK && success) {
return true;
}
oaknode_project_free(project_);
project_ = nullptr;
return false;
}
}
+73
View File
@@ -0,0 +1,73 @@
/***
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_PROJECTLOADMANAGER_H
#define OAK_PROJECTLOADMANAGER_H
#include <string>
#include "node/project.h"
#include "task.h"
namespace olive
{
/**
* @brief Base task for loading a project from a file
*/
class ProjectLoadBaseTask : public Task {
public:
ProjectLoadBaseTask(const std::string &filename);
/**
* @brief Take the loaded project (ownership transfer). NULL if the
* task has not succeeded.
*/
OakNodeProject *take_project()
{
OakNodeProject *p = project_;
project_ = nullptr;
return p;
}
const std::string &get_filename() const
{
return filename_;
}
protected:
OakNodeProject *project_;
private:
std::string filename_;
};
class ProjectLoadTask : public ProjectLoadBaseTask {
public:
ProjectLoadTask(const std::string &filename);
protected:
virtual bool run() override;
};
}
#endif // OAK_PROJECTLOADMANAGER_H
@@ -0,0 +1,34 @@
/***
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));
}
}
@@ -0,0 +1,63 @@
/***
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,377 @@
/***
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 "loadotio.h"
#ifdef USE_OTIO
#include <opentimelineio/clip.h>
#include <opentimelineio/externalReference.h>
#include <opentimelineio/gap.h>
#include <opentimelineio/serializableCollection.h>
#include <opentimelineio/timeline.h>
#include <opentimelineio/transition.h>
#include <QApplication>
#include <QFileInfo>
#include <QThread>
#include "coreengine.h"
#include "node/audio/volume/volume.h"
#include "node/block/clip/clip.h"
#include "node/block/gap/gap.h"
#include "node/block/transition/crossdissolve/crossdissolvetransition.h"
#include "node/distort/transform/transformdistortnode.h"
#include "node/generator/matrix/matrix.h"
#include "node/math/math/math.h"
#include "node/nodeundo.h"
#include "node/project/folder/folder.h"
#include "node/project/footage/footage.h"
#include "node/project/sequence/sequence.h"
#include "timeline/timelineundogeneral.h"
namespace olive
{
LoadOTIOTask::LoadOTIOTask(const QString &s)
: ProjectLoadBaseTask(s)
{
}
bool LoadOTIOTask::run()
{
OTIO::ErrorStatus es;
auto root = OTIO::SerializableObjectWithMetadata::from_json_file(
get_filename().toStdString(), &es);
if (es.outcome != OTIO::ErrorStatus::Outcome::OK) {
set_error(
tr("Failed to load OpenTimelineIO from file \"%1\" \n\nOpenTimelineIO Error:\n\n%2")
.arg(get_filename(),
QString::fromStdString(es.full_description)));
return false;
}
project_ = new Project();
project_->initialize();
project_->set_modified(true);
std::vector<OTIO::Timeline *> timelines;
if (root->schema_name() == "SerializableCollection") {
// This is a number of timelines
std::vector<OTIO::SerializableObject::Retainer<OTIO::SerializableObject>>
&root_children =
static_cast<OTIO::SerializableCollection *>(root)->children();
timelines.resize(root_children.size());
for (size_t j = 0; j < root_children.size(); j++) {
timelines[j] =
static_cast<OTIO::Timeline *>(root_children[j].value);
}
} else if (root->schema_name() == "Timeline") {
// This is a single timeline
timelines.push_back(static_cast<OTIO::Timeline *>(root));
} else {
// Unknown root, we don't know what to do with this
set_error(tr("Unknown OpenTimelineIO root element"));
delete project_;
project_ = nullptr;
return false;
}
// Keep track of imported footage
QMap<QString, Footage *> imported_footage;
QMap<OTIO::Timeline *, Sequence *> timeline_sequnce_map;
// Variables used for loading bar
float number_of_clips = 0;
float clips_done = 0;
// Generate a list of sequences with the same names as the timelines.
// Assumes each timeline has a unique name.
int unnamed_sequence_count = 0;
for (auto timeline : timelines) {
Sequence *sequence = new Sequence();
if (!timeline->name().empty()) {
sequence->set_label(QString::fromStdString(timeline->name()));
} else {
// If the otio timeline does not provide a name, create a default one here
unnamed_sequence_count++;
QString label = tr("Sequence %1").arg(unnamed_sequence_count);
sequence->set_label(QString::fromStdString(label.toStdString()));
}
// Set default params incase they aren't edited.
sequence->set_default_parameters();
timeline_sequnce_map.insert(timeline, sequence);
// Get number of clips for loading bar
for (auto track : timeline->tracks()->children()) {
auto otio_track = static_cast<OTIO::Track *>(track.value);
number_of_clips += otio_track->children().size();
}
}
// Dialog has to be called from the main thread so we pass the list of sequences here.
bool accepted = false;
QMetaObject::invokeMethod(
EngineCore::instance(), "show_otio_import_dialog", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(bool, accepted),
Q_ARG(QList<Sequence *>, timeline_sequnce_map.values()));
if (!accepted) {
// Cancel to indicate to caller that this task did not complete and to simply dispose of it
Cancel();
qDeleteAll(timeline_sequnce_map); // Clear sequences
return true;
}
foreach (auto timeline, timeline_sequnce_map.keys()) {
Sequence *sequence = timeline_sequnce_map.value(timeline);
sequence->setParent(project_);
FolderAddChild(project_->root(), sequence).redo_now();
// Create a folder for this sequence's footage
Folder *sequence_footage = new Folder();
sequence_footage->set_label(QString::fromStdString(timeline->name()));
sequence_footage->setParent(project_);
FolderAddChild(project_->root(), sequence_footage).redo_now();
// Iterate through tracks
for (auto c : timeline->tracks()->children()) {
auto otio_track = static_cast<OTIO::Track *>(c.value);
// Create a new track
Track *track = nullptr;
// Determine what kind of track it is
if (otio_track->kind() == "Video" ||
otio_track->kind() == "Audio") {
Track::Type type;
if (otio_track->kind() == "Video") {
type = Track::k_video;
} else {
type = Track::k_audio;
}
// Create track
TimelineAddTrackCommand t(sequence->track_list(type));
t.redo_now();
track = t.track();
} else {
qWarning() << "Found unknown track type:"
<< otio_track->kind().c_str();
continue;
}
// Get clips from track
auto clip_map = otio_track->children();
if (es.outcome != OTIO::ErrorStatus::Outcome::OK) {
set_error(tr("Failed to load clip"));
return false;
}
Block *previous_block = nullptr;
bool prev_block_transition = false;
for (auto otio_block_retainer : clip_map) {
auto otio_block = otio_block_retainer.value;
Block *block = nullptr;
if (otio_block->schema_name() == "Clip") {
block = new ClipBlock();
} else if (otio_block->schema_name() == "Gap") {
block = new GapBlock();
} else if (otio_block->schema_name() == "Transition") {
// Todo: Look into OTIO supported transitions and add them to Olive
block = new CrossDissolveTransition();
} else {
// We don't know what this is yet, just create a gap for now so that *something* is there
qWarning() << "Found unknown block type:"
<< otio_block->schema_name().c_str();
block = new GapBlock();
}
block->setParent(project_);
block->set_label(QString::fromStdString(otio_block->name()));
track->append_block(block);
Rational start_time;
Rational duration;
if (otio_block->schema_name() == "Clip" ||
otio_block->schema_name() == "Gap") {
start_time = Rational::from_double(
static_cast<OTIO::Item *>(otio_block)
->source_range()
->start_time()
.to_seconds());
duration = Rational::from_double(
static_cast<OTIO::Item *>(otio_block)
->source_range()
->duration()
.to_seconds());
if (otio_block->schema_name() == "Clip") {
static_cast<ClipBlock *>(block)->set_media_in(
start_time);
}
block->set_length_and_media_out(duration);
}
// If the previous block was a transition, connect the current block to it
if (prev_block_transition) {
TransitionBlock *previous_transition_block =
static_cast<TransitionBlock *>(previous_block);
Node::connect_edge(
block, NodeInput(previous_transition_block,
TransitionBlock::k_in_block_input));
prev_block_transition = false;
}
if (otio_block->schema_name() == "Transition") {
TransitionBlock *transition_block =
static_cast<TransitionBlock *>(block);
OTIO::Transition *otio_block_transition =
static_cast<OTIO::Transition *>(otio_block);
// Set how far the transition eats into the previous clip
transition_block->set_offsets_and_length(
Rational::fromRationalTime(
otio_block_transition->in_offset()),
Rational::fromRationalTime(
otio_block_transition->out_offset()));
if (previous_block) {
Node::connect_edge(
previous_block,
NodeInput(transition_block,
TransitionBlock::k_out_block_input));
}
prev_block_transition = true;
// Add nodes to the graph and set up contexts
block->setParent(sequence->parent());
// Position transition in its own context
block->set_node_position_in_context(block, QPointF(0, 0));
}
if (otio_block->schema_name() == "Gap") {
// Add nodes to the graph and set up contexts
block->setParent(sequence->parent());
// Position transition in its own context
block->set_node_position_in_context(block, QPointF(0, 0));
}
// Update this after it's used but before any continue statements
previous_block = block;
if (otio_block->schema_name() == "Clip") {
auto otio_clip = static_cast<OTIO::Clip *>(otio_block);
if (!otio_clip->media_reference()) {
continue;
}
if (otio_clip->media_reference()->schema_name() ==
"ExternalReference") {
// Link footage
QString footage_url = QString::fromStdString(
static_cast<OTIO::ExternalReference *>(
otio_clip->media_reference())
->target_url());
Footage *probed_item;
if (imported_footage.contains(footage_url)) {
probed_item = imported_footage.value(footage_url);
} else {
probed_item = new Footage(footage_url);
imported_footage.insert(footage_url, probed_item);
probed_item->setParent(project_);
QFileInfo info(probed_item->filename());
probed_item->set_label(info.fileName());
FolderAddChild add(sequence_footage, probed_item);
add.redo_now();
}
// Add nodes to the graph and set up contexts
block->setParent(sequence->parent());
// Position clip in its own context
block->set_node_position_in_context(block, QPointF(0, 0));
// Position footage in its context
block->set_node_position_in_context(probed_item,
QPointF(-2, 0));
if (track->type() == Track::k_video) {
TransformDistortNode *transform =
new TransformDistortNode();
transform->setParent(sequence->parent());
Node::connect_edge(
probed_item,
NodeInput(transform,
TransformDistortNode::k_texture_input));
Node::connect_edge(transform,
NodeInput(block,
ClipBlock::k_buffer_in));
block->set_node_position_in_context(transform,
QPointF(-1, 0));
} else {
VolumeNode *volume_node = new VolumeNode();
volume_node->setParent(sequence->parent());
Node::connect_edge(
probed_item,
NodeInput(volume_node,
VolumeNode::k_samples_input));
Node::connect_edge(volume_node,
NodeInput(block,
ClipBlock::k_buffer_in));
block->set_node_position_in_context(volume_node,
QPointF(-1, 0));
}
}
}
clips_done++;
emit progress_changed(clips_done / number_of_clips);
}
}
}
project_->moveToThread(qApp->thread());
return true;
}
}
#endif // USE_OTIO
@@ -0,0 +1,47 @@
/***
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_OTIODECODER_H
#define OAK_OTIODECODER_H
#ifdef USE_OTIO
#include "common/otioutils.h"
#include "node/project.h"
#include "task/project/load/loadbasetask.h"
namespace olive
{
class LoadOTIOTask : public ProjectLoadBaseTask {
Q_OBJECT
public:
LoadOTIOTask(const QString &filename);
protected:
virtual bool run() override;
};
}
#endif
#endif // OAK_OTIODECODER_H
+98
View File
@@ -0,0 +1,98 @@
/***
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 "save.h"
#include "node/serializer.h"
namespace olive
{
namespace
{
std::string project_filename(OakNodeProject *project)
{
int needed = oaknode_project_filename(project, nullptr, 0);
if (needed <= 0) {
return std::string();
}
std::string filename(size_t(needed), 0);
oaknode_project_filename(project, filename.data(), needed);
filename.resize(size_t(needed) - 1);
return filename;
}
} // namespace
ProjectSaveTask::ProjectSaveTask(OakNodeProject *project,
bool use_compression)
: project_(project)
, use_compression_(use_compression)
{
set_title("Saving '" + project_filename(project_) + "'");
}
bool ProjectSaveTask::run()
{
std::string using_filename = override_filename_.empty()
? project_filename(project_)
: override_filename_;
if (using_filename.empty()) {
set_error("Project has no filename to save to.");
return false;
}
int code = OAKNODE_SERIALIZER_RESULT_FILE_ERROR;
char details[512];
details[0] = 0;
int result = oaknode_serializer_save_to_file(
project_, using_filename.c_str(), use_compression_ ? 1 : 0, &code,
details, sizeof(details));
bool success = false;
switch (code) {
case OAKNODE_SERIALIZER_RESULT_SUCCESS:
success = true;
break;
case OAKNODE_SERIALIZER_RESULT_XML_ERROR:
set_error("Failed to write XML data.");
break;
case OAKNODE_SERIALIZER_RESULT_FILE_ERROR:
set_error(std::string("Failed to open file for writing: ") + details);
break;
case OAKNODE_SERIALIZER_RESULT_OVERWRITE_ERROR:
set_error("Failed to overwrite \"" + using_filename +
"\". Project has been saved as \"" + details +
"\" instead.");
success = true;
break;
default:
set_error("Unknown error.");
break;
}
(void)result;
return success;
}
}
+60
View File
@@ -0,0 +1,60 @@
/***
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_PROJECTSAVEMANAGER_H
#define OAK_PROJECTSAVEMANAGER_H
#include <string>
#include "node/project.h"
#include "task.h"
namespace olive
{
class ProjectSaveTask : public Task {
public:
ProjectSaveTask(OakNodeProject *project, bool use_compression);
OakNodeProject *get_project() const
{
return project_;
}
void set_override_filename(const std::string &filename)
{
override_filename_ = filename;
}
protected:
virtual bool run() override;
private:
OakNodeProject *project_;
std::string override_filename_;
bool use_compression_;
};
}
#endif // OAK_PROJECTSAVEMANAGER_H
@@ -0,0 +1,278 @@
/***
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 "saveotio.h"
#ifdef USE_OTIO
#include <opentimelineio/clip.h>
#include <opentimelineio/externalReference.h>
#include <opentimelineio/gap.h>
#include <opentimelineio/serializableCollection.h>
#include <opentimelineio/serializableObject.h>
#include <opentimelineio/transition.h>
#include "node/block/clip/clip.h"
#include "node/block/gap/gap.h"
#include "node/block/transition/transition.h"
#include "node/project/footage/footage.h"
namespace olive
{
SaveOTIOTask::SaveOTIOTask(Project *project)
: project_(project)
{
set_title(tr("Exporting project to OpenTimelineIO"));
}
bool SaveOTIOTask::run()
{
QVector<Sequence *> sequences =
project_->root()->list_children_of_type<Sequence>();
if (sequences.isEmpty()) {
set_error(tr("Project contains no sequences to export."));
return false;
}
std::vector<OTIO::SerializableObject *> serialized;
foreach (Sequence *seq, sequences) {
auto otio_timeline = SerializeTimeline(seq);
if (otio_timeline) {
// Append to list
serialized.push_back(otio_timeline);
} else {
// Delete all existing timelines
for (auto s : serialized) {
s->possibly_delete();
}
// Error out of function
set_error(
tr("Failed to serialize sequence \"%1\"").arg(seq->get_label()));
return false;
}
}
OTIO::ErrorStatus es;
if (serialized.size() == 1) {
// Serialize timeline on its own
auto t = serialized.front();
t->to_json_file(project_->filename().toStdString(), &es);
t->possibly_delete();
} else {
// Serialize all into a SerializableCollection
auto collection =
new OTIO::SerializableCollection("Sequences", serialized);
collection->to_json_file(project_->filename().toStdString(), &es);
collection->possibly_delete();
// Delete all existing timelines
for (auto s : serialized) {
s->possibly_delete();
}
}
return (es.outcome == OTIO::ErrorStatus::Outcome::OK);
}
OTIO::Timeline *SaveOTIOTask::SerializeTimeline(Sequence *sequence)
{
auto otio_timeline = new OTIO::Timeline(sequence->get_label().toStdString());
// Retainers clean themselves up when the final user is removed
OTIO::Timeline::Retainer<OTIO::Timeline> *timeline_retainer =
new OTIO::Timeline::Retainer<OTIO::Timeline>(otio_timeline);
// Suppress unused variable warning
Q_UNUSED(timeline_retainer);
double rate = sequence->get_video_params().frame_rate().to_double();
if (qIsNaN(rate)) {
return nullptr;
}
if (!SerializeTrackList(sequence->track_list(Track::k_video), otio_timeline,
rate) ||
!SerializeTrackList(sequence->track_list(Track::k_audio), otio_timeline,
rate)) {
otio_timeline->possibly_delete();
return nullptr;
}
return otio_timeline;
}
OTIO::Track *SaveOTIOTask::SerializeTrack(Track *track, double sequence_rate,
Rational max_track_length)
{
auto otio_track = new OTIO::Track();
OTIO::ErrorStatus es;
switch (track->type()) {
case Track::k_video:
otio_track->set_kind("Video");
break;
case Track::k_audio:
otio_track->set_kind("Audio");
break;
default:
qWarning()
<< "Don't know OTIO track kind for native type" << track->type();
goto fail;
}
foreach (Block *block, track->blocks()) {
OTIO::Composable *otio_block = nullptr;
if (dynamic_cast<ClipBlock *>(block)) {
auto otio_clip = new OTIO::Clip(block->get_label().toStdString());
otio_clip->set_source_range(
OTIO::TimeRange(block->in().toRationalTime(sequence_rate),
block->length().toRationalTime(sequence_rate)));
QVector<Footage *> media_nodes = block->find_input_nodes<Footage>();
if (!media_nodes.isEmpty()) {
OTIO::TimeRange available_range;
if (otio_track->kind().compare("Video") == 0) {
// OTIO ExternalReference uses the source clips frame rate (or sample rate) as opposed to
// the sequences rate
double source_frame_rate = static_cast<ClipBlock *>(block)
->connected_viewer()
->get_video_params()
.frame_rate()
.to_double();
available_range = OTIO::TimeRange(
OTIO::RationalTime(0, source_frame_rate),
OTIO::RationalTime(
media_nodes.first()->get_video_params().duration(),
source_frame_rate));
} else if (otio_track->kind().compare("Audio") == 0) {
available_range = OTIO::TimeRange(
OTIO::RationalTime(
0,
media_nodes.first()->get_audio_params().sample_rate()),
OTIO::RationalTime(
media_nodes.first()->get_audio_params().duration(),
media_nodes.first()->get_audio_params().sample_rate()));
}
auto media_ref = new OTIO::ExternalReference(
media_nodes.first()->filename().toStdString(),
available_range);
otio_clip->set_media_reference(media_ref);
}
otio_block = otio_clip;
} else if (dynamic_cast<GapBlock *>(block)) {
otio_block =
new OTIO::Gap(OTIO::TimeRange(block->in().toRationalTime(),
block->length().toRationalTime()),
block->get_label().toStdString());
} else if (dynamic_cast<TransitionBlock *>(block)) {
auto otio_transition =
new OTIO::Transition(block->get_label().toStdString());
TransitionBlock *our_transition =
static_cast<TransitionBlock *>(block);
otio_transition->set_in_offset(
our_transition->in_offset().toRationalTime());
otio_transition->set_out_offset(
our_transition->out_offset().toRationalTime());
otio_block = new OTIO::Transition();
}
if (!otio_block) {
// We shouldn't ever get here, but catch without crashing if we ever do
goto fail;
}
otio_track->append_child(otio_block, &es);
if (es.outcome != OTIO::ErrorStatus::Outcome::OK) {
goto fail;
}
}
// All OTIO tracks must have the same duration so we add a Gap to fill the remaining time
if (otio_track->duration(&es).to_seconds() < max_track_length.to_double()) {
double time_left = max_track_length.to_double() -
otio_track->duration(&es).to_seconds();
OTIO::Gap *gap = new OTIO::Gap(OTIO::TimeRange(
otio_track->duration(&es), OTIO::RationalTime(time_left, 1.0)));
otio_track->append_child(gap, &es);
if (es.outcome != OTIO::ErrorStatus::Outcome::OK) {
goto fail;
}
}
return otio_track;
fail:
otio_track->possibly_delete();
return nullptr;
}
bool SaveOTIOTask::SerializeTrackList(TrackList *list,
OTIO::Timeline *otio_timeline,
double sequence_rate)
{
OTIO::ErrorStatus es;
Rational max_track_length = RATIONAL_MIN;
foreach (Track *track, list->get_tracks()) {
if (track->track_length() > max_track_length) {
max_track_length = track->track_length();
}
}
foreach (Track *track, list->get_tracks()) {
auto otio_track =
SerializeTrack(track, sequence_rate, max_track_length);
if (!otio_track) {
return false;
}
otio_timeline->tracks()->append_child(otio_track, &es);
if (es.outcome != OTIO::ErrorStatus::Outcome::OK) {
otio_track->possibly_delete();
return false;
}
}
return true;
}
}
#endif // USE_OTIO
@@ -0,0 +1,61 @@
/***
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_PROJECTSAVEASOTIOTASK_H
#define OAK_PROJECTSAVEASOTIOTASK_H
#ifdef USE_OTIO
#include <opentimelineio/timeline.h>
#include <opentimelineio/track.h>
#include "common/otioutils.h"
#include "node/project.h"
#include "task/task.h"
namespace olive
{
class SaveOTIOTask : public Task {
Q_OBJECT
public:
SaveOTIOTask(Project *project);
protected:
virtual bool run() override;
private:
OTIO::Timeline *SerializeTimeline(Sequence *sequence);
OTIO::Track *SerializeTrack(Track *track, double sequence_rate,
Rational max_track_length);
bool SerializeTrackList(TrackList *list, OTIO::Timeline *otio_timeline,
double sequence_rate);
Project *project_;
};
}
#endif
#endif // OAK_PROJECTSAVEASOTIOTASK_H
+310
View File
@@ -0,0 +1,310 @@
/***
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 "proxy.h"
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
namespace olive
{
namespace
{
std::string shell_quote(const std::string &s)
{
std::string out = "'";
for (char c : s) {
if (c == '\'') {
out += "'\\''";
} else {
out += c;
}
}
out += "'";
return out;
}
/**
* @brief Probes the source duration with the ffprobe next to ffmpeg
*
* Returns 0 when ffprobe is unavailable or the duration cannot be
* determined, in which case the task simply reports no intermediate
* progress.
*/
double probe_source_duration_seconds(const std::string &ffmpeg_path,
const std::string &source_filename)
{
std::filesystem::path ffprobe =
std::filesystem::path(ffmpeg_path).parent_path() / "ffprobe";
#if defined(_WIN32)
ffprobe += ".exe";
#endif
std::error_code ec;
if (!std::filesystem::exists(ffprobe, ec)) {
return 0.0;
}
std::string cmd = shell_quote(ffprobe.string()) +
" -v error -show_entries format=duration"
" -of default=noprint_wrappers=1:nokey=1 " +
shell_quote(source_filename);
FILE *pipe = popen(cmd.c_str(), "r");
if (!pipe) {
return 0.0;
}
char buf[128] = { 0 };
std::string output;
while (fgets(buf, sizeof(buf), pipe)) {
output += buf;
}
if (pclose(pipe) != 0) {
return 0.0;
}
return strtod(output.c_str(), nullptr);
}
} // namespace
ProxyTask::ProxyTask(const OakCodecTaskRequest &request,
const oakcodec_proxy_params &params)
: source_filename_(request.input_filename ? request.input_filename : "")
, stream_index_(request.stream_index)
, params_(params)
, output_filename_(request.output_filename ? request.output_filename
: "")
{
// Divider-based requests (width/height == 0) take the source fraction
if (request.proxy_width > 0 && request.proxy_height > 0) {
params_.width = request.proxy_width;
params_.height = request.proxy_height;
params_.divider = 1;
}
set_title("Generating Proxy " + source_filename_ + ":" +
std::to_string(stream_index_));
}
std::vector<std::string>
ProxyTask::build_arguments(const std::string &source_filename,
int stream_index,
const oakcodec_proxy_params &params,
const std::string &output_filename)
{
std::string scale_filter;
if (params.divider > 1) {
// Fraction of the source resolution, rounded down to even dimensions
// as required by yuv420p
scale_filter = "scale=w=trunc(iw/" + std::to_string(params.divider) +
"/2)*2:h=trunc(ih/" + std::to_string(params.divider) +
"/2)*2";
} else {
scale_filter = "scale=w=" + std::to_string(params.width) +
":h=" + std::to_string(params.height) +
":force_original_aspect_ratio=decrease";
}
const std::string container_format =
params.extension[0] ? params.extension : "mp4";
std::vector<std::string> args;
args.emplace_back("-y");
// Report machine-readable progress on stdout for the task dialog
args.emplace_back("-nostats");
args.emplace_back("-progress");
args.emplace_back("pipe:1");
args.emplace_back("-i");
args.emplace_back(source_filename);
// Map the requested video stream first so it is stream 0 in the proxy
args.emplace_back("-map");
args.emplace_back("0:" + std::to_string(stream_index));
if (params.include_audio) {
// Keep the source audio (if any) so the proxy can also be used for
// audio preview. Audio streams follow the video stream in source order.
args.emplace_back("-map");
args.emplace_back("0:a?");
args.emplace_back("-c:a");
args.emplace_back("aac");
args.emplace_back("-b:a");
args.emplace_back("128k");
} else {
args.emplace_back("-an");
}
args.emplace_back("-vf");
args.emplace_back(scale_filter);
args.emplace_back("-c:v");
args.emplace_back("libx264");
args.emplace_back("-preset");
args.emplace_back(params.preset);
args.emplace_back("-crf");
args.emplace_back(std::to_string(params.crf));
args.emplace_back("-pix_fmt");
args.emplace_back("yuv420p");
args.emplace_back("-movflags");
args.emplace_back("+faststart");
args.emplace_back("-f");
args.emplace_back(container_format);
args.emplace_back(output_filename);
return args;
}
double ProxyTask::parse_progress(const std::string &line,
double duration_seconds)
{
if (duration_seconds <= 0.0) {
return -1.0;
}
int64_t out_time_us = -1;
if (line.rfind("out_time_us=", 0) == 0) {
out_time_us = strtoll(line.c_str() + 12, nullptr, 10);
} else if (line.rfind("out_time_ms=", 0) == 0) {
// Despite the name, ffmpeg reports this value in microseconds
out_time_us = strtoll(line.c_str() + 12, nullptr, 10);
}
if (out_time_us < 0) {
return -1.0;
}
double progress = out_time_us / 1000000.0 / duration_seconds;
return progress < 0.0 ? 0.0 : (progress > 1.0 ? 1.0 : progress);
}
bool ProxyTask::run()
{
char ffmpeg_buf[1024];
if (oakcodec_proxy_find_ffmpeg(nullptr, ffmpeg_buf,
sizeof(ffmpeg_buf)) <= 0) {
set_error("Failed to generate proxy: ffmpeg executable was not "
"found. Set the ffmpeg path in Preferences > Disk > Proxy "
"Settings.");
fprintf(stderr, "ProxyTask: ffmpeg executable not found\n");
return false;
}
std::filesystem::path output_dir =
std::filesystem::path(output_filename_).parent_path();
std::error_code ec;
if (!output_dir.empty() && !std::filesystem::exists(output_dir, ec) &&
!std::filesystem::create_directories(output_dir, ec)) {
set_error("Failed to create proxy output directory");
return false;
}
fprintf(stderr, "ProxyTask: starting ffmpeg proxy generation: %s -> %s\n",
source_filename_.c_str(), output_filename_.c_str());
std::filesystem::remove(output_filename_, ec);
const std::string working_filename = output_filename_ + ".working.mp4";
std::filesystem::remove(working_filename, ec);
const std::vector<std::string> args = build_arguments(
source_filename_, stream_index_, params_, working_filename);
std::string cmd = shell_quote(ffmpeg_buf);
for (const std::string &arg : args) {
cmd += " ";
cmd += shell_quote(arg);
}
cmd += " 2>&1";
const double duration_seconds =
probe_source_duration_seconds(ffmpeg_buf, source_filename_);
FILE *process = popen(cmd.c_str(), "r");
if (!process) {
set_error("Failed to start ffmpeg for proxy generation");
return false;
}
std::string progress_buffer;
double last_progress = 0.0;
char buf[256];
while (true) {
size_t n = fread(buf, 1, sizeof(buf), process);
if (n > 0) {
progress_buffer.append(buf, n);
size_t newline;
while ((newline = progress_buffer.find('\n')) !=
std::string::npos) {
std::string line = progress_buffer.substr(0, newline);
progress_buffer.erase(0, newline + 1);
double progress = parse_progress(line, duration_seconds);
if (progress >= 0.0 && progress - last_progress > 0.001) {
last_progress = progress;
emit_progress(progress);
}
}
}
if (n < sizeof(buf)) {
if (feof(process)) {
break;
}
if (ferror(process)) {
break;
}
}
if (is_cancelled()) {
pclose(process);
std::filesystem::remove(working_filename, ec);
set_error("Proxy generation was cancelled");
return false;
}
}
int exit_status = pclose(process);
if (exit_status != 0) {
std::filesystem::remove(working_filename, ec);
set_error("ffmpeg failed to generate proxy");
fprintf(stderr, "ProxyTask: ffmpeg failed with exit status %d\n",
exit_status);
return false;
}
if (!std::filesystem::exists(working_filename, ec)) {
set_error("ffmpeg finished but proxy file was not created");
return false;
}
std::filesystem::rename(working_filename, output_filename_, ec);
if (ec) {
set_error("Failed to move proxy into place");
return false;
}
fprintf(stderr, "ProxyTask: proxy generation succeeded: %s\n",
output_filename_.c_str());
emit_progress(1.0);
return true;
}
}
+75
View File
@@ -0,0 +1,75 @@
/***
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_PROXYTASK_H
#define OAK_PROXYTASK_H
#include <string>
#include <vector>
#include "codec/proxy.h"
#include "codec/task.h"
#include "task.h"
namespace olive
{
class ProxyTask : public Task {
public:
ProxyTask(const OakCodecTaskRequest &request,
const oakcodec_proxy_params &params);
/**
* @brief Builds the ffmpeg command line for a proxy generation run
*
* Extracted for testability. The video stream is always mapped first so
* that it is stream 0 in the proxy file; audio streams (when enabled)
* follow in source order.
*/
static std::vector<std::string> build_arguments(
const std::string &source_filename, int stream_index,
const oakcodec_proxy_params &params,
const std::string &output_filename);
/**
* @brief Parses one line of ffmpeg "-progress" output
*
* Extracted for testability. If the line carries an output timestamp
* ("out_time_us=" or "out_time_ms="), returns the progress fraction in
* the range [0, 1] against duration_seconds. Returns a negative value
* when the line carries no timestamp or duration_seconds is unknown.
*/
static double parse_progress(const std::string &line,
double duration_seconds);
protected:
virtual bool run() override;
private:
std::string source_filename_;
int stream_index_;
oakcodec_proxy_params params_;
std::string output_filename_;
};
}
#endif // OAK_PROXYTASK_H
+352
View File
@@ -0,0 +1,352 @@
/***
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();
}
}
+154
View File
@@ -0,0 +1,154 @@
/***
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
+233
View File
@@ -0,0 +1,233 @@
/***
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_TASK_H
#define OAK_TASK_H
#include <chrono>
#include <functional>
#include <mutex>
#include <string>
#include <vector>
#include "render/cancelatom.h"
#include <new>
namespace olive
{
/**
* @brief A base class for background tasks running in Olive.
*
* Tasks are multithreaded by design (i.e. they will always spawn
* a new thread and run in it).
*
* To subclass your own Task, override run() and return TRUE on success or FALSE on failure. Note that a Task can
* provide a "negative" output and still have succeeded. For example, the ProbeTask's role is to determine whether a
* certain media file can be used in Olive. Even if the probe *fails* to find a Decoder for this file, the Task itself
* has *succeeded* at discovering this. A failure of ProbeTask would indicate a catastrophic failure meaning it was
* unable to determine anything about the file.
*
* Tasks should be used with the TaskManager which will manage starting and deleting them.
*
* De-Qt version: Qt signals are replaced by listener callbacks
* (01 §4: async tasks are the one place callbacks are allowed - they are
* the command's return channel).
*/
class Task {
public:
/**
* @brief Task lifecycle events delivered to listeners
*/
enum EventType {
k_event_started,
k_event_progress,
k_event_finished
};
/**
* @brief Listener callback. For k_event_finished, `value` is 1.0 on
* success and 0.0 on failure; for k_event_progress it is 0..1; for
* k_event_started it is the start time in milliseconds.
*/
using EventListener = std::function<void(EventType, double)>;
Task()
: title_("Task")
, error_("Unknown error")
, start_time_(0)
, cancel_atom_(oakrender_cancelatom_init())
{
}
virtual ~Task()
{
oakrender_cancelatom_free(&cancel_atom_);
}
/**
* @brief Retrieve the current title of this Task
*/
const std::string &get_title() const
{
return title_;
}
/**
* @brief Returns the error that occurred if run() returns false
*/
const std::string &get_error() const
{
return error_;
}
const int64_t &get_start_time() const
{
return start_time_;
}
/**
* @brief Register a lifecycle listener (async command return channel)
*/
void add_event_listener(EventListener listener)
{
std::lock_guard<std::mutex> lock(listeners_mutex_);
listeners_.push_back(std::move(listener));
}
/**
* @brief Run this task
*
* @return True if the task completed successfully, false if not.
*
* \see get_error() if this returns false.
*/
bool start()
{
start_time_ = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch())
.count();
emit_event(k_event_started, double(start_time_));
bool ret = run();
emit_event(k_event_finished, ret ? 1.0 : 0.0);
return ret;
}
/**
* @brief Reset state so that run() can be called again.
*/
virtual void reset()
{
}
/**
* @brief Cancel the Task
*
* Sends a signal to the Task to stop as soon as possible. Always call this directly.
*/
void cancel()
{
oakrender_cancelatom_cancel(cancel_atom_);
cancel_event();
}
bool is_cancelled() const
{
int cancelled = 0;
oakrender_cancelatom_is_cancelled(cancel_atom_, &cancelled);
return cancelled != 0;
}
/**
* @brief The task's cancellation atom (borrowed handle; addref if
* retained beyond the task's lifetime)
*/
OakCancelAtom get_cancel_atom() const
{
return cancel_atom_;
}
/**
* @brief Emit a progress value between 0.0 and 1.0
*/
void emit_progress(double p)
{
emit_event(k_event_progress, p);
}
protected:
virtual bool run() = 0;
/**
* @brief Called when the task is cancelled (subclass hook)
*/
virtual void cancel_event()
{
}
/**
* @brief Set the error message
*/
void set_error(const std::string &s)
{
error_ = s;
}
/**
* @brief Set the Task title
*/
void set_title(const std::string &s)
{
title_ = s;
}
private:
void emit_event(EventType type, double value)
{
std::vector<EventListener> listeners;
{
std::lock_guard<std::mutex> lock(listeners_mutex_);
listeners = listeners_;
}
for (const EventListener &listener : listeners) {
listener(type, value);
}
}
std::string title_;
std::string error_;
int64_t start_time_;
OakCancelAtom cancel_atom_;
std::mutex listeners_mutex_;
std::vector<EventListener> listeners_;
};
}
#endif // OAK_TASK_H
+146
View File
@@ -0,0 +1,146 @@
/***
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 "taskmanager.h"
namespace olive
{
TaskManager *TaskManager::instance_ = nullptr;
TaskManager::~TaskManager()
{
std::vector<std::unique_ptr<Entry>> tasks;
{
std::lock_guard<std::mutex> lock(mutex_);
tasks = std::move(tasks_);
}
for (auto &entry : tasks) {
entry->task->cancel();
}
for (auto &entry : tasks) {
if (entry->thread.joinable()) {
entry->thread.join();
}
}
}
void TaskManager::create_instance()
{
instance_ = new TaskManager();
}
void TaskManager::destroy_instance()
{
delete instance_;
instance_ = nullptr;
}
TaskManager *TaskManager::instance()
{
return instance_;
}
int TaskManager::get_task_count() const
{
std::lock_guard<std::mutex> lock(const_cast<std::mutex &>(mutex_));
return int(tasks_.size());
}
Task *TaskManager::get_task_at(int index) const
{
std::lock_guard<std::mutex> lock(const_cast<std::mutex &>(mutex_));
if (index < 0 || index >= int(tasks_.size())) {
return nullptr;
}
return tasks_[index]->task;
}
void TaskManager::add_task(Task *t)
{
auto entry = std::make_unique<Entry>();
entry->task = t;
entry->finished = std::make_shared<std::atomic<bool>>(false);
entry->succeeded = std::make_shared<std::atomic<bool>>(false);
std::shared_ptr<std::atomic<bool>> finished = entry->finished;
std::shared_ptr<std::atomic<bool>> succeeded = entry->succeeded;
entry->thread = std::thread([t, finished, succeeded]() {
bool ret = t->start();
succeeded->store(ret);
finished->store(true);
});
std::lock_guard<std::mutex> lock(mutex_);
tasks_.push_back(std::move(entry));
}
void TaskManager::cancel_task(Task *t)
{
t->cancel();
}
void TaskManager::cancel_task_and_wait(Task *t)
{
t->cancel();
std::thread tmp;
{
std::lock_guard<std::mutex> lock(mutex_);
for (auto &entry : tasks_) {
if (entry->task == t && entry->thread.joinable()) {
tmp = std::move(entry->thread);
break;
}
}
}
if (tmp.joinable()) {
tmp.join();
}
}
void TaskManager::delete_finished()
{
std::vector<std::unique_ptr<Entry>> finished_entries;
{
std::lock_guard<std::mutex> lock(mutex_);
auto it = tasks_.begin();
while (it != tasks_.end()) {
if ((*it)->finished->load()) {
finished_entries.push_back(std::move(*it));
it = tasks_.erase(it);
} else {
it++;
}
}
}
for (auto &entry : finished_entries) {
if (entry->thread.joinable()) {
entry->thread.join();
}
}
}
}
+97
View File
@@ -0,0 +1,97 @@
/***
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_TASKMANAGER_H
#define OAK_TASKMANAGER_H
#include <map>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
#include "task.h"
namespace olive
{
/**
* @brief An object that manages background Task objects, handling their start and end
*
* TaskManager handles the life of a Task object. After a new Task is created, it should be sent to TaskManager through
* add_task(). TaskManager will take ownership of the task and run it on a worker thread.
*
* De-Qt version: no signals. The caller that creates/deletes tasks knows
* what changed (01 §4), so no list-changed notification exists here.
*/
class TaskManager {
public:
TaskManager() = default;
/**
* @brief Ensures all Tasks are cancelled, joined and deleted
*/
virtual ~TaskManager();
static void create_instance();
static void destroy_instance();
static TaskManager *instance();
int get_task_count() const;
Task *get_task_at(int index) const;
/**
* @brief Add a new Task
*
* TaskManager takes ownership of this Task and will be responsible for freeing it.
* A Task object should only be added once.
*/
void add_task(Task *t);
void cancel_task(Task *t);
void cancel_task_and_wait(Task *t);
/**
* @brief Delete all finished tasks (successful or failed)
*/
void delete_finished();
private:
struct Entry {
Task *task;
std::thread thread;
std::shared_ptr<std::atomic<bool>> finished;
std::shared_ptr<std::atomic<bool>> succeeded;
};
std::mutex mutex_;
std::vector<std::unique_ptr<Entry>> tasks_;
static TaskManager *instance_;
};
}
#endif // OAK_TASKMANAGER_H
+177
View File
@@ -0,0 +1,177 @@
# 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/>.
cmake_minimum_required(VERSION 3.16)
project(oaktask-standalone CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# oaktask manipulates node graphs through the oaknode C ABI; the
# not-yet-split engine modules' symbols are allowed to dangle, resolved
# by transitional stubs in src/{node,render}/transition.
set(OAK_REPO_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../../..)
list(APPEND CMAKE_MODULE_PATH "${OAK_REPO_ROOT}/cmake")
set(BUILD_TESTS ON CACHE BOOL "" FORCE)
set(OLIVECORE_BUILD_TESTS OFF CACHE BOOL "" FORCE)
# The /opt/otio OpenTimelineIO dylibs use @loader_path install names that
# break when linked from a build tree; oaktask does not need OTIO.
set(CMAKE_DISABLE_FIND_PACKAGE_OpenTimelineIO ON)
find_package(EXPAT REQUIRED)
find_package(OpenColorIO CONFIG REQUIRED)
find_package(OpenImageIO CONFIG REQUIRED)
set(OCIO_LIBRARIES OpenColorIO::OpenColorIO)
set(OCIO_INCLUDE_DIRS ${OpenColorIO_INCLUDE_DIRS})
set(OIIO_LIBRARIES OpenImageIO::OpenImageIO)
set(OIIO_INCLUDE_DIRS ${OpenImageIO_INCLUDE_DIRS})
add_subdirectory(${OAK_REPO_ROOT}/core ${CMAKE_BINARY_DIR}/core)
target_include_directories(olivecore PUBLIC
${OAK_REPO_ROOT}/third_party/openfx/include
)
add_subdirectory(${OAK_REPO_ROOT}/ffmpeg_bridge ${CMAKE_BINARY_DIR}/ffmpeg_bridge)
set(BUILD_TESTS OFF)
add_subdirectory(${OAK_REPO_ROOT}/src/undo ${CMAKE_BINARY_DIR}/undo)
add_subdirectory(${OAK_REPO_ROOT}/src/common ${CMAKE_BINARY_DIR}/common)
add_subdirectory(${OAK_REPO_ROOT}/src/render/src ${CMAKE_BINARY_DIR}/render)
add_subdirectory(${OAK_REPO_ROOT}/src/render/c_api ${CMAKE_BINARY_DIR}/render_c_api)
set(BUILD_TESTS ON)
enable_testing()
set(BUILD_TESTS OFF)
add_subdirectory(${OAK_REPO_ROOT}/src/timeline ${CMAKE_BINARY_DIR}/timeline)
add_subdirectory(${OAK_REPO_ROOT}/src/codec ${CMAKE_BINARY_DIR}/codec)
set(BUILD_TESTS ON)
add_subdirectory(${OAK_REPO_ROOT}/src/node ${CMAKE_BINARY_DIR}/node)
add_subdirectory(${OAK_REPO_ROOT}/src/task ${CMAKE_BINARY_DIR}/task)
target_include_directories(oakcodec PUBLIC
${OAK_REPO_ROOT}/src/render/transition
${OAK_REPO_ROOT}/src/node/transition
${OAK_REPO_ROOT}/src/render/src
${OAK_REPO_ROOT}/src/node/src
)
foreach(t oakcodec oaktimeline)
if(TARGET ${t})
target_link_options(${t} PRIVATE
"-undefined" "dynamic_lookup"
)
endif()
endforeach()
# oaknode needs its transition stubs and the oakrender real headers
# (src/node/transition/render/* bridge there); src/render/transition
# must precede src/node/transition.
target_include_directories(oaknode BEFORE PUBLIC
${OAK_REPO_ROOT}/src/render/transition
${OAK_REPO_ROOT}/src/node/transition
${OAK_REPO_ROOT}/src/render/src
)
target_include_directories(oaknode PUBLIC
${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include
/opt/homebrew/include
/opt/homebrew/include/Imath
)
target_link_options(oaknode PRIVATE
"-undefined" "dynamic_lookup"
)
target_include_directories(oakrender BEFORE PUBLIC
${OAK_REPO_ROOT}/src/render/transition
${OAK_REPO_ROOT}/src/node/transition
)
target_include_directories(oakrender PUBLIC
${OAK_REPO_ROOT}/engine/include
${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include
/opt/homebrew/include
/opt/homebrew/include/Imath
/opt/homebrew/include/OpenEXR
)
foreach(t oakrender oakgl oakgl2 oakvulkan)
if(TARGET ${t})
target_link_options(${t} PRIVATE
"-undefined" "dynamic_lookup"
)
endif()
endforeach()
target_include_directories(oaktask PUBLIC
${OAK_REPO_ROOT}/src/render/transition
${OAK_REPO_ROOT}/src/node/transition
${OAK_REPO_ROOT}/src/render/src
${OAK_REPO_ROOT}/src/node/src
)
target_link_options(oaktask PRIVATE
"-undefined" "dynamic_lookup"
)
target_link_libraries(oakrender PRIVATE
oaknode
oakcommon
oakundo
olivecore
ffmpeg_bridge
${OCIO_LIBRARIES}
${OIIO_LIBRARIES}
"-framework OpenGL"
"-framework CoreVideo"
"-framework Metal"
"-framework QuartzCore"
)
target_link_libraries(oaknode-gtest PRIVATE oakrender oaktimeline oakcodec)
# liboakrender references the oakengine_ipc_* C ABI (worker IPC) via
# dynamic_lookup; the real implementation (engine/src/capi/ipc.cpp) is still
# Qt-based, so test binaries link an inert shim instead.
target_sources(oaknode-gtest PRIVATE
${OAK_REPO_ROOT}/src/node/standalone/oakengine_ipc_shim.cpp
)
target_include_directories(oaknode-gtest PRIVATE
${OAK_REPO_ROOT}/engine/include
)
target_link_libraries(oaktask-gtest PRIVATE oakrender)
target_sources(oaktask-gtest PRIVATE
${OAK_REPO_ROOT}/src/node/standalone/oakengine_ipc_shim.cpp
)
target_include_directories(oaktask-gtest PRIVATE
${OAK_REPO_ROOT}/engine/include
)
# OFX typeinfo is referenced via dynamic_lookup; force-load the host
# support archive so RTTI resolves (same as oaknode's test wiring).
find_library(OAKNODE_OFX_HOST_ARCHIVE NAMES OfxHost
PATHS ${OAK_REPO_ROOT}/build/third_party/openfx/HostSupport
NO_DEFAULT_PATH)
if(NOT OAKNODE_OFX_HOST_ARCHIVE)
message(FATAL_ERROR
"libOfxHost.a not found; run the full-tree build once or set "
"OAKNODE_OFX_HOST_ARCHIVE")
endif()
target_link_options(oaktask-gtest PRIVATE
"-Wl,-force_load,${OAKNODE_OFX_HOST_ARCHIVE}")
target_link_options(oaknode-gtest PRIVATE
"-Wl,-force_load,${OAKNODE_OFX_HOST_ARCHIVE}")
# oaknode's Sequence::add_default_nodes() constructs
# TimelineAddTrackCommand, which lives in liboaktask; the two
# libraries resolve each other at runtime (dynamic_lookup).
target_link_libraries(oaknode-gtest PRIVATE oaktask)
+22
View File
@@ -0,0 +1,22 @@
find_package(GTest REQUIRED)
include(GoogleTest)
add_executable(oaktask-gtest
task_test.cpp
)
target_compile_definitions(oaktask-gtest PRIVATE
OAK_REPO_ROOT="${OAK_REPO_ROOT}")
target_link_libraries(oaktask-gtest PRIVATE
oaktask
oaktimeline
GTest::gtest
GTest::gtest_main
)
gtest_discover_tests(oaktask-gtest
DISCOVERY_MODE PRE_TEST
PROPERTIES ENVIRONMENT
"OCIO=${OAK_REPO_ROOT}/engine/render/ocioconf/config.ocio"
)
+316
View File
@@ -0,0 +1,316 @@
/***
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 <filesystem>
#include <vector>
#include <gtest/gtest.h>
#include "codec/conform.h"
#include "codec/decoder.h"
#include "codec/task.h"
#include "node/folder.h"
#include "node/project.h"
#include "task/manager.h"
#include "task/project.h"
#include "task/task.h"
#include "undo/undostack.h"
namespace
{
class OakTaskFixture : public ::testing::Test {
protected:
void SetUp() override
{
project_ = oaknode_project_init();
ASSERT_NE(project_, nullptr);
ASSERT_EQ(oaknode_project_initialize(project_), OAKNODE_OK);
}
void TearDown() override
{
oaknode_project_free(project_);
}
OakNodeProject *project_ = nullptr;
};
// ---- task family ----------------------------------------------------------
TEST_F(OakTaskFixture, TaskLifecycleSync)
{
oaktask_task_free(nullptr); // no-op on NULL
OakTaskTask *t = oaktask_create_project_save(project_, nullptr, 0);
ASSERT_NE(t, nullptr);
char title[128];
EXPECT_GT(oaktask_task_title(t, title, sizeof(title)), 0);
EXPECT_EQ(oaktask_task_title(nullptr, title, sizeof(title)),
OAKTASK_E_INVALID);
EXPECT_EQ(oaktask_task_is_finished(t), 0);
EXPECT_EQ(oaktask_task_is_finished(nullptr), OAKTASK_E_INVALID);
// Save to an override path in the temp dir
std::string path =
(std::filesystem::temp_directory_path() / "oaktask_save_test.ove")
.string();
oaktask_task_free(t);
t = oaktask_create_project_save(project_, path.c_str(), 0);
ASSERT_NE(t, nullptr);
EXPECT_EQ(oaktask_task_start_sync(t), 1);
EXPECT_EQ(oaktask_task_is_finished(t), 1);
EXPECT_TRUE(std::filesystem::exists(path));
char err[256];
EXPECT_GE(oaktask_task_error(t, err, sizeof(err)), 0);
oaktask_task_free(t);
EXPECT_EQ(oaktask_debug_alive_count(), 0);
std::filesystem::remove(path);
}
TEST_F(OakTaskFixture, LoadMissingFileFails)
{
OakTaskTask *t =
oaktask_create_project_load("/nonexistent/definitely-missing.ove");
ASSERT_NE(t, nullptr);
EXPECT_EQ(oaktask_task_start_sync(t), 0);
char err[256];
EXPECT_GT(oaktask_task_error(t, err, sizeof(err)), 0);
EXPECT_EQ(oaktask_load_take_project(t), nullptr);
oaktask_task_free(t);
EXPECT_EQ(oaktask_create_project_load(nullptr), nullptr);
}
TEST_F(OakTaskFixture, SaveLoadRoundTrip)
{
std::string path =
(std::filesystem::temp_directory_path() / "oaktask_roundtrip.ove")
.string();
OakTaskTask *save =
oaktask_create_project_save(project_, path.c_str(), 0);
ASSERT_NE(save, nullptr);
ASSERT_EQ(oaktask_task_start_sync(save), 1);
oaktask_task_free(save);
OakTaskTask *load = oaktask_create_project_load(path.c_str());
ASSERT_NE(load, nullptr);
ASSERT_EQ(oaktask_task_start_sync(load), 1);
OakNodeProject *loaded = oaktask_load_take_project(load);
ASSERT_NE(loaded, nullptr);
EXPECT_EQ(oaktask_load_take_project(load), nullptr);
oaknode_project_free(loaded);
oaktask_task_free(load);
std::filesystem::remove(path);
}
TEST_F(OakTaskFixture, ImportDemoFootage)
{
OakNodeFolder *folder = oaknode_folder_create(project_);
ASSERT_NE(folder, nullptr);
const char *urls[] = { OAK_REPO_ROOT "/tests/demo.mp4" };
OakTaskTask *t =
oaktask_create_project_import(folder, project_, urls, 1);
ASSERT_NE(t, nullptr);
ASSERT_EQ(oaktask_task_start_sync(t), 1);
if (oaktask_import_footage_count(t) == 0) {
// Footage probing still resolves through oaknode's codec
// transition stubs (module wiring milestone) - demo.mp4 reports
// invalid until then.
GTEST_SKIP()
<< "footage probe not wired to oakcodec yet (step 4)";
}
EXPECT_EQ(oaktask_import_footage_count(t), 1);
EXPECT_NE(oaktask_import_footage_at(t, 0), nullptr);
EXPECT_EQ(oaktask_import_footage_at(t, 5), nullptr);
EXPECT_EQ(oaktask_import_invalid_count(t), 0);
EXPECT_EQ(oaktask_import_invalid_at(t, 0, nullptr, 0),
OAKTASK_E_NOT_FOUND);
// The import command adds the footage to the folder; undo removes it
OakUndoCommand *cmd = oaktask_import_take_command(t);
ASSERT_NE(cmd, nullptr);
EXPECT_EQ(oaktask_import_take_command(t), nullptr);
EXPECT_EQ(oaknode_folder_child_count(folder), 0); // not yet redone
oakundo_command_redo_now(cmd);
EXPECT_EQ(oaknode_folder_child_count(folder), 1);
oakundo_command_undo_now(cmd);
EXPECT_EQ(oaknode_folder_child_count(folder), 0);
oakundo_command_free(cmd);
oaktask_task_free(t);
EXPECT_EQ(oaktask_create_project_import(nullptr, project_, urls, 1),
nullptr);
EXPECT_EQ(oaktask_import_footage_count(nullptr), OAKTASK_E_INVALID);
EXPECT_EQ(oaktask_debug_alive_count(), 0);
}
// ---- manager family -------------------------------------------------------
TEST(OakTaskManager, InitShutdownAndCodecSubmitter)
{
EXPECT_EQ(oaktask_manager_init(), OAKTASK_OK);
EXPECT_EQ(oaktask_manager_init(), OAKTASK_E_STATE);
EXPECT_EQ(oakcodec_task_submit_is_registered(), 1);
EXPECT_GE(oaktask_manager_count(), 0);
oaktask_manager_delete_finished();
EXPECT_EQ(oaktask_manager_at(0), nullptr);
oaktask_manager_shutdown();
EXPECT_EQ(oakcodec_task_submit_is_registered(), 0);
EXPECT_EQ(oaktask_manager_count(), OAKTASK_E_STATE);
}
TEST(OakTaskManager, AsyncStartAndSubscribe)
{
ASSERT_EQ(oaktask_manager_init(), OAKTASK_OK);
OakNodeProject *project = oaknode_project_init();
ASSERT_NE(project, nullptr);
ASSERT_EQ(oaknode_project_initialize(project), OAKNODE_OK);
std::string path =
(std::filesystem::temp_directory_path() / "oaktask_async.ove")
.string();
OakTaskTask *t = oaktask_create_project_save(project, path.c_str(), 0);
ASSERT_NE(t, nullptr);
struct Seen {
int started;
int finished;
double succeeded;
} seen = { 0, 0, 0.0 };
EXPECT_EQ(oaktask_task_subscribe(
t,
[](int event_id, double value, void *userdata) {
Seen *s = static_cast<Seen *>(userdata);
if (event_id == OAKTASK_EVENT_STARTED) {
s->started++;
} else if (event_id == OAKTASK_EVENT_FINISHED) {
s->finished++;
s->succeeded = value;
}
},
&seen),
0);
EXPECT_EQ(oaktask_task_subscribe(t, nullptr, nullptr),
OAKTASK_E_INVALID);
ASSERT_EQ(oaktask_task_start(t), OAKTASK_OK);
EXPECT_EQ(oaktask_task_start(t), OAKTASK_E_STATE);
EXPECT_EQ(oaktask_task_wait(t), OAKTASK_OK);
EXPECT_EQ(oaktask_task_is_finished(t), 1);
EXPECT_EQ(oaktask_task_succeeded(t), 1);
EXPECT_EQ(seen.started, 1);
EXPECT_EQ(seen.finished, 1);
EXPECT_EQ(seen.succeeded, 1.0);
EXPECT_TRUE(std::filesystem::exists(path));
std::filesystem::remove(path);
oaktask_task_free(t);
oaknode_project_free(project);
oaktask_manager_shutdown();
EXPECT_EQ(oaktask_debug_alive_count(), 0);
}
// ---- conform submitter end-to-end ------------------------------------------
TEST(OakTaskConform, SubmittedConformProducesPcm)
{
const std::string demo = OAK_REPO_ROOT "/tests/demo.mp4";
if (!std::filesystem::exists(demo)) {
GTEST_SKIP() << "demo.mp4 not available";
}
ASSERT_EQ(oaktask_manager_init(), OAKTASK_OK);
ASSERT_EQ(oakcodec_conform_create_instance(), OAKCODEC_OK);
// Probe demo.mp4 for an audio stream
OakDecoder probe = oakcodec_decoder_probe(demo.c_str());
ASSERT_NE(probe.ctx, nullptr);
if (oakcodec_decoder_probe_audio_stream_count(probe) <= 0) {
oakcodec_decoder_free(&probe);
oaktask_manager_shutdown();
GTEST_SKIP() << "demo.mp4 has no audio stream";
}
oakcodec_audio_stream_info info = {};
ASSERT_EQ(oakcodec_decoder_probe_get_audio_stream(probe, 0, &info),
OAKCODEC_OK);
oakcodec_decoder_free(&probe);
std::string cache_path =
(std::filesystem::temp_directory_path() / "oaktask_conform_cache")
.string();
std::error_code ec;
std::filesystem::remove_all(cache_path, ec);
std::filesystem::create_directories(cache_path, ec);
const int sample_format = 4; /* olive::core::SampleFormat::f32_p */
int state = oakcodec_conform_get_state(
cache_path.c_str(), demo.c_str(), info.stream_index,
info.sample_rate, info.channel_layout, sample_format, 1);
EXPECT_EQ(state, 0 /* OAKCODEC_CONFORM_EXISTS */);
int count = oakcodec_conform_filename_count(
cache_path.c_str(), demo.c_str(), info.stream_index,
info.sample_rate, info.channel_layout, sample_format);
EXPECT_GT(count, 0);
std::filesystem::remove_all(cache_path, ec);
oakcodec_conform_destroy_instance();
oaktask_manager_shutdown();
}
} // namespace