engine: footage properties and project explorer migrate to the facade
- new facade API: video stream overrides (colorspace/range/interlacing/ premultiply), pixel aspect, image-sequence params, stream enable, source start time, and colorspace candidates - all undoable - project explorer proxy actions now run through FacadeProxyTask and the facade media-management functions (ProxyManager references in projectexplorer.cpp drop from 5 call sites to a comment) - footage properties dialog reads/writes through the facade; its two app-side undo command classes are gone - handle-model fix: the footage handle is a heap state object, not a plain pointer cast - oakengine_footage_borrow() wraps app-held Footage nodes correctly (nine UB reinterpret_casts caught by the DialogFootageProperties tests)
This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
|
||||
#include "export.h"
|
||||
#include "init.h"
|
||||
#include "node.h"
|
||||
#include "project.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
@@ -52,6 +53,10 @@ extern "C" {
|
||||
* oakengine_footage_free() on a borrowed handle only releases the handle
|
||||
* wrapper, never the node.
|
||||
*
|
||||
* - Borrowing (oakengine_footage_borrow()): wrap a Footage node the
|
||||
* caller already holds (e.g. selected in the UI) in the same borrowed
|
||||
* handle model, without probing or importing anything.
|
||||
*
|
||||
* Image sequences: the application's import asks the user whether numbered
|
||||
* stills form a sequence (EngineCore::confirm_image_sequence_handler). No
|
||||
* such handler exists behind this facade, so imported stills are always
|
||||
@@ -200,6 +205,19 @@ OAKENGINE_API int oakengine_footage_get_source_start_time(
|
||||
OAKENGINE_API OakEngineFootage *oakengine_project_import_footage(
|
||||
OakEngineProject *project, const char *path);
|
||||
|
||||
/**
|
||||
* @brief Wrap an existing project footage node in a BORROWED handle.
|
||||
*
|
||||
* For callers that already hold an engine node (e.g. the application
|
||||
* wrapping the Footage it has selected) and want to use the media
|
||||
* management / stream override functions below. `node` must be a footage
|
||||
* node (anything else yields NULL, see oakengine_footage_last_error()).
|
||||
* The handle borrows the node -- it becomes invalid when the node's
|
||||
* project is freed, and oakengine_footage_free() only releases the
|
||||
* wrapper.
|
||||
*/
|
||||
OAKENGINE_API OakEngineFootage *oakengine_footage_borrow(OakEngineNode *node);
|
||||
|
||||
/* ---- Media management: relink and proxies -----------------------------------
|
||||
*
|
||||
* These functions operate on BORROWED import handles (footage nodes living
|
||||
@@ -214,10 +232,10 @@ OAKENGINE_API OakEngineFootage *oakengine_project_import_footage(
|
||||
*
|
||||
* Calls Footage::set_filename(), which triggers the engine's full reprobe
|
||||
* cascade (Footage::clear(): streams, decoder link and the proxy state are
|
||||
* reset before re-probing). Fails with OAKENGINE_E_NOT_FOUND when
|
||||
* `new_path` does not exist, and with OAKENGINE_E_FAILED when the new file
|
||||
* cannot be probed as media. Not undoable (same as the application's
|
||||
* relink dialog).
|
||||
* reset before re-probing). The footage label is left unchanged. Fails
|
||||
* with OAKENGINE_E_NOT_FOUND when `new_path` does not exist, and with
|
||||
* OAKENGINE_E_FAILED when the new file cannot be probed as media. Not
|
||||
* undoable (same as the application's relink action).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_footage_relink(OakEngineFootage *footage,
|
||||
const char *new_path);
|
||||
@@ -229,8 +247,8 @@ OAKENGINE_API int oakengine_footage_relink(OakEngineFootage *footage,
|
||||
* Simplified version of the application's relink dialog matching: each
|
||||
* footage whose stored filename does not exist is relinked to
|
||||
* `search_dir`/<file name> when that file exists (exact file-name match,
|
||||
* no recursion). Returns the number of relinked items (>= 0) or a negative
|
||||
* code.
|
||||
* no recursion; labels are left unchanged). Returns the number of
|
||||
* relinked items (>= 0) or a negative code.
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_project_find_offline_footage(OakEngineProject *project,
|
||||
@@ -282,6 +300,123 @@ OAKENGINE_API int oakengine_footage_proxy_set_enabled(OakEngineFootage *self,
|
||||
OAKENGINE_API int oakengine_footage_proxy_get_path(OakEngineFootage *self,
|
||||
char *buf, int buf_size);
|
||||
|
||||
/* ---- Stream parameter overrides (footage properties) ------------------------
|
||||
*
|
||||
* Per-stream overrides shown by the application's footage properties
|
||||
* dialog (app/dialog/footageproperties). All setters here are UNDOABLE
|
||||
* (pushed onto the global undo stack as one command each, matching the
|
||||
* dialog's undoable commands; the engine stores these as plain
|
||||
* VideoParams/AudioParams values on the footage node). Getters are direct
|
||||
* reads. Everything in this section requires a borrowed import handle
|
||||
* (probe handles are rejected with OAKENGINE_E_INVALID), and stream
|
||||
* indexes are the index within the stream's own type (0-based).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Read a video stream's override parameters.
|
||||
*
|
||||
* Fills the current values (VideoParams): colorspace name (buf/size
|
||||
* convention, may be NULL to skip), color_range
|
||||
* (VideoParams::ColorRange), interlacing (VideoParams::Interlacing) and
|
||||
* premultiplied-alpha flag. Any output pointer may be NULL. Returns
|
||||
* OAKENGINE_E_NOT_FOUND for an out-of-range stream index.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_footage_get_video_stream_overrides(
|
||||
OakEngineFootage *self, int stream_index, char *colorspace_buf,
|
||||
int colorspace_size, int *color_range, int *interlacing,
|
||||
int *premultiplied);
|
||||
|
||||
/**
|
||||
* @brief Write a video stream's overrides (undoable).
|
||||
*
|
||||
* Pass NULL for `colorspace` and -1 for any int field to leave that field
|
||||
* unchanged. An empty colorspace string ("") clears the override back to
|
||||
* the project's default input color space.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_footage_set_video_stream_overrides(
|
||||
OakEngineFootage *self, int stream_index, const char *colorspace,
|
||||
int color_range, int interlacing, int premultiplied);
|
||||
|
||||
/**
|
||||
* @brief Video stream pixel aspect ratio (num/den).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_footage_get_pixel_aspect(OakEngineFootage *self,
|
||||
int stream_index, int *num,
|
||||
int *den);
|
||||
|
||||
/**
|
||||
* @brief Set the video stream pixel aspect ratio (undoable). Both values
|
||||
* must be > 0.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_footage_set_pixel_aspect(OakEngineFootage *self,
|
||||
int stream_index, int num,
|
||||
int den);
|
||||
|
||||
/**
|
||||
* @brief Image-sequence parameters of a video stream: start index,
|
||||
* duration in frames and frame rate (num/den). Returns
|
||||
* OAKENGINE_E_NOT_FOUND for an out-of-range index.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_footage_get_image_sequence_params(
|
||||
OakEngineFootage *self, int stream_index, int64_t *start_index,
|
||||
int64_t *duration, int *frame_rate_num, int *frame_rate_den);
|
||||
|
||||
/**
|
||||
* @brief Set image-sequence parameters (undoable). `duration` must be > 0
|
||||
* and the frame rate positive.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_footage_set_image_sequence_params(
|
||||
OakEngineFootage *self, int stream_index, int64_t start_index,
|
||||
int64_t duration, int frame_rate_num, int frame_rate_den);
|
||||
|
||||
/**
|
||||
* @brief 1 if a stream is enabled (VideoParams/AudioParams/SubtitleParams
|
||||
* ::enabled()). `track_type` is an OAKENGINE_TRACK_TYPE_* value;
|
||||
* OAKENGINE_E_NOT_FOUND for an out-of-range index.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_footage_get_stream_enabled(OakEngineFootage *self,
|
||||
int track_type,
|
||||
int index);
|
||||
|
||||
/**
|
||||
* @brief Enable or disable a stream (undoable).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_footage_set_stream_enabled(OakEngineFootage *self,
|
||||
int track_type,
|
||||
int index, int enabled);
|
||||
|
||||
/**
|
||||
* @brief Set or clear the source start time (undoable; mirrors the
|
||||
* dialog's FootageSetSourceStartTimeCommand with source "manual").
|
||||
* `enabled` == 0 clears it; otherwise the time is num/den seconds.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_footage_set_source_start_time(
|
||||
OakEngineFootage *self, int enabled, int64_t num, int64_t den);
|
||||
|
||||
/**
|
||||
* @brief The detection source of the source start time (e.g. "manual" or
|
||||
* the metadata field it was read from; empty when unset). buf/size
|
||||
* convention.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_footage_get_source_start_time_source(
|
||||
OakEngineFootage *self, char *buf, int buf_size);
|
||||
|
||||
/* ---- Colorspace candidates --------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Number of color spaces in the project's color config (the same
|
||||
* list the footage properties dialog's color space dropdown shows).
|
||||
*/
|
||||
OAKENGINE_API int
|
||||
oakengine_footage_colorspace_count(const OakEngineFootage *self);
|
||||
|
||||
/**
|
||||
* @brief Color space name at `index` in the project color config
|
||||
* (buf/size convention). OAKENGINE_E_NOT_FOUND for an out-of-range index.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_footage_colorspace_at(
|
||||
const OakEngineFootage *self, int index, char *buf, int buf_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+512
-5
@@ -149,6 +149,183 @@ olive::Footage *borrowed_node(OakEngineFootage *self)
|
||||
return node;
|
||||
}
|
||||
|
||||
// Push an undoable command onto the global undo stack when the engine is
|
||||
// initialized, otherwise execute it directly.
|
||||
void push_or_run(olive::UndoCommand *command, const QString &name)
|
||||
{
|
||||
if (olive::EngineCore::instance()) {
|
||||
olive::EngineCore::instance()->undo_stack()->push(command, name);
|
||||
} else {
|
||||
command->redo_now();
|
||||
delete command;
|
||||
}
|
||||
}
|
||||
|
||||
// Undo commands for footage stream overrides. The engine has no undo
|
||||
// commands for these (the application's footage properties dialog carries
|
||||
// them at the app layer), so the facade carries read-modify-write
|
||||
// equivalents with the same semantics.
|
||||
class FootageVideoParamsCommand : public olive::UndoCommand {
|
||||
public:
|
||||
FootageVideoParamsCommand(olive::Footage *footage, int index,
|
||||
const olive::VideoParams ¶ms)
|
||||
: footage_(footage)
|
||||
, index_(index)
|
||||
, new_params_(params)
|
||||
{
|
||||
}
|
||||
|
||||
virtual olive::Project *get_relevant_project() const override
|
||||
{
|
||||
return footage_->project();
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void redo() override
|
||||
{
|
||||
old_params_ = footage_->get_video_params(index_);
|
||||
footage_->set_video_params(new_params_, index_);
|
||||
}
|
||||
|
||||
virtual void undo() override
|
||||
{
|
||||
footage_->set_video_params(old_params_, index_);
|
||||
}
|
||||
|
||||
private:
|
||||
olive::Footage *footage_;
|
||||
int index_;
|
||||
olive::VideoParams old_params_;
|
||||
olive::VideoParams new_params_;
|
||||
};
|
||||
|
||||
class FootageAudioParamsCommand : public olive::UndoCommand {
|
||||
public:
|
||||
FootageAudioParamsCommand(olive::Footage *footage, int index,
|
||||
const olive::AudioParams ¶ms)
|
||||
: footage_(footage)
|
||||
, index_(index)
|
||||
, new_params_(params)
|
||||
{
|
||||
}
|
||||
|
||||
virtual olive::Project *get_relevant_project() const override
|
||||
{
|
||||
return footage_->project();
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void redo() override
|
||||
{
|
||||
old_params_ = footage_->get_audio_params(index_);
|
||||
footage_->set_audio_params(new_params_, index_);
|
||||
}
|
||||
|
||||
virtual void undo() override
|
||||
{
|
||||
footage_->set_audio_params(old_params_, index_);
|
||||
}
|
||||
|
||||
private:
|
||||
olive::Footage *footage_;
|
||||
int index_;
|
||||
olive::AudioParams old_params_;
|
||||
olive::AudioParams new_params_;
|
||||
};
|
||||
|
||||
class FootageSubtitleParamsCommand : public olive::UndoCommand {
|
||||
public:
|
||||
FootageSubtitleParamsCommand(olive::Footage *footage, int index,
|
||||
const olive::SubtitleParams ¶ms)
|
||||
: footage_(footage)
|
||||
, index_(index)
|
||||
, new_params_(params)
|
||||
{
|
||||
}
|
||||
|
||||
virtual olive::Project *get_relevant_project() const override
|
||||
{
|
||||
return footage_->project();
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void redo() override
|
||||
{
|
||||
old_params_ = footage_->get_subtitle_params(index_);
|
||||
footage_->set_subtitle_params(new_params_, index_);
|
||||
}
|
||||
|
||||
virtual void undo() override
|
||||
{
|
||||
footage_->set_subtitle_params(old_params_, index_);
|
||||
}
|
||||
|
||||
private:
|
||||
olive::Footage *footage_;
|
||||
int index_;
|
||||
olive::SubtitleParams old_params_;
|
||||
olive::SubtitleParams new_params_;
|
||||
};
|
||||
|
||||
class FootageSourceStartTimeCommand : public olive::UndoCommand {
|
||||
public:
|
||||
FootageSourceStartTimeCommand(olive::Footage *footage, bool enabled,
|
||||
const olive::Rational &time)
|
||||
: footage_(footage)
|
||||
, new_enabled_(enabled)
|
||||
, new_time_(time)
|
||||
{
|
||||
}
|
||||
|
||||
virtual olive::Project *get_relevant_project() const override
|
||||
{
|
||||
return footage_->project();
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void redo() override
|
||||
{
|
||||
old_enabled_ = footage_->has_source_start_time();
|
||||
old_time_ = footage_->source_start_time();
|
||||
old_source_ = footage_->source_start_time_source();
|
||||
|
||||
if (new_enabled_) {
|
||||
footage_->set_source_start_time(new_time_,
|
||||
QStringLiteral("manual"));
|
||||
} else {
|
||||
footage_->clear_source_start_time();
|
||||
}
|
||||
}
|
||||
|
||||
virtual void undo() override
|
||||
{
|
||||
if (old_enabled_) {
|
||||
footage_->set_source_start_time(old_time_, old_source_);
|
||||
} else {
|
||||
footage_->clear_source_start_time();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
olive::Footage *footage_;
|
||||
bool new_enabled_;
|
||||
olive::Rational new_time_;
|
||||
bool old_enabled_ = false;
|
||||
olive::Rational old_time_;
|
||||
QString old_source_;
|
||||
};
|
||||
|
||||
// Stream access by facade track type; returns false for unknown indexes.
|
||||
bool video_stream_at(const olive::Footage *f, int index,
|
||||
olive::VideoParams *out)
|
||||
{
|
||||
if (index < 0 || index >= f->get_video_stream_count()) {
|
||||
return false;
|
||||
}
|
||||
*out = f->get_video_params(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Internal cross-family accessor (not part of the public C ABI): returns
|
||||
@@ -383,6 +560,21 @@ OakEngineFootage *oakengine_project_import_footage(OakEngineProject *project,
|
||||
return wrap(state);
|
||||
}
|
||||
|
||||
OakEngineFootage *oakengine_footage_borrow(OakEngineNode *node)
|
||||
{
|
||||
set_error(QString());
|
||||
auto *footage =
|
||||
dynamic_cast<olive::Footage *>(reinterpret_cast<olive::Node *>(node));
|
||||
if (!footage) {
|
||||
set_error(QStringLiteral("node is not a footage node"));
|
||||
return nullptr;
|
||||
}
|
||||
auto *state = new OakEngineFootageState();
|
||||
state->borrowed = true;
|
||||
state->node = footage;
|
||||
return wrap(state);
|
||||
}
|
||||
|
||||
int oakengine_footage_relink(OakEngineFootage *footage, const char *new_path)
|
||||
{
|
||||
set_error(QString());
|
||||
@@ -400,10 +592,11 @@ int oakengine_footage_relink(OakEngineFootage *footage, const char *new_path)
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
|
||||
// Same as the application's relink dialog: set_filename() triggers
|
||||
// clear() (streams, decoder and proxy state reset) and a reprobe.
|
||||
// Same as the application's relink action: set_filename() triggers
|
||||
// clear() (streams, decoder and proxy state reset) and a reprobe. The
|
||||
// label is intentionally left alone (relinking changes the path, not
|
||||
// the user's naming).
|
||||
node->set_filename(path);
|
||||
node->set_label(QFileInfo(path).fileName());
|
||||
if (!node->is_valid()) {
|
||||
set_error(QStringLiteral("failed to probe \"%1\" as media")
|
||||
.arg(path));
|
||||
@@ -438,12 +631,12 @@ int oakengine_project_find_offline_footage(OakEngineProject *project,
|
||||
if (filename.isEmpty() || QFileInfo::exists(filename)) {
|
||||
continue;
|
||||
}
|
||||
// Exact file-name match in the search directory (no recursion).
|
||||
// Exact file-name match in the search directory (no recursion). The
|
||||
// label is intentionally left alone.
|
||||
const QString candidate =
|
||||
dir.filePath(QFileInfo(filename).fileName());
|
||||
if (QFileInfo::exists(candidate)) {
|
||||
footage->set_filename(candidate);
|
||||
footage->set_label(QFileInfo(candidate).fileName());
|
||||
if (footage->is_valid()) {
|
||||
relinked++;
|
||||
}
|
||||
@@ -575,4 +768,318 @@ int oakengine_footage_proxy_get_path(OakEngineFootage *self, char *buf,
|
||||
return string_to_buf(impl(self)->node->proxy_path(), buf, buf_size);
|
||||
}
|
||||
|
||||
/* ---- Stream parameter overrides --------------------------------------------- */
|
||||
|
||||
int oakengine_footage_get_video_stream_overrides(
|
||||
OakEngineFootage *self, int stream_index, char *colorspace_buf,
|
||||
int colorspace_size, int *color_range, int *interlacing,
|
||||
int *premultiplied)
|
||||
{
|
||||
set_error(QString());
|
||||
olive::Footage *node = borrowed_node(self);
|
||||
if (!node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::VideoParams vp;
|
||||
if (!video_stream_at(node, stream_index, &vp)) {
|
||||
set_error(QStringLiteral("no video stream at index %1")
|
||||
.arg(stream_index));
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
if (colorspace_buf) {
|
||||
string_to_buf(vp.colorspace(), colorspace_buf, colorspace_size);
|
||||
}
|
||||
if (color_range) {
|
||||
*color_range = int(vp.color_range());
|
||||
}
|
||||
if (interlacing) {
|
||||
*interlacing = int(vp.interlacing());
|
||||
}
|
||||
if (premultiplied) {
|
||||
*premultiplied = vp.premultiplied_alpha() ? 1 : 0;
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_footage_set_video_stream_overrides(
|
||||
OakEngineFootage *self, int stream_index, const char *colorspace,
|
||||
int color_range, int interlacing, int premultiplied)
|
||||
{
|
||||
set_error(QString());
|
||||
olive::Footage *node = borrowed_node(self);
|
||||
if (!node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::VideoParams vp;
|
||||
if (!video_stream_at(node, stream_index, &vp)) {
|
||||
set_error(QStringLiteral("no video stream at index %1")
|
||||
.arg(stream_index));
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
if (colorspace) {
|
||||
vp.set_colorspace(QString::fromUtf8(colorspace));
|
||||
}
|
||||
if (color_range >= 0) {
|
||||
vp.set_color_range(
|
||||
static_cast<olive::VideoParams::ColorRange>(color_range));
|
||||
}
|
||||
if (interlacing >= 0) {
|
||||
vp.set_interlacing(
|
||||
static_cast<olive::VideoParams::Interlacing>(interlacing));
|
||||
}
|
||||
if (premultiplied >= 0) {
|
||||
vp.set_premultiplied_alpha(premultiplied != 0);
|
||||
}
|
||||
push_or_run(new FootageVideoParamsCommand(node, stream_index, vp),
|
||||
QStringLiteral("Set Video Stream Overrides"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_footage_get_pixel_aspect(OakEngineFootage *self,
|
||||
int stream_index, int *num, int *den)
|
||||
{
|
||||
set_error(QString());
|
||||
olive::Footage *node = borrowed_node(self);
|
||||
if (!node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::VideoParams vp;
|
||||
if (!video_stream_at(node, stream_index, &vp)) {
|
||||
set_error(QStringLiteral("no video stream at index %1")
|
||||
.arg(stream_index));
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
if (num) {
|
||||
*num = vp.pixel_aspect_ratio().numerator();
|
||||
}
|
||||
if (den) {
|
||||
*den = vp.pixel_aspect_ratio().denominator();
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_footage_set_pixel_aspect(OakEngineFootage *self,
|
||||
int stream_index, int num, int den)
|
||||
{
|
||||
set_error(QString());
|
||||
olive::Footage *node = borrowed_node(self);
|
||||
if (!node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (num <= 0 || den <= 0) {
|
||||
set_error(QStringLiteral("invalid pixel aspect ratio %1/%2")
|
||||
.arg(num)
|
||||
.arg(den));
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::VideoParams vp;
|
||||
if (!video_stream_at(node, stream_index, &vp)) {
|
||||
set_error(QStringLiteral("no video stream at index %1")
|
||||
.arg(stream_index));
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
vp.set_pixel_aspect_ratio(olive::Rational(num, den));
|
||||
push_or_run(new FootageVideoParamsCommand(node, stream_index, vp),
|
||||
QStringLiteral("Set Pixel Aspect Ratio"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_footage_get_image_sequence_params(
|
||||
OakEngineFootage *self, int stream_index, int64_t *start_index,
|
||||
int64_t *duration, int *frame_rate_num, int *frame_rate_den)
|
||||
{
|
||||
set_error(QString());
|
||||
olive::Footage *node = borrowed_node(self);
|
||||
if (!node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::VideoParams vp;
|
||||
if (!video_stream_at(node, stream_index, &vp)) {
|
||||
set_error(QStringLiteral("no video stream at index %1")
|
||||
.arg(stream_index));
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
if (start_index) {
|
||||
*start_index = vp.start_time();
|
||||
}
|
||||
if (duration) {
|
||||
*duration = vp.duration();
|
||||
}
|
||||
if (frame_rate_num) {
|
||||
*frame_rate_num = vp.frame_rate().numerator();
|
||||
}
|
||||
if (frame_rate_den) {
|
||||
*frame_rate_den = vp.frame_rate().denominator();
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_footage_set_image_sequence_params(
|
||||
OakEngineFootage *self, int stream_index, int64_t start_index,
|
||||
int64_t duration, int frame_rate_num, int frame_rate_den)
|
||||
{
|
||||
set_error(QString());
|
||||
olive::Footage *node = borrowed_node(self);
|
||||
if (!node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (start_index < 0 || duration <= 0 || frame_rate_num <= 0 ||
|
||||
frame_rate_den <= 0) {
|
||||
set_error(QStringLiteral(
|
||||
"invalid image sequence parameters (need start >= 0, duration > "
|
||||
"0, positive frame rate)"));
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
olive::VideoParams vp;
|
||||
if (!video_stream_at(node, stream_index, &vp)) {
|
||||
set_error(QStringLiteral("no video stream at index %1")
|
||||
.arg(stream_index));
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
vp.set_start_time(start_index);
|
||||
vp.set_duration(duration);
|
||||
const olive::Rational frame_rate(frame_rate_num, frame_rate_den);
|
||||
vp.set_frame_rate(frame_rate);
|
||||
vp.set_time_base(frame_rate.flipped());
|
||||
push_or_run(new FootageVideoParamsCommand(node, stream_index, vp),
|
||||
QStringLiteral("Set Image Sequence Parameters"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_footage_get_stream_enabled(OakEngineFootage *self,
|
||||
int track_type, int index)
|
||||
{
|
||||
if (!self || !impl(self)->node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const olive::Footage *node = impl(self)->node;
|
||||
switch (track_type) {
|
||||
case 0:
|
||||
if (index >= 0 && index < node->get_video_stream_count()) {
|
||||
return node->get_video_params(index).enabled() ? 1 : 0;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (index >= 0 && index < node->get_audio_stream_count()) {
|
||||
return node->get_audio_params(index).enabled() ? 1 : 0;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (index >= 0 && index < node->get_subtitle_stream_count()) {
|
||||
return node->get_subtitle_params(index).enabled() ? 1 : 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
|
||||
int oakengine_footage_set_stream_enabled(OakEngineFootage *self,
|
||||
int track_type, int index,
|
||||
int enabled)
|
||||
{
|
||||
set_error(QString());
|
||||
olive::Footage *node = borrowed_node(self);
|
||||
if (!node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
switch (track_type) {
|
||||
case 0:
|
||||
if (index >= 0 && index < node->get_video_stream_count()) {
|
||||
olive::VideoParams vp = node->get_video_params(index);
|
||||
vp.set_enabled(enabled != 0);
|
||||
push_or_run(
|
||||
new FootageVideoParamsCommand(node, index, vp),
|
||||
QStringLiteral("Set Stream Enabled"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (index >= 0 && index < node->get_audio_stream_count()) {
|
||||
olive::AudioParams ap = node->get_audio_params(index);
|
||||
ap.set_enabled(enabled != 0);
|
||||
push_or_run(
|
||||
new FootageAudioParamsCommand(node, index, ap),
|
||||
QStringLiteral("Set Stream Enabled"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (index >= 0 && index < node->get_subtitle_stream_count()) {
|
||||
olive::SubtitleParams sp = node->get_subtitle_params(index);
|
||||
sp.set_enabled(enabled != 0);
|
||||
push_or_run(
|
||||
new FootageSubtitleParamsCommand(node, index, sp),
|
||||
QStringLiteral("Set Stream Enabled"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
set_error(QStringLiteral("unknown track type %1").arg(track_type));
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
set_error(QStringLiteral("no stream of type %1 at index %2")
|
||||
.arg(track_type)
|
||||
.arg(index));
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
|
||||
int oakengine_footage_set_source_start_time(OakEngineFootage *self,
|
||||
int enabled, int64_t num,
|
||||
int64_t den)
|
||||
{
|
||||
set_error(QString());
|
||||
olive::Footage *node = borrowed_node(self);
|
||||
if (!node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (enabled && den == 0) {
|
||||
set_error(QStringLiteral("invalid time denominator 0"));
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
push_or_run(new FootageSourceStartTimeCommand(
|
||||
node, enabled != 0,
|
||||
olive::Rational::from_double(
|
||||
den != 0 ? double(num) / double(den) : 0.0)),
|
||||
QStringLiteral("Set Source Start Time"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_footage_get_source_start_time_source(OakEngineFootage *self,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
if (!self || !impl(self)->node) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
return string_to_buf(impl(self)->node->source_start_time_source(), buf,
|
||||
buf_size);
|
||||
}
|
||||
|
||||
/* ---- Colorspace candidates ----------------------------------------------------- */
|
||||
|
||||
int oakengine_footage_colorspace_count(const OakEngineFootage *self)
|
||||
{
|
||||
if (!self || !impl(self)->node || !impl(self)->node->project()) {
|
||||
return 0;
|
||||
}
|
||||
return impl(self)->node->project()->color_manager()->get_config()
|
||||
->getNumColorSpaces();
|
||||
}
|
||||
|
||||
int oakengine_footage_colorspace_at(const OakEngineFootage *self, int index,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
if (!self || !impl(self)->node || !impl(self)->node->project()) {
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const ocio::ConstConfigRcPtr config =
|
||||
impl(self)->node->project()->color_manager()->get_config();
|
||||
if (index < 0 || index >= config->getNumColorSpaces()) {
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
return string_to_buf(config->getColorSpaceNameByIndex(index), buf,
|
||||
buf_size);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -217,6 +217,10 @@ static void test_failures(void)
|
||||
assert(oakengine_project_import_footage(NULL, path) == NULL);
|
||||
oakengine_project_free(project);
|
||||
|
||||
// Borrowing nothing yields no handle.
|
||||
assert(oakengine_footage_borrow(NULL) == NULL);
|
||||
assert(oakengine_footage_last_error(err, sizeof(err)) > 0);
|
||||
|
||||
// NULL safety.
|
||||
oakengine_footage_free(NULL);
|
||||
assert(oakengine_footage_get_decoder_name(NULL, err, sizeof(err)) ==
|
||||
@@ -409,6 +413,181 @@ static void test_proxy(void)
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
static void test_stream_overrides(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
char path[4096];
|
||||
demo_path(path, sizeof(path));
|
||||
OakEngineFootage *f = oakengine_project_import_footage(project, path);
|
||||
assert(f != NULL);
|
||||
|
||||
char cs[128];
|
||||
int range = -1, interlace = -1, premult = -1;
|
||||
|
||||
// Defaults from the probe.
|
||||
assert(oakengine_footage_get_video_stream_overrides(
|
||||
f, 0, cs, sizeof(cs), &range, &interlace, &premult) ==
|
||||
OAKENGINE_OK);
|
||||
assert(range == 0); // limited
|
||||
assert(interlace == 0); // progressive
|
||||
assert(premult == 0);
|
||||
|
||||
// Full override write + read-back + undo/redo.
|
||||
assert(oakengine_footage_set_video_stream_overrides(
|
||||
f, 0, "Rec.709 OETF", 1, 1, 1) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_get_video_stream_overrides(
|
||||
f, 0, cs, sizeof(cs), &range, &interlace, &premult) ==
|
||||
OAKENGINE_OK);
|
||||
assert(strcmp(cs, "Rec.709 OETF") == 0);
|
||||
assert(range == 1 && interlace == 1 && premult == 1);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_get_video_stream_overrides(
|
||||
f, 0, cs, sizeof(cs), &range, &interlace, &premult) ==
|
||||
OAKENGINE_OK);
|
||||
assert(range == 0 && interlace == 0 && premult == 0);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_get_video_stream_overrides(
|
||||
f, 0, cs, sizeof(cs), &range, &interlace, &premult) ==
|
||||
OAKENGINE_OK);
|
||||
assert(range == 1 && interlace == 1 && premult == 1);
|
||||
|
||||
// Partial write: NULL/-1 leaves fields alone.
|
||||
assert(oakengine_footage_set_video_stream_overrides(f, 0, NULL, 0, -1,
|
||||
-1) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_get_video_stream_overrides(
|
||||
f, 0, cs, sizeof(cs), &range, &interlace, &premult) ==
|
||||
OAKENGINE_OK);
|
||||
assert(range == 0 && interlace == 1 && premult == 1);
|
||||
|
||||
// Pixel aspect ratio.
|
||||
int num = 0, den = 0;
|
||||
assert(oakengine_footage_get_pixel_aspect(f, 0, &num, &den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(num == 1 && den == 1);
|
||||
assert(oakengine_footage_set_pixel_aspect(f, 0, 4, 3) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_get_pixel_aspect(f, 0, &num, &den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(num == 4 && den == 3);
|
||||
assert(oakengine_footage_set_pixel_aspect(f, 0, 0, 3) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_get_pixel_aspect(f, 0, &num, &den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(num == 1 && den == 1);
|
||||
|
||||
// Image-sequence parameters round-trip (stored even for non-sequence
|
||||
// footage; the dialog shows them only for image sequences).
|
||||
int64_t start = -1, dur = -1;
|
||||
assert(oakengine_footage_get_image_sequence_params(f, 0, &start, &dur,
|
||||
&num, &den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_footage_set_image_sequence_params(f, 0, 10, 100, 25,
|
||||
1) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_get_image_sequence_params(f, 0, &start, &dur,
|
||||
&num, &den) ==
|
||||
OAKENGINE_OK);
|
||||
assert(start == 10 && dur == 100 && num == 25 && den == 1);
|
||||
assert(oakengine_footage_set_image_sequence_params(f, 0, 0, 0, 25, 1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
|
||||
// Stream enable toggles.
|
||||
assert(oakengine_footage_get_stream_enabled(f, 0, 0) == 1);
|
||||
assert(oakengine_footage_set_stream_enabled(f, 0, 0, 0) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_get_stream_enabled(f, 0, 0) == 0);
|
||||
assert(oakengine_footage_set_stream_enabled(f, 0, 0, 1) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_get_stream_enabled(f, 0, 0) == 1);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_get_stream_enabled(f, 0, 0) == 0);
|
||||
assert(oakengine_footage_get_stream_enabled(f, 1, 0) == 1);
|
||||
assert(oakengine_footage_get_stream_enabled(f, 0, 99) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_footage_set_stream_enabled(f, 9, 0, 1) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// Source start time set/clear with source label.
|
||||
int snum = 0, sden = 0;
|
||||
assert(oakengine_footage_get_source_start_time(f, &snum, &sden) == 1);
|
||||
char source[64];
|
||||
assert(oakengine_footage_set_source_start_time(f, 1, 42, 1) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_footage_get_source_start_time(f, &snum, &sden) == 1);
|
||||
assert(snum == 42 && sden == 1);
|
||||
assert(oakengine_footage_get_source_start_time_source(f, source,
|
||||
sizeof(source)) > 0);
|
||||
assert(strcmp(source, "manual") == 0);
|
||||
assert(oakengine_footage_set_source_start_time(f, 0, 0, 1) ==
|
||||
OAKENGINE_OK);
|
||||
assert(oakengine_footage_get_source_start_time(f, &snum, &sden) == 0);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_footage_get_source_start_time(f, &snum, &sden) == 1);
|
||||
assert(snum == 42);
|
||||
|
||||
// Out-of-range and NULL.
|
||||
assert(oakengine_footage_get_video_stream_overrides(
|
||||
f, 9, cs, sizeof(cs), &range, &interlace, &premult) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_footage_set_video_stream_overrides(f, 9, "x", 0, 0,
|
||||
0) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_footage_get_pixel_aspect(f, 9, &num, &den) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_footage_get_image_sequence_params(f, 9, &start, &dur,
|
||||
&num, &den) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_footage_get_video_stream_overrides(
|
||||
NULL, 0, cs, sizeof(cs), &range, &interlace, &premult) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
OakEngineFootage *probed = oakengine_footage_probe(path);
|
||||
assert(probed != NULL);
|
||||
assert(oakengine_footage_set_video_stream_overrides(probed, 0, "x", 0,
|
||||
0, 0) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_footage_colorspace_count(probed) == 0);
|
||||
oakengine_footage_free(probed);
|
||||
|
||||
oakengine_footage_free(f);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
static void test_colorspace_candidates(void)
|
||||
{
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
char path[4096];
|
||||
demo_path(path, sizeof(path));
|
||||
OakEngineFootage *f = oakengine_project_import_footage(project, path);
|
||||
assert(f != NULL);
|
||||
|
||||
const int count = oakengine_footage_colorspace_count(f);
|
||||
assert(count > 0);
|
||||
char name[128];
|
||||
int found_rec709 = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
assert(oakengine_footage_colorspace_at(f, i, name, sizeof(name)) > 0);
|
||||
assert(strlen(name) > 0);
|
||||
if (strcmp(name, "Rec.709 OETF") == 0) {
|
||||
found_rec709 = 1;
|
||||
}
|
||||
}
|
||||
assert(found_rec709 == 1);
|
||||
assert(oakengine_footage_colorspace_at(f, count, name, sizeof(name)) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_footage_colorspace_at(f, -1, name, sizeof(name)) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_footage_colorspace_at(NULL, 0, name, sizeof(name)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_footage_colorspace_count(NULL) == 0);
|
||||
|
||||
oakengine_footage_free(f);
|
||||
oakengine_project_free(project);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
make_tmpdir();
|
||||
@@ -429,6 +608,8 @@ int main(void)
|
||||
test_relink();
|
||||
test_find_offline();
|
||||
test_proxy();
|
||||
test_stream_overrides();
|
||||
test_colorspace_candidates();
|
||||
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user