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:
@@ -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).
|
||||
|
||||
@@ -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()).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -5,3 +5,4 @@ add_subdirectory(render)
|
||||
add_subdirectory(codec)
|
||||
add_subdirectory(audio)
|
||||
add_subdirectory(timeline)
|
||||
add_subdirectory(task)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
add_subdirectory(src)
|
||||
add_subdirectory(c_api)
|
||||
|
||||
if(BUILD_TESTS)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
@@ -0,0 +1,5 @@
|
||||
target_sources(oaktask PRIVATE
|
||||
task.cpp
|
||||
manager.cpp
|
||||
project.cpp
|
||||
)
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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(¶ms);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 ¶ms)
|
||||
: 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 ¶ms);
|
||||
|
||||
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 ×) override;
|
||||
|
||||
virtual bool audio_downloaded(const TimeRange &range,
|
||||
const SampleBuffer &samples) override;
|
||||
|
||||
private:
|
||||
Project *project_;
|
||||
|
||||
Footage *footage_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // OAK_PRECACHETASK_H
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 ¶ms)
|
||||
: 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 ¶ms,
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 ¶ms);
|
||||
|
||||
/**
|
||||
* @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 ¶ms,
|
||||
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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user