feat(rust): oakcodec/oaktask/oakplugin crates + oakotio + node/render/storage skeletons
- oakcodec: full crate incl. real FFmpeg decode/encode via ffmpeg-next (162 tests, 84.4% cov); new oakcodec_encoding_* metadata family (include/codec/format.h, C++ + Rust sides) - oaktask: manager/tasks/project load-save incl. OTIO via oakotio (82 tests, 86.4% cov); concurrent render loop with reorder buffer - oakplugin: M11 phase 1+2 — self-contained OFX host, GL path, ofxColour, pluginrenderer absorbed as render_driver (99 tests, 81.7% cov); instance.h additions documented - oakotio: native serde-based OTIO read/write (24 tests) - oaknode gap fill: dragger/keyframe-helper/multicam C ABI families (113/113 gtest); fixes a latent NodeInputDragger segfault - oaknode Rust skeleton: 43 built-in node type declarations - oakrender/oakstorage: declaration skeletons (implementation pending) - notes.md: gap analysis + tech-debt ledger
This commit is contained in:
@@ -109,3 +109,7 @@ act
|
||||
|
||||
# Local OpenTimelineIO build (see docs/build.md)
|
||||
otio-install/
|
||||
|
||||
# Rust
|
||||
**/target/
|
||||
tarpaulin-out/
|
||||
|
||||
@@ -505,12 +505,52 @@ image/ffmpeg-next 全套。
|
||||
|
||||
## 技术债登记(2026-08-09)
|
||||
|
||||
- **oaktask 渲染循环同步化**:src/task/rust/src/render.rs 目前是
|
||||
"一帧一 ticket、wait 到底"的同步循环(C++ 原版是最多
|
||||
hardware_concurrency 个 ticket 并发 + 完成队列 + condvar)。
|
||||
可观察契约不变但导出/预缓存吞吐下降。修复方案:oakrender
|
||||
ticket arena 本就支持多 ticket 在飞 + 回调,改为 N 并发 +
|
||||
按序交付,并发度参数照 C++。待 render crate 落定后立即派代理
|
||||
补上(测试已就绪)。
|
||||
- **oaktask 渲染循环同步化 ✅(2026-08-09 修复)**:src/task/rust/src/render.rs
|
||||
的同步循环("一帧一 ticket、wait 到底")已改为并发循环,与 C++
|
||||
原版一致:最多 `max_inflight`(默认 `available_parallelism`,照 C++
|
||||
`max(1, hardware_concurrency)`)个 ticket 在飞,ticket 完成回调
|
||||
(`oakrender_ticket_finished_fn`)把完成的 ticket 推进完成队列并唤醒
|
||||
渲染线程(队列 + condvar);乱序完成经 reorder buffer 按时间戳序
|
||||
(音频先、帧按时间升序)交付给 behavior 钩子;取消/钩子错误会
|
||||
cancel+wait 全部在飞 ticket(其完成回调仍然恰好触发一次)后再返回。
|
||||
可观察契约不变。配套:tests/common/mod.rs 的 ticket stub 升级为
|
||||
模拟 ticket arena(按提交序分配 id、可脚本化乱序完成 `stub_complete`、
|
||||
DEFER 模式、取消原子会完成在飞 ticket);新增
|
||||
tests/render_loop_test.rs(乱序交付、音频优先、取消 drain、进度单调、
|
||||
错误停止派发、窗口化)。bridge/render.rs 的
|
||||
`oakrender_ticket_finished_fn` 由 3 参修正为 2 参
|
||||
`(ticket, userdata)`,与 include/render/ticket.h 及 oakrender 实现一致
|
||||
(原 3 参镜像与实际 ABI 不符)。
|
||||
遗留注意:ticket.h 规定回调收到的 ticket 是"提交者句柄的借用副本,
|
||||
提交者持有并释放",因此渲染循环只释放 submit 函数返回的那一份句柄、
|
||||
不释放队列里的借用副本(C++ 原版对两者都调 free,若真实 oakrender 的
|
||||
句柄副本不各自计数,则 C++ 路径存在双释放风险,Rust 侧按头文件契约
|
||||
规避);ticket.h 无 poll/try_wait 查询,等待完全走完成回调 + condvar。
|
||||
- **oaktask 导出缺口**:临时文件重命名(失败不留半成品)与
|
||||
sidecar 字幕编码器未实现;precache 缺项目深拷贝。
|
||||
|
||||
## engine/ vs src/ 模块覆盖比对(2026-08-09)
|
||||
|
||||
对照 engine/include/oakengine/*.h(facade 头)与 include/<mod>/*.h
|
||||
(模块 C ABI)的缺口:
|
||||
|
||||
**模块侧真实缺口(要补)**:
|
||||
- oaknode:NodeInputDragger 族(oakengine_dragger_*,6 函数)未迁;
|
||||
keyframe 辅助 4 个(compute_paste_value/get_valid_bezier_point/
|
||||
has_sibling_at_time/opposing_bezier_type)未迁;MultiCam 族
|
||||
(oakengine_multicam_*,9 函数)未迁(oakrender 只有
|
||||
set_cacher_multicam 一个入口)。
|
||||
- oakcodec:格式/编解码器枚举族(oakengine_encoding_format_* /
|
||||
codec_*,12 函数)未迁——oakcodec 只有
|
||||
export_format_get_extension 一个。
|
||||
- oaktimeline/oaknode:engine/timeline.h 的 sequence/clip 便捷层
|
||||
(add_footage_clip/ripple_delete_*/move_clip/trim 等 ~90 函数)
|
||||
大部分是 facade 便捷封装,底层原语(edit 命令族)已齐,facade
|
||||
包装时按需下沉或留在 facade。
|
||||
|
||||
**属于 facade 层(不进模块,属预期)**:viewer/playback/preview/
|
||||
display/gizmo/app/worker/ipc/config/disk/lut/events/exporter ——
|
||||
即 M9 §4 裁决的 liboakengine 装配层职责。
|
||||
|
||||
**已核对无缺口**:undo(39 vs 26 系 facade 组合函数)、audio、
|
||||
group passthrough、keyframe 主体、project/footage 主体。
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
/***
|
||||
|
||||
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_CODEC_FORMAT_H
|
||||
#define OAK_EDITOR_CODEC_FORMAT_H
|
||||
|
||||
#include "error.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file format.h
|
||||
* @brief C ABI for the oakcodec container-format / codec metadata queries
|
||||
* (olive::ExportFormat / olive::ExportCodec / olive::Encoder statics).
|
||||
*
|
||||
* This family is the module-side mirror of the facade's
|
||||
* oakengine_encoding_format_* / codec_* surface (oakengine/encoding.h):
|
||||
* the export dialog queries it to populate its format/codec combo boxes and
|
||||
* to enable/disable the bit-rate controls. The functions are stateless —
|
||||
* no handles involved.
|
||||
*
|
||||
* Enum int fields carry the engine's own enum values
|
||||
* (olive::ExportFormat::Format, olive::ExportCodec::Codec,
|
||||
* olive::core::SampleFormat::Format) — the same values oakengine/encoding.h
|
||||
* documents. Return-code convention follows include/codec/error.h: 0
|
||||
* (OAKCODEC_OK) on success, a negative OAKCODEC_E_* code on failure, and
|
||||
* string getters return the required buffer size in bytes INCLUDING the
|
||||
* terminating NUL as a non-negative value (two-stage convention). Note this
|
||||
* differs from oakcodec_export_format_get_extension() (encoder.h), which
|
||||
* predates this family and reports unknown formats as the empty string.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Container formats (olive::ExportFormat::Format) referenced by name
|
||||
* in UI code. Only append; the values are serialized in project/preset
|
||||
* files. The complete list lives in src/codec/src/exportformat.h.
|
||||
*/
|
||||
#define OAKCODEC_ENCODING_FORMAT_MATROSKA 1
|
||||
#define OAKCODEC_ENCODING_FORMAT_MPEG4_VIDEO 2
|
||||
#define OAKCODEC_ENCODING_FORMAT_QUICKTIME 4
|
||||
#define OAKCODEC_ENCODING_FORMAT_PNG 5
|
||||
#define OAKCODEC_ENCODING_FORMAT_WAV 7
|
||||
#define OAKCODEC_ENCODING_FORMAT_SRT 13
|
||||
|
||||
/**
|
||||
* @brief Codecs (olive::ExportCodec::Codec) referenced by name in UI code.
|
||||
* Only append; the values are serialized. The complete list lives in
|
||||
* src/codec/src/exportcodec.h.
|
||||
*/
|
||||
#define OAKCODEC_ENCODING_CODEC_H264 1
|
||||
#define OAKCODEC_ENCODING_CODEC_H264RGB 2
|
||||
#define OAKCODEC_ENCODING_CODEC_H265 3
|
||||
#define OAKCODEC_ENCODING_CODEC_CINEFORM 7
|
||||
#define OAKCODEC_ENCODING_CODEC_AAC 12
|
||||
#define OAKCODEC_ENCODING_CODEC_PCM 13
|
||||
#define OAKCODEC_ENCODING_CODEC_SRT 17
|
||||
#define OAKCODEC_ENCODING_CODEC_AV1 18
|
||||
|
||||
/* ---- Container format / codec metadata ---------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Number of container formats (olive::ExportFormat::k_format_count).
|
||||
*/
|
||||
OAKCODEC_API int oakcodec_encoding_format_count(void);
|
||||
|
||||
/**
|
||||
* @brief Display name of a container format (buf/size, two-stage).
|
||||
*
|
||||
* @return The required buffer size (including the NUL), or
|
||||
* OAKCODEC_E_INVALID when `format` is out of range.
|
||||
*/
|
||||
OAKCODEC_API int oakcodec_encoding_format_name(int format, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief File extension (no dot) of a container format (buf/size,
|
||||
* two-stage); same return convention as
|
||||
* oakcodec_encoding_format_name().
|
||||
*/
|
||||
OAKCODEC_API int oakcodec_encoding_format_extension(int format, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Number of video codecs a container format supports, or
|
||||
* OAKCODEC_E_INVALID when the format is invalid.
|
||||
*/
|
||||
OAKCODEC_API int oakcodec_encoding_format_video_codec_count(int format);
|
||||
|
||||
/**
|
||||
* @brief The `index`-th video codec of `format` as an
|
||||
* olive::ExportCodec::Codec value.
|
||||
*
|
||||
* @return OAKCODEC_E_INVALID when the format is invalid, or
|
||||
* OAKCODEC_E_NOT_FOUND when the index is out of range.
|
||||
*/
|
||||
OAKCODEC_API int oakcodec_encoding_format_video_codec_at(int format,
|
||||
int index);
|
||||
|
||||
/** @brief Audio-codec variant of the two functions above. */
|
||||
OAKCODEC_API int oakcodec_encoding_format_audio_codec_count(int format);
|
||||
OAKCODEC_API int oakcodec_encoding_format_audio_codec_at(int format,
|
||||
int index);
|
||||
|
||||
/** @brief Subtitle-codec variant of the two functions above. */
|
||||
OAKCODEC_API int oakcodec_encoding_format_subtitle_codec_count(int format);
|
||||
OAKCODEC_API int oakcodec_encoding_format_subtitle_codec_at(int format,
|
||||
int index);
|
||||
|
||||
/**
|
||||
* @brief Display name of a codec (buf/size, two-stage).
|
||||
*
|
||||
* @return The required buffer size (including the NUL), or
|
||||
* OAKCODEC_E_INVALID when `codec` is out of range.
|
||||
*/
|
||||
OAKCODEC_API int oakcodec_encoding_codec_name(int codec, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/** @brief 1 when `codec` encodes still images (PNG/TIFF/OpenEXR), else 0
|
||||
* (0 also for an invalid codec). */
|
||||
OAKCODEC_API int oakcodec_encoding_codec_is_still_image(int codec);
|
||||
|
||||
/** @brief 1 when `codec` is lossless (no bit-rate setting applies), else 0
|
||||
* (0 also for an invalid codec). */
|
||||
OAKCODEC_API int oakcodec_encoding_codec_is_lossless(int codec);
|
||||
|
||||
/**
|
||||
* @brief Number of encoded pixel formats (e.g. "yuv420p") usable with
|
||||
* `codec` inside `format`, or OAKCODEC_E_INVALID when either
|
||||
* argument is out of range. The list is queried from the format's
|
||||
* encoder (FFmpeg/OIIO), so codecs without an encoder report 0.
|
||||
*/
|
||||
OAKCODEC_API int oakcodec_encoding_pix_fmt_count(int format, int codec);
|
||||
|
||||
/**
|
||||
* @brief The `index`-th encoded pixel format name (buf/size, two-stage).
|
||||
*
|
||||
* @return The required buffer size (including the NUL), or
|
||||
* OAKCODEC_E_INVALID for bad format/codec, or
|
||||
* OAKCODEC_E_NOT_FOUND when the index is out of range.
|
||||
*/
|
||||
OAKCODEC_API int oakcodec_encoding_pix_fmt_at(int format, int codec,
|
||||
int index, char *buf,
|
||||
int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Index of `pix_fmt` (e.g. "yuv420p") in `codec`'s supported pixel
|
||||
* format list; 0 (the codec's preferred format) when absent or
|
||||
* `pix_fmt` is NULL/empty or `codec` is invalid.
|
||||
*/
|
||||
OAKCODEC_API int oakcodec_encoding_pix_fmt_index(int codec,
|
||||
const char *pix_fmt);
|
||||
|
||||
/**
|
||||
* @brief Number of sample formats usable with `codec` inside `format`, or
|
||||
* OAKCODEC_E_INVALID when either argument is out of range.
|
||||
*/
|
||||
OAKCODEC_API int oakcodec_encoding_sample_format_count(int format,
|
||||
int codec);
|
||||
|
||||
/**
|
||||
* @brief The `index`-th sample format as an olive::core::SampleFormat::Format
|
||||
* value.
|
||||
*
|
||||
* @return OAKCODEC_E_INVALID for bad format/codec, or
|
||||
* OAKCODEC_E_NOT_FOUND when the index is out of range.
|
||||
*/
|
||||
OAKCODEC_API int oakcodec_encoding_sample_format_at(int format, int codec,
|
||||
int index);
|
||||
|
||||
/* ---- Image-sequence filename helpers (olive::Encoder statics) ----------- */
|
||||
|
||||
/** @brief 1 when `filename` contains a "[#####]" digit placeholder, else 0
|
||||
* (0 for NULL). */
|
||||
OAKCODEC_API int
|
||||
oakcodec_encoding_filename_contains_digit_placeholder(const char *filename);
|
||||
|
||||
/** @brief Digit count of the filename's "[#####]" placeholder; 0 when none
|
||||
* (0 for NULL). */
|
||||
OAKCODEC_API int
|
||||
oakcodec_encoding_image_sequence_digit_count(const char *filename);
|
||||
|
||||
/**
|
||||
* @brief `filename` with the digit placeholder removed (buf/size, two-stage;
|
||||
* a leading separator like "_"/"-"/"."/" " before the placeholder is
|
||||
* removed along with it).
|
||||
*
|
||||
* @return The required buffer size (including the NUL), or
|
||||
* OAKCODEC_E_INVALID when `filename` is NULL.
|
||||
*/
|
||||
OAKCODEC_API int
|
||||
oakcodec_encoding_filename_remove_digit_placeholder(const char *filename,
|
||||
char *buf, int buf_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //OAK_EDITOR_CODEC_FORMAT_H
|
||||
@@ -0,0 +1,137 @@
|
||||
/***
|
||||
|
||||
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_NODE_DRAGGER_H
|
||||
#define OAK_EDITOR_NODE_DRAGGER_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "node/error.h"
|
||||
#include "node/node.h"
|
||||
#include "undo/undocommand.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file dragger.h
|
||||
* @brief C ABI for olive::NodeInputDragger (src/node/src/inputdragger.h):
|
||||
* live drag of an input's value with a single commit command.
|
||||
*
|
||||
* A dragger wraps the engine's NodeInputDragger state machine
|
||||
* (start -> drag* -> end). start() records the drag anchor and, when the
|
||||
* input is keyframing, creates one keyframe at the drag time (on every
|
||||
* track when requested); drag() live-sets the dragged component (clamped
|
||||
* by the input's min/max properties when present); end() returns ONE
|
||||
* undoable command that commits the whole drag -- undo removes the
|
||||
* created keyframe(s) (restoring the pre-drag keyframe count), redo
|
||||
* re-creates them with the final value.
|
||||
*
|
||||
* A dragger must be ended before it is freed; freeing a started dragger
|
||||
* leaks the created keyframe(s) (the same ownership rule as the C++
|
||||
* class).
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Reference-counted handle to an input dragger
|
||||
* (olive::NodeInputDragger).
|
||||
*
|
||||
* The object never leaves the library that created it; every external
|
||||
* reference is one of these handles. Semantics are shared_ptr-like:
|
||||
* oaknode_dragger_create() returns a handle with count 1, addref(ctx)
|
||||
* takes another reference, release(ctx) drops one and the library
|
||||
* destroys the object when the count reaches zero.
|
||||
*/
|
||||
typedef struct OakNodeDragger {
|
||||
void *ctx; /**< Opaque pointer to the reference-counted object. */
|
||||
void (*addref)(void *ctx); /**< Atomically increments the count. */
|
||||
void (*release)(void *ctx); /**< Decrements the count, destroys at 0. */
|
||||
uint32_t abi_version; /**< OAKNODE_ABI_VERSION. */
|
||||
} OakNodeDragger;
|
||||
|
||||
/**
|
||||
* @brief Create an input dragger for live-drag of an input's value.
|
||||
*
|
||||
* `input_id` must name an existing input of `node`; `element` addresses
|
||||
* an array input's element (-1 for non-array inputs). `track` is the
|
||||
* create-time default; the track passed to oaknode_dragger_start()
|
||||
* establishes the actual drag track.
|
||||
*
|
||||
* @return Dragger handle with count 1; ctx is NULL on invalid arguments
|
||||
* or allocation failure.
|
||||
*/
|
||||
OakNodeDragger oaknode_dragger_create(OakNodeNode node, const char *input_id,
|
||||
int element, int track);
|
||||
|
||||
/**
|
||||
* @brief Start the drag at the given rational time (creates a keyframe
|
||||
* when the input is keyframing).
|
||||
*
|
||||
* `insert_on_all_tracks` != 0 also creates sibling keyframes on every
|
||||
* other track of the input. OAKNODE_E_STATE when the dragger was already
|
||||
* started.
|
||||
*/
|
||||
int oaknode_dragger_start(OakNodeDragger dragger, int64_t time_num,
|
||||
int64_t time_den, int track,
|
||||
int insert_on_all_tracks);
|
||||
|
||||
/**
|
||||
* @brief Drag to a new per-track component value (live; no undo).
|
||||
*
|
||||
* `value` carries the dragged component of the input's declared type:
|
||||
* scalar types in f[0]/num; for split-track types (VEC2/3/4/COLOR) the
|
||||
* POD type must match the input's declared type and the dragged
|
||||
* component sits in f[0] (the facade's dragger convention). The value is
|
||||
* clamped to the input's min/max properties when present.
|
||||
* OAKNODE_E_STATE when the dragger was not started.
|
||||
*/
|
||||
int oaknode_dragger_drag(OakNodeDragger dragger, const oaknode_value *value);
|
||||
|
||||
/**
|
||||
* @brief End the drag, returning ONE undoable command for the whole drag.
|
||||
*
|
||||
* `*out_command` receives an owned command handle (execute it with
|
||||
* oakundo_command_redo_now(), push it onto an OakUndoStack, or release
|
||||
* it with oakundo_command_free()). OAKNODE_E_STATE when the dragger was
|
||||
* not started.
|
||||
*/
|
||||
int oaknode_dragger_end(OakNodeDragger dragger, OakUndoCommand *out_command);
|
||||
|
||||
/**
|
||||
* @brief 1 if the dragger has been started and not yet ended.
|
||||
*/
|
||||
int oaknode_dragger_is_started(OakNodeDragger dragger, int *out_started);
|
||||
|
||||
/**
|
||||
* @brief Release one reference to a dragger handle.
|
||||
*
|
||||
* Convenience wrapper around handle.release(handle.ctx): destroys the
|
||||
* dragger when the count reaches zero. NULL handle or NULL ctx is a
|
||||
* no-op; clears `dragger->ctx` after releasing. The dragger must have
|
||||
* been ended (see the file comment).
|
||||
*/
|
||||
void oaknode_dragger_free(OakNodeDragger *dragger);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //OAK_EDITOR_NODE_DRAGGER_H
|
||||
@@ -235,6 +235,59 @@ int oaknode_keyframe_get_input(OakNodeKeyframe keyframe, char *buf,
|
||||
int oaknode_keyframe_get_parent(OakNodeKeyframe keyframe,
|
||||
OakNodeNode *out_node);
|
||||
|
||||
/**
|
||||
* @brief A bezier control point guaranteed valid for animation
|
||||
* (NodeKeyframe::valid_bezier_control_in()/out()).
|
||||
*
|
||||
* Unlike oaknode_keyframe_get_bezier_control(), the returned point is
|
||||
* clamped so the curve never overlaps: the in-handle's x cannot pass the
|
||||
* previous keyframe's time and the out-handle's x cannot pass the next
|
||||
* keyframe's time. `handle` is an oaknode_keyframe_bezier.
|
||||
*/
|
||||
int oaknode_keyframe_get_valid_bezier_control(OakNodeKeyframe keyframe,
|
||||
int handle, double *out_x,
|
||||
double *out_y);
|
||||
|
||||
/**
|
||||
* @brief The opposing bezier handle type
|
||||
* (NodeKeyframe::get_opposing_bezier_type): OAKNODE_KEYFRAME_IN_HANDLE
|
||||
* (0) <-> OAKNODE_KEYFRAME_OUT_HANDLE (1).
|
||||
*
|
||||
* @return The opposing handle type, or OAKNODE_E_INVALID for a type
|
||||
* outside the two handle values.
|
||||
*/
|
||||
int oaknode_keyframe_opposing_bezier_type(int type);
|
||||
|
||||
/**
|
||||
* @brief Compute the combined node value to use when inserting
|
||||
* `keyframe` onto `target_node` (the keyframe paste path).
|
||||
*
|
||||
* Takes the target node's split value at the keyframe's time, replaces
|
||||
* the keyframe's own track with the keyframe's value, and combines the
|
||||
* per-track components into a single normal value (mirrors the facade's
|
||||
* oakengine_keyframe_compute_paste_value). OAKNODE_E_NOT_FOUND when the
|
||||
* keyframe's input id does not exist on `target_node`; OAKNODE_E_FAILED
|
||||
* for input types without a POD representation.
|
||||
*/
|
||||
int oaknode_keyframe_compute_paste_value(OakNodeNode target_node,
|
||||
OakNodeKeyframe keyframe,
|
||||
oaknode_value *out);
|
||||
|
||||
/**
|
||||
* @brief 1 if a sibling keyframe exists at the given rational time on
|
||||
* this keyframe's own track (NodeKeyframe::has_sibling_at_time(): the
|
||||
* track's key at `time` that is not this keyframe — the move-collision
|
||||
* check). Unlike the facade, the time is an exact rational rather than a
|
||||
* whole-second frame timestamp, and no track argument is needed (the
|
||||
* lookup is relative to this keyframe's track).
|
||||
*
|
||||
* An orphaned keyframe (no parent node) has no siblings: `*out_value`
|
||||
* is set to 0 and OAKNODE_OK is returned.
|
||||
*/
|
||||
int oaknode_keyframe_has_sibling_at_time(OakNodeKeyframe keyframe,
|
||||
int64_t time_num, int64_t time_den,
|
||||
int *out_value);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/***
|
||||
|
||||
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_NODE_MULTICAM_H
|
||||
#define OAK_EDITOR_NODE_MULTICAM_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "node/error.h"
|
||||
#include "node/node.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file multicam.h
|
||||
* @brief C ABI for olive::MultiCamNode (src/node/src/input/multicam/
|
||||
* multicamnode.h): multi-camera source switching and the source-grid
|
||||
* math used by the multicam viewer.
|
||||
*
|
||||
* The input-id getters return static strings (never freed) naming the
|
||||
* multicam node's inputs: current source (combo), sources (array),
|
||||
* sequence and sequence type. A node that is not a MultiCamNode (or a
|
||||
* NULL handle) fails the per-node queries with OAKNODE_E_INVALID.
|
||||
*
|
||||
* The grid helpers are static and pure: they only depend on their
|
||||
* arguments, not on a node.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief The input id string for the current camera ("current_in").
|
||||
*/
|
||||
const char *oaknode_multicam_input_current(void);
|
||||
|
||||
/**
|
||||
* @brief The input id string for the sources array ("sources_in").
|
||||
*/
|
||||
const char *oaknode_multicam_input_sources(void);
|
||||
|
||||
/**
|
||||
* @brief The input id string for the sequence ("sequence_in").
|
||||
*/
|
||||
const char *oaknode_multicam_input_sequence(void);
|
||||
|
||||
/**
|
||||
* @brief The input id string for the sequence type ("sequence_type_in").
|
||||
*/
|
||||
const char *oaknode_multicam_input_sequence_type(void);
|
||||
|
||||
/**
|
||||
* @brief Number of connected source cameras (MultiCamNode::
|
||||
* get_source_count(); the connected sequence's track count, or the
|
||||
* sources array size when no sequence is connected).
|
||||
*
|
||||
* OAKNODE_E_INVALID when `node` is not a multicam.
|
||||
*/
|
||||
int oaknode_multicam_get_source_count(OakNodeNode node, int *out_count);
|
||||
|
||||
/**
|
||||
* @brief Compute the grid (rows, cols) that holds `source_count` cells.
|
||||
*
|
||||
* Mirrors MultiCamNode::get_rows_and_columns(): the grid grows from
|
||||
* 1x1, widening the smaller dimension, until rows * cols >= source_count
|
||||
* (0 sources yields 1x1). OAKNODE_E_INVALID for a negative count or
|
||||
* NULL out pointers.
|
||||
*/
|
||||
int oaknode_multicam_get_rows_and_columns(int source_count, int *rows,
|
||||
int *cols);
|
||||
|
||||
/**
|
||||
* @brief Convert a flat source index to (row, col) in a rows x cols grid
|
||||
* (row-major: col = index % cols, row = index / cols).
|
||||
*
|
||||
* OAKNODE_E_INVALID for a negative index, degenerate grid or NULL out
|
||||
* pointers.
|
||||
*/
|
||||
int oaknode_multicam_index_to_row_cols(int index, int rows, int cols,
|
||||
int *out_row, int *out_col);
|
||||
|
||||
/**
|
||||
* @brief Convert (row, col) to a flat source index (col + row * cols).
|
||||
*
|
||||
* @return The flat index (>= 0), or OAKNODE_E_INVALID when the cell is
|
||||
* out of range or the grid is degenerate.
|
||||
*/
|
||||
int oaknode_multicam_rows_cols_to_index(int row, int col, int rows,
|
||||
int cols);
|
||||
|
||||
/**
|
||||
* @brief The current source index (MultiCamNode::get_current_source(),
|
||||
* the "current_in" combo value).
|
||||
*
|
||||
* OAKNODE_E_INVALID when `node` is not a multicam.
|
||||
*/
|
||||
int oaknode_multicam_get_current_source(OakNodeNode node, int *out_source);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //OAK_EDITOR_NODE_MULTICAM_H
|
||||
@@ -96,6 +96,74 @@ int oakplugin_instance_cancel(OakPluginInstance instance);
|
||||
/** @brief Alive-count for leak assertions in tests. */
|
||||
int oakplugin_debug_alive_count(void);
|
||||
|
||||
/*
|
||||
* M11 §4(GL 路径 + render 驱动收编)新增声明。既有签名不变。
|
||||
*
|
||||
* oakrender 的 PluginJob 经本组入口把整帧渲染流程(RoI/RoD、
|
||||
* 多输入收集、isIdentity 短路、参数覆盖、CPU/GL 渲染与输出装配)
|
||||
* 委托给 oakplugin 的 render 驱动(Rust 侧 render_driver 模块,
|
||||
* 语义对照 src/render/src/plugin/pluginrenderer.cpp)。
|
||||
*/
|
||||
|
||||
/** @brief 一帧渲染任务的参数覆盖条目(参数名 → oaknode_value POD;
|
||||
* 字符串参数走 oakplugin_instance_set_param_string)。 */
|
||||
typedef struct oakplugin_job_value {
|
||||
const char *key;
|
||||
oaknode_value value;
|
||||
} oakplugin_job_value;
|
||||
|
||||
/** @brief 一帧渲染任务的输入 clip 纹理条目。纹理为借用句柄
|
||||
* (job 内有效)。 */
|
||||
typedef struct oakplugin_job_texture {
|
||||
const char *clip;
|
||||
OakRenderTexture texture;
|
||||
} oakplugin_job_texture;
|
||||
|
||||
/**
|
||||
* @brief beginSequenceRender 括号。oakrender 对同一实例的一批帧先
|
||||
* begin 后 end,中间逐帧 oakplugin_instance_render_job
|
||||
* (OFX:render action 由 begin/end sequence render 括号包围)。
|
||||
* `interactive` 为信息性标记(Phase 2 不传入 action)。
|
||||
*/
|
||||
int oakplugin_instance_render_begin_sequence(OakPluginInstance instance,
|
||||
double start_time,
|
||||
double end_time,
|
||||
int interactive);
|
||||
|
||||
/** @brief endSequenceRender 括号(与 render_begin_sequence 配对)。 */
|
||||
int oakplugin_instance_render_end_sequence(OakPluginInstance instance,
|
||||
double start_time,
|
||||
double end_time,
|
||||
int interactive);
|
||||
|
||||
/**
|
||||
* @brief 一帧渲染的单一 C ABI 调用(PluginJob 的载体)。
|
||||
*
|
||||
* @param dst 目标纹理(oakrender 创建)。GL 模式下调用方须先把
|
||||
* dst 附着为渲染器输出目标并保持 GL 上下文 current
|
||||
* (OFX "OpenGL Current Context" 规则;等价 C++
|
||||
* PluginRenderer::attach_output_texture)。
|
||||
* @param src 主输入纹理(effect_input_id / SimpleSource;可空句柄)。
|
||||
* @param effect_input_id job.src 落点的 clip 名(可 NULL)。
|
||||
* @param inputs / input_count 其余输入 clip 的纹理表。
|
||||
* @param values / value_count 参数覆盖表。
|
||||
* @param renderer GL 渲染器(空句柄 → CPU 路径)。
|
||||
* @param clear_destination / interactive 信息性标记(Phase 2,
|
||||
* render 驱动暂不处理;上层渲染器负责目标清空)。
|
||||
*/
|
||||
int oakplugin_instance_render_job(OakPluginInstance instance,
|
||||
OakRenderTexture dst,
|
||||
double time_seconds,
|
||||
int clear_destination,
|
||||
int interactive,
|
||||
const char *effect_input_id,
|
||||
OakRenderTexture src,
|
||||
const oakplugin_job_texture *inputs,
|
||||
int input_count,
|
||||
const oakplugin_job_value *values,
|
||||
int value_count,
|
||||
OakRenderRenderer renderer);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
Generated
+135
@@ -0,0 +1,135 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "oakcore-rs"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "oakotio"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"oakcore-rs",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
@@ -0,0 +1,38 @@
|
||||
# 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/>.
|
||||
|
||||
[package]
|
||||
name = "oakotio"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Oak Video Editor OpenTimelineIO binding crate: pure-Rust serde model of the OTIO JSON format used by project load/save (src/task/src/project/loadotio, saveotio)"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
# serde + serde_json: JSON codec for the OTIO JSON format. The
|
||||
# `preserve_order` feature keeps Map insertion order so metadata and unknown
|
||||
# fields round-trip in file order, and a custom 4-space pretty formatter
|
||||
# reproduces the opentimelineio writer byte-for-byte. See README.md for the
|
||||
# rationale (no maintained pure-Rust OTIO crate exists on crates.io).
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
|
||||
# Shared value types (Rational). Same path dependency the other bindings use.
|
||||
oakcore-rs = { path = "../../oakcore-rs" }
|
||||
@@ -0,0 +1,144 @@
|
||||
# oakotio
|
||||
|
||||
Pure-Rust OpenTimelineIO JSON binding for the Oak Video Editor's Rust
|
||||
rewrite. This crate is a self-contained serde model of the OTIO JSON format,
|
||||
covering exactly the object graph Oak's project load/save tasks use
|
||||
(`src/task/src/project/loadotio/loadotio.cpp` and
|
||||
`src/task/src/project/saveotio/saveotio.cpp`): `RationalTime`, `TimeRange`,
|
||||
`Clip`, `Gap`, `Transition`, `Track`, `Stack`, `Timeline`,
|
||||
`ExternalReference`, `MissingReference` and `SerializableCollection`.
|
||||
|
||||
The writer reproduces the opentimelineio C++ writer's output byte for byte
|
||||
(4-space indentation, `": "` separators, inline empty objects and arrays,
|
||||
shortest float representation, no trailing newline); the reader tolerates
|
||||
hand-written files, preserving unknown fields verbatim across a round-trip
|
||||
and defaulting missing fields.
|
||||
|
||||
Part of the Oak `src/bindings/` family (siblings: `oakaudioout`).
|
||||
This is an **rlib** — nothing is exported dynamically.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
src/bindings/oakotio/
|
||||
├── Cargo.toml # rlib; deps: serde, serde_json (crates.io), oakcore-rs (path)
|
||||
├── src/
|
||||
│ ├── lib.rs # crate docs + module wiring + re-exports + from_json_* entry points
|
||||
│ ├── error.rs # OtioError (Json | Io) + Result<T>
|
||||
│ └── model.rs # serde structs for the 10 OTIO schemas + value types +
|
||||
│ # Rational::from_double port + in-crate unit tests
|
||||
└── tests/
|
||||
├── data/ # C++-writer golden files (golden_timeline.json,
|
||||
│ # golden_collection.json, golden_typed_transition.json,
|
||||
│ # floatfmt.json)
|
||||
├── parity.rs # read parity: every golden file parses and round-trips
|
||||
├── semantic.rs # semantic checks over golden_timeline.json (what the
|
||||
│ # C++ load task reads back)
|
||||
└── save_parity.rs # save parity: a built Timeline serializes byte-identical
|
||||
# to golden_timeline.json and re-parses identically
|
||||
```
|
||||
|
||||
## API summary
|
||||
|
||||
- `from_json_string(&str) -> Result<Serializable>` /
|
||||
`from_json_file(path) -> Result<Serializable>` — parse a document whose root
|
||||
is a `Timeline`, a `SerializableCollection`, or an unknown schema kept whole
|
||||
as `Serializable::Raw`.
|
||||
- `RationalTime` — `new(value, rate)` (C++ argument order), `value`, `rate`,
|
||||
`to_seconds`, `is_invalid_time`, `invalid_time`, `rescaled_to`,
|
||||
`to_rational`, `from_rational`.
|
||||
- `TimeRange` — `new(start_time, duration)` (C++ argument order), `start_time`,
|
||||
`duration`.
|
||||
- `Clip` — `new(name)`, `name`, `source_range`, `set_source_range`,
|
||||
`media_reference`, `media_references`, `set_media_reference`.
|
||||
- `Gap` — `new(source_range, name)`, `name`, `source_range`.
|
||||
- `Transition` — `new(name)`, `name`, `in_offset`, `out_offset`,
|
||||
`transition_type`, `set_in_offset`, `set_out_offset`.
|
||||
- `Track` — `new(kind)`, `kind`, `children`, `append_child`.
|
||||
- `Stack` — `children`, `append_child`.
|
||||
- `Timeline` — `new(name)`, `name`, `tracks`, `tracks_mut`,
|
||||
`global_start_time`, `to_json_string`, `to_json_file`.
|
||||
- `SerializableCollection` — `new(name, children)`, `name`, `children`,
|
||||
`to_json_string`, `to_json_file`.
|
||||
- `MediaReference` / `Composable` / `Serializable` enums — downcasts
|
||||
(`as_clip`, `as_track`, ...) and `schema_name` for dynamic dispatch by
|
||||
`OTIO_SCHEMA`.
|
||||
|
||||
All fallible operations return `Result<T, OtioError>`.
|
||||
|
||||
## Backend choice: hand-written serde over the `opentimelineio` crate
|
||||
|
||||
| Option | Verdict |
|
||||
| --- | --- |
|
||||
| crates.io `opentimelineio` | Not viable: the crate is unmaintained, binds the C++ library via FFI (large, ABI-fragile), and does not build a pure-Rust model Oak's load/save tasks can read directly. No actively maintained pure-Rust OTIO implementation exists on crates.io. |
|
||||
| **This crate: `serde` + `serde_json`** | Pure-Rust (no C++ runtime), fully controllable field order and formatting, preserves unknown fields for forward compatibility, and ports the only piece of C++ numeric behavior Oak needs (`Rational::from_double`) on top of `oakcore_rs::Rational`. |
|
||||
|
||||
The C++ side serializes with `opentimelineio::schema::Timeline::to_json_string`
|
||||
(4-space pretty formatter); this crate reproduces that exact writer with a
|
||||
`serde_json::PrettyFormatter` (`with_indent(b" ")`), `preserve_order` maps
|
||||
so insertion order is kept, and ryu float formatting, which is what the C++
|
||||
writer (rapidjson) emits. The result is byte-for-byte parity with C++-written
|
||||
files (verified against the golden files).
|
||||
|
||||
## Dependency registry
|
||||
|
||||
Runtime dependencies (crates.io):
|
||||
|
||||
- `serde` 1 (with `derive`) — (de)serialization for the OTIO schema structs.
|
||||
- `serde_json` 1 (with `preserve_order`) — JSON codec; `preserve_order` keeps
|
||||
map insertion order so metadata and unknown fields round-trip in file
|
||||
order.
|
||||
- `oakcore-rs` (path: `../../oakcore-rs`) — shared `Rational` value type
|
||||
(used by the `Rational::from_double` port); same path dependency the other
|
||||
bindings use.
|
||||
|
||||
Build and test:
|
||||
|
||||
```sh
|
||||
cd src/bindings/oakotio
|
||||
cargo build
|
||||
cargo test
|
||||
```
|
||||
|
||||
## C++ parity notes
|
||||
|
||||
The C++ anchors this crate reproduces:
|
||||
|
||||
- **`Rational::from_double`** (`core/src/util/rational.cpp`) — ported in
|
||||
`model.rs` on top of `oakcore_rs::Rational::new` (which applies the exact
|
||||
C++ `reduce_fraction(INT_MAX)` reduction). NaN and out-of-range magnitudes
|
||||
collapse to the null sentinel `Rational::NULL`; the retry pass against
|
||||
`INT64_MAX` fires for tiny magnitudes and is itself reduced back to 0/1 by
|
||||
the `INT_MAX` ceiling, matching the C++ result.
|
||||
- **Writer format** — `opentimelineio::schema::Timeline::to_json_string`:
|
||||
4-space indentation, `": "` separators, inline empty `{}`/`[]`, shortest
|
||||
float representation (ryu = rapidjson), no trailing newline. Golden files
|
||||
written by the C++ writer round-trip byte-identically.
|
||||
- **Field order** — struct field order matches the C++ writer's output order
|
||||
(e.g. `RationalTime`: `rate` then `value`; `TimeRange`: `duration` then
|
||||
`start_time`; `Track`: `children` then `kind`).
|
||||
- **`media_references`** — the C++ `Clip` stores a `std::map<string, ...>`;
|
||||
this crate uses `BTreeMap`, which serializes keys in the same sorted order.
|
||||
- **Missing-reference serialization** — `MissingReference` writes
|
||||
`available_range`/`available_image_bounds` as `null` and omits `target_url`,
|
||||
exactly like the C++ writer.
|
||||
|
||||
## Deviations from the C++ code (deliberate)
|
||||
|
||||
- **Unknown fields are kept, not dropped** — the C++ reader discards
|
||||
unrecognized JSON fields; this crate preserves them (via `#[serde(flatten)]`
|
||||
catch-all maps) so a document written by a newer opentimelineio still
|
||||
round-trips. This is a superset of the C++ behavior.
|
||||
- **Defaults are lenient** — missing fields deserialize to their type's
|
||||
default (the C++ `AnyDictionary` fill defaults), so hand-written files
|
||||
without optional fields parse cleanly.
|
||||
- **`RationalTime`/`TimeRange` are `Clone`, not `Copy`** — they carry a
|
||||
`String` schema field, so value accessors (`value()`, `rate()`,
|
||||
`duration()`, ...) take `&self` and return clones; the C++ value semantics
|
||||
(`to_seconds`, `rescaled_to`) are unaffected.
|
||||
|
||||
## Scope
|
||||
|
||||
Covers only what `loadotio.cpp` / `saveotio.cpp` touch. No media-resolution,
|
||||
no `Marker`/`Effect` schemas (kept as raw `Value` for round-tripping), and no
|
||||
plugin API — `src/plugin/` is intentionally untouched.
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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/>.
|
||||
|
||||
//! Error type for the oakotio binding.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Errors produced by loading or saving OpenTimelineIO JSON.
|
||||
#[derive(Debug)]
|
||||
pub enum OtioError {
|
||||
/// The document could not be parsed (or a value could not be
|
||||
/// serialized) as JSON.
|
||||
Json(serde_json::Error),
|
||||
/// The underlying file could not be read or written.
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for OtioError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
OtioError::Json(e) => write!(f, "OpenTimelineIO JSON error: {e}"),
|
||||
OtioError::Io(e) => write!(f, "OpenTimelineIO file error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for OtioError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
OtioError::Json(e) => Some(e),
|
||||
OtioError::Io(e) => Some(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for OtioError {
|
||||
fn from(e: serde_json::Error) -> OtioError {
|
||||
OtioError::Json(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for OtioError {
|
||||
fn from(e: std::io::Error) -> OtioError {
|
||||
OtioError::Io(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience alias used by the binding API.
|
||||
pub type Result<T> = std::result::Result<T, OtioError>;
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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/>.
|
||||
|
||||
//! Pure-Rust OpenTimelineIO JSON binding for Oak Video Editor.
|
||||
//!
|
||||
//! `oakotio` is a self-contained serde model of the OpenTimelineIO JSON
|
||||
//! format, covering exactly the object graph Oak's project load/save tasks
|
||||
//! use (`src/task/src/project/loadotio/loadotio.cpp` and
|
||||
//! `src/task/src/project/saveotio/saveotio.cpp`): `RationalTime`,
|
||||
//! `TimeRange`, `Clip`, `Gap`, `Transition`, `Track`, `Stack`, `Timeline`,
|
||||
//! `ExternalReference`, `MissingReference` and `SerializableCollection`.
|
||||
//!
|
||||
//! The writer reproduces the opentimelineio C++ writer's output byte for
|
||||
//! byte (4-space indentation, `": "` separators, inline empty objects and
|
||||
//! arrays, shortest float representation, no trailing newline); the reader
|
||||
//! tolerates hand-written files, preserving unknown fields verbatim across a
|
||||
//! round-trip and defaulting missing fields. See `README.md` for the design
|
||||
//! rationale and the parity notes against the C++ implementation.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
pub mod error;
|
||||
pub mod model;
|
||||
|
||||
pub use error::{OtioError, Result};
|
||||
pub use model::*;
|
||||
|
||||
/// Parse an OpenTimelineIO JSON document from a string.
|
||||
///
|
||||
/// The root may be a `Timeline`, a `SerializableCollection`, or any other
|
||||
/// schema; unrecognized roots are kept whole as `model::Serializable::Raw`
|
||||
/// so they round-trip untouched.
|
||||
pub fn from_json_string(text: &str) -> Result<Serializable> {
|
||||
Ok(serde_json::from_str(text)?)
|
||||
}
|
||||
|
||||
/// Read and parse an OpenTimelineIO JSON document from a file.
|
||||
pub fn from_json_file(path: impl AsRef<Path>) -> Result<Serializable> {
|
||||
from_json_string(&std::fs::read_to_string(path)?)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"OTIO_SCHEMA": "Timeline.1",
|
||||
"metadata": {},
|
||||
"name": "F",
|
||||
"global_start_time": null,
|
||||
"tracks": {
|
||||
"OTIO_SCHEMA": "Stack.1",
|
||||
"metadata": {},
|
||||
"name": "tracks",
|
||||
"source_range": null,
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Track.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"source_range": null,
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Clip.2",
|
||||
"metadata": {},
|
||||
"name": "c1",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 1.0,
|
||||
"value": 0.1
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 3.0,
|
||||
"value": 1.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"media_references": {
|
||||
"DEFAULT_MEDIA": {
|
||||
"OTIO_SCHEMA": "MissingReference.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"available_range": null,
|
||||
"available_image_bounds": null
|
||||
}
|
||||
},
|
||||
"active_media_reference_key": "DEFAULT_MEDIA"
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Clip.2",
|
||||
"metadata": {},
|
||||
"name": "c2",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 1.0,
|
||||
"value": 123456789.125
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 4.8
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"media_references": {
|
||||
"DEFAULT_MEDIA": {
|
||||
"OTIO_SCHEMA": "MissingReference.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"available_range": null,
|
||||
"available_image_bounds": null
|
||||
}
|
||||
},
|
||||
"active_media_reference_key": "DEFAULT_MEDIA"
|
||||
}
|
||||
],
|
||||
"kind": "Video"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
{
|
||||
"OTIO_SCHEMA": "SerializableCollection.1",
|
||||
"metadata": {},
|
||||
"name": "Sequences",
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Timeline.1",
|
||||
"metadata": {},
|
||||
"name": "Seq One",
|
||||
"global_start_time": null,
|
||||
"tracks": {
|
||||
"OTIO_SCHEMA": "Stack.1",
|
||||
"metadata": {},
|
||||
"name": "tracks",
|
||||
"source_range": null,
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Track.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"source_range": null,
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Clip.2",
|
||||
"metadata": {},
|
||||
"name": "Seq One Clip",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 30.0,
|
||||
"value": 1440.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 30.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"media_references": {
|
||||
"DEFAULT_MEDIA": {
|
||||
"OTIO_SCHEMA": "ExternalReference.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"available_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 25.0,
|
||||
"value": 100.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 25.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"available_image_bounds": null,
|
||||
"target_url": "file:///tmp/Seq One.mp4"
|
||||
}
|
||||
},
|
||||
"active_media_reference_key": "DEFAULT_MEDIA"
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Gap.1",
|
||||
"metadata": {},
|
||||
"name": "Seq One Gap",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 576.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Transition.1",
|
||||
"metadata": {},
|
||||
"name": "Seq One Transition",
|
||||
"in_offset": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 12.0
|
||||
},
|
||||
"out_offset": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 12.0
|
||||
},
|
||||
"transition_type": ""
|
||||
}
|
||||
],
|
||||
"kind": "Video"
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Track.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"source_range": null,
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Clip.2",
|
||||
"metadata": {},
|
||||
"name": "Seq One Audio",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 30.0,
|
||||
"value": 1440.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 30.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"media_references": {
|
||||
"DEFAULT_MEDIA": {
|
||||
"OTIO_SCHEMA": "ExternalReference.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"available_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 48000.0,
|
||||
"value": 0.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 48000.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"available_image_bounds": null,
|
||||
"target_url": "file:///tmp/Seq One.wav"
|
||||
}
|
||||
},
|
||||
"active_media_reference_key": "DEFAULT_MEDIA"
|
||||
}
|
||||
],
|
||||
"kind": "Audio"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Timeline.1",
|
||||
"metadata": {},
|
||||
"name": "Seq Two",
|
||||
"global_start_time": null,
|
||||
"tracks": {
|
||||
"OTIO_SCHEMA": "Stack.1",
|
||||
"metadata": {},
|
||||
"name": "tracks",
|
||||
"source_range": null,
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Track.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"source_range": null,
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Clip.2",
|
||||
"metadata": {},
|
||||
"name": "Seq Two Clip",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 25.0,
|
||||
"value": 1200.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 25.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"media_references": {
|
||||
"DEFAULT_MEDIA": {
|
||||
"OTIO_SCHEMA": "ExternalReference.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"available_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 25.0,
|
||||
"value": 100.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 25.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"available_image_bounds": null,
|
||||
"target_url": "file:///tmp/Seq Two.mp4"
|
||||
}
|
||||
},
|
||||
"active_media_reference_key": "DEFAULT_MEDIA"
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Gap.1",
|
||||
"metadata": {},
|
||||
"name": "Seq Two Gap",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 576.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Transition.1",
|
||||
"metadata": {},
|
||||
"name": "Seq Two Transition",
|
||||
"in_offset": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 12.0
|
||||
},
|
||||
"out_offset": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 12.0
|
||||
},
|
||||
"transition_type": ""
|
||||
}
|
||||
],
|
||||
"kind": "Video"
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Track.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"source_range": null,
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Clip.2",
|
||||
"metadata": {},
|
||||
"name": "Seq Two Audio",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 25.0,
|
||||
"value": 1200.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 25.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"media_references": {
|
||||
"DEFAULT_MEDIA": {
|
||||
"OTIO_SCHEMA": "ExternalReference.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"available_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 48000.0,
|
||||
"value": 0.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 48000.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"available_image_bounds": null,
|
||||
"target_url": "file:///tmp/Seq Two.wav"
|
||||
}
|
||||
},
|
||||
"active_media_reference_key": "DEFAULT_MEDIA"
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Gap.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 1.0,
|
||||
"value": 12.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 25.0,
|
||||
"value": 1200.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"kind": "Audio"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"OTIO_SCHEMA": "Timeline.1",
|
||||
"metadata": {},
|
||||
"name": "My Sequence",
|
||||
"global_start_time": null,
|
||||
"tracks": {
|
||||
"OTIO_SCHEMA": "Stack.1",
|
||||
"metadata": {},
|
||||
"name": "tracks",
|
||||
"source_range": null,
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Track.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"source_range": null,
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Clip.2",
|
||||
"metadata": {},
|
||||
"name": "My Sequence Clip",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 1152.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"media_references": {
|
||||
"DEFAULT_MEDIA": {
|
||||
"OTIO_SCHEMA": "ExternalReference.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"available_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 25.0,
|
||||
"value": 100.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 25.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"available_image_bounds": null,
|
||||
"target_url": "file:///tmp/My Sequence.mp4"
|
||||
}
|
||||
},
|
||||
"active_media_reference_key": "DEFAULT_MEDIA"
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Gap.1",
|
||||
"metadata": {},
|
||||
"name": "My Sequence Gap",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 576.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Transition.1",
|
||||
"metadata": {},
|
||||
"name": "My Sequence Transition",
|
||||
"in_offset": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 12.0
|
||||
},
|
||||
"out_offset": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 12.0
|
||||
},
|
||||
"transition_type": ""
|
||||
}
|
||||
],
|
||||
"kind": "Video"
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Track.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"source_range": null,
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Clip.2",
|
||||
"metadata": {},
|
||||
"name": "My Sequence Audio",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 1152.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"media_references": {
|
||||
"DEFAULT_MEDIA": {
|
||||
"OTIO_SCHEMA": "ExternalReference.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"available_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 48000.0,
|
||||
"value": 0.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 48000.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"available_image_bounds": null,
|
||||
"target_url": "file:///tmp/My Sequence.wav"
|
||||
}
|
||||
},
|
||||
"active_media_reference_key": "DEFAULT_MEDIA"
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Gap.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 1.0,
|
||||
"value": 12.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 1152.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"kind": "Audio"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
{
|
||||
"OTIO_SCHEMA": "Timeline.1",
|
||||
"metadata": {},
|
||||
"name": "Typed Transition",
|
||||
"global_start_time": null,
|
||||
"tracks": {
|
||||
"OTIO_SCHEMA": "Stack.1",
|
||||
"metadata": {},
|
||||
"name": "tracks",
|
||||
"source_range": null,
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Track.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"source_range": null,
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Clip.2",
|
||||
"metadata": {},
|
||||
"name": "Typed Transition Clip",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 1152.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"media_references": {
|
||||
"DEFAULT_MEDIA": {
|
||||
"OTIO_SCHEMA": "ExternalReference.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"available_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 25.0,
|
||||
"value": 100.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 25.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"available_image_bounds": null,
|
||||
"target_url": "file:///tmp/Typed Transition.mp4"
|
||||
}
|
||||
},
|
||||
"active_media_reference_key": "DEFAULT_MEDIA"
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Gap.1",
|
||||
"metadata": {},
|
||||
"name": "Typed Transition Gap",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 576.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Transition.1",
|
||||
"metadata": {},
|
||||
"name": "Typed Transition Transition",
|
||||
"in_offset": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 12.0
|
||||
},
|
||||
"out_offset": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 12.0
|
||||
},
|
||||
"transition_type": "SMPTE_Dissolve"
|
||||
}
|
||||
],
|
||||
"kind": "Video"
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Track.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"source_range": null,
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"children": [
|
||||
{
|
||||
"OTIO_SCHEMA": "Clip.2",
|
||||
"metadata": {},
|
||||
"name": "Typed Transition Audio",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 1152.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true,
|
||||
"media_references": {
|
||||
"DEFAULT_MEDIA": {
|
||||
"OTIO_SCHEMA": "ExternalReference.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"available_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 48000.0,
|
||||
"value": 0.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 48000.0,
|
||||
"value": 0.0
|
||||
}
|
||||
},
|
||||
"available_image_bounds": null,
|
||||
"target_url": "file:///tmp/Typed Transition.wav"
|
||||
}
|
||||
},
|
||||
"active_media_reference_key": "DEFAULT_MEDIA"
|
||||
},
|
||||
{
|
||||
"OTIO_SCHEMA": "Gap.1",
|
||||
"metadata": {},
|
||||
"name": "",
|
||||
"source_range": {
|
||||
"OTIO_SCHEMA": "TimeRange.1",
|
||||
"duration": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 1.0,
|
||||
"value": 12.0
|
||||
},
|
||||
"start_time": {
|
||||
"OTIO_SCHEMA": "RationalTime.1",
|
||||
"rate": 24.0,
|
||||
"value": 1152.0
|
||||
}
|
||||
},
|
||||
"effects": [],
|
||||
"markers": [],
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"kind": "Audio"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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/>.
|
||||
|
||||
//! Byte-for-byte round-trip tests against the golden files captured from
|
||||
//! the opentimelineio C++ writer: parse, re-serialize, and require the
|
||||
//! output to be identical (4-space indent, `": "` separators, inline empty
|
||||
//! containers, shortest floats, no trailing newline).
|
||||
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn read_golden(name: &str) -> String {
|
||||
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/data")
|
||||
.join(name);
|
||||
fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path:?}: {e}"))
|
||||
}
|
||||
|
||||
fn assert_round_trip(name: &str) {
|
||||
let text = read_golden(name);
|
||||
let doc = oakotio::from_json_string(&text).unwrap_or_else(|e| panic!("parse {name}: {e}"));
|
||||
let out = doc
|
||||
.to_json_string()
|
||||
.unwrap_or_else(|e| panic!("serialize {name}: {e}"));
|
||||
assert_eq!(out, text, "round-trip mismatch for {name}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_timeline_round_trips() {
|
||||
assert_round_trip("golden_timeline.json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_collection_round_trips() {
|
||||
assert_round_trip("golden_collection.json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_typed_transition_round_trips() {
|
||||
assert_round_trip("golden_typed_transition.json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floatfmt_round_trips() {
|
||||
assert_round_trip("floatfmt.json");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_timeline_parses_as_timeline_root() {
|
||||
let doc = oakotio::from_json_string(&read_golden("golden_timeline.json")).unwrap();
|
||||
assert_eq!(doc.schema_name(), "Timeline");
|
||||
assert!(doc.as_timeline().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_collection_parses_as_collection_root() {
|
||||
let doc = oakotio::from_json_string(&read_golden("golden_collection.json")).unwrap();
|
||||
assert_eq!(doc.schema_name(), "SerializableCollection");
|
||||
assert!(doc.as_collection().is_some());
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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/>.
|
||||
|
||||
//! Save-side parity: rebuild `golden_timeline.json` through the public
|
||||
//! builder API and require the serialized bytes to match the golden file
|
||||
//! byte for byte. This proves the writer (not just the reader) reproduces
|
||||
//! the opentimelineio C++ output.
|
||||
|
||||
use oakotio::{
|
||||
Clip, Composable, ExternalReference, Gap, MediaReference, RationalTime, TimeRange, Timeline,
|
||||
Track, Transition,
|
||||
};
|
||||
|
||||
fn build_golden_timeline() -> Timeline {
|
||||
let mut timeline = Timeline::new("My Sequence");
|
||||
|
||||
let mut video = Track::new("Video");
|
||||
|
||||
let mut clip = Clip::new("My Sequence Clip");
|
||||
clip.set_source_range(TimeRange::new(
|
||||
RationalTime::new(0.0, 24.0),
|
||||
RationalTime::new(1152.0, 24.0),
|
||||
));
|
||||
clip.set_media_reference(MediaReference::ExternalReference(ExternalReference::new(
|
||||
"file:///tmp/My Sequence.mp4",
|
||||
Some(TimeRange::new(
|
||||
RationalTime::new(0.0, 25.0),
|
||||
RationalTime::new(100.0, 25.0),
|
||||
)),
|
||||
)));
|
||||
video.append_child(Composable::Clip(clip));
|
||||
|
||||
video.append_child(Composable::Gap(Gap::new(
|
||||
TimeRange::new(RationalTime::new(0.0, 24.0), RationalTime::new(576.0, 24.0)),
|
||||
"My Sequence Gap",
|
||||
)));
|
||||
|
||||
let mut transition = Transition::new("My Sequence Transition");
|
||||
transition.set_in_offset(RationalTime::new(12.0, 24.0));
|
||||
transition.set_out_offset(RationalTime::new(12.0, 24.0));
|
||||
video.append_child(Composable::Transition(transition));
|
||||
|
||||
let mut audio = Track::new("Audio");
|
||||
|
||||
let mut audio_clip = Clip::new("My Sequence Audio");
|
||||
audio_clip.set_source_range(TimeRange::new(
|
||||
RationalTime::new(0.0, 24.0),
|
||||
RationalTime::new(1152.0, 24.0),
|
||||
));
|
||||
audio_clip.set_media_reference(MediaReference::ExternalReference(ExternalReference::new(
|
||||
"file:///tmp/My Sequence.wav",
|
||||
Some(TimeRange::new(
|
||||
RationalTime::new(0.0, 48000.0),
|
||||
RationalTime::new(0.0, 48000.0),
|
||||
)),
|
||||
)));
|
||||
audio.append_child(Composable::Clip(audio_clip));
|
||||
|
||||
audio.append_child(Composable::Gap(Gap::new(
|
||||
TimeRange::new(RationalTime::new(1152.0, 24.0), RationalTime::new(12.0, 1.0)),
|
||||
"",
|
||||
)));
|
||||
|
||||
timeline.tracks_mut().append_child(Composable::Track(video));
|
||||
timeline.tracks_mut().append_child(Composable::Track(audio));
|
||||
|
||||
timeline
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_timeline_matches_golden_bytes() {
|
||||
let golden = std::fs::read_to_string(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/golden_timeline.json"
|
||||
))
|
||||
.expect("read golden_timeline.json");
|
||||
|
||||
let built = build_golden_timeline();
|
||||
let out = built.to_json_string().expect("serialize built timeline");
|
||||
|
||||
assert_eq!(out, golden);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_timeline_reparses_identically() {
|
||||
// The builder output must also parse back into an equivalent graph.
|
||||
let built = build_golden_timeline();
|
||||
let out = built.to_json_string().unwrap();
|
||||
let reparsed = oakotio::from_json_string(&out).unwrap();
|
||||
assert_eq!(
|
||||
reparsed.as_timeline().unwrap().name(),
|
||||
"My Sequence",
|
||||
"reparsed timeline keeps the name"
|
||||
);
|
||||
assert_eq!(reparsed.as_timeline().unwrap().tracks().children().len(), 2);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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/>.
|
||||
|
||||
//! Semantic tests over `golden_timeline.json`: walk the parsed object graph
|
||||
//! and assert the values Oak's C++ load task depends on.
|
||||
|
||||
use oakotio::{Clip, ExternalReference, MediaReference, Serializable, Timeline};
|
||||
|
||||
fn golden_timeline() -> Timeline {
|
||||
let text = std::fs::read_to_string(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/data/golden_timeline.json"
|
||||
))
|
||||
.expect("read golden_timeline.json");
|
||||
match oakotio::from_json_string(&text).expect("parse golden_timeline.json") {
|
||||
Serializable::Timeline(t) => t,
|
||||
other => panic!("expected Timeline root, got {}", other.schema_name()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_timeline_fields() {
|
||||
let tl = golden_timeline();
|
||||
assert_eq!(tl.name(), "My Sequence");
|
||||
assert_eq!(tl.global_start_time(), None);
|
||||
assert_eq!(tl.tracks().children().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_track_contents() {
|
||||
let tl = golden_timeline();
|
||||
let video = &tl.tracks().children()[0];
|
||||
let track = video
|
||||
.as_track()
|
||||
.expect("first child of tracks stack is a Track");
|
||||
assert_eq!(track.kind(), "Video");
|
||||
assert_eq!(track.children().len(), 3);
|
||||
|
||||
// Clip -> Gap -> Transition, in order.
|
||||
let clip = track.children()[0]
|
||||
.as_clip()
|
||||
.expect("video track child 0 is a Clip");
|
||||
assert_eq!(clip.name(), "My Sequence Clip");
|
||||
let range = clip.source_range().expect("clip has a source_range");
|
||||
assert_eq!((range.duration().value(), range.duration().rate()), (1152.0, 24.0));
|
||||
assert_eq!((range.start_time().value(), range.start_time().rate()), (0.0, 24.0));
|
||||
|
||||
let gap = track.children()[1]
|
||||
.as_gap()
|
||||
.expect("video track child 1 is a Gap");
|
||||
assert_eq!(gap.name(), "My Sequence Gap");
|
||||
let grange = gap.source_range().expect("gap has a source_range");
|
||||
assert_eq!((grange.duration().value(), grange.duration().rate()), (576.0, 24.0));
|
||||
|
||||
let trans = track.children()[2]
|
||||
.as_transition()
|
||||
.expect("video track child 2 is a Transition");
|
||||
assert_eq!(trans.name(), "My Sequence Transition");
|
||||
assert_eq!((trans.in_offset().value(), trans.in_offset().rate()), (12.0, 24.0));
|
||||
assert_eq!((trans.out_offset().value(), trans.out_offset().rate()), (12.0, 24.0));
|
||||
assert_eq!(trans.transition_type(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_clip_media_reference() {
|
||||
let tl = golden_timeline();
|
||||
let clip = &tl.tracks().children()[0].as_track().unwrap().children()[0];
|
||||
let clip = clip.as_clip().unwrap();
|
||||
|
||||
let reference = clip.media_reference().expect("clip resolves a media reference");
|
||||
assert_eq!(reference.schema_name(), "ExternalReference");
|
||||
let external: &ExternalReference = match reference {
|
||||
MediaReference::ExternalReference(e) => e,
|
||||
other => panic!("expected ExternalReference, got {}", other.schema_name()),
|
||||
};
|
||||
assert_eq!(external.target_url(), "file:///tmp/My Sequence.mp4");
|
||||
let available = external.available_range().expect("available_range is set");
|
||||
assert_eq!((available.duration().value(), available.duration().rate()), (100.0, 25.0));
|
||||
assert_eq!((available.start_time().value(), available.start_time().rate()), (0.0, 25.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_track_contents() {
|
||||
let tl = golden_timeline();
|
||||
let audio = &tl.tracks().children()[1];
|
||||
let track = audio
|
||||
.as_track()
|
||||
.expect("second child of tracks stack is a Track");
|
||||
assert_eq!(track.kind(), "Audio");
|
||||
assert_eq!(track.children().len(), 2);
|
||||
|
||||
let clip: &Clip = track.children()[0]
|
||||
.as_clip()
|
||||
.expect("audio track child 0 is a Clip");
|
||||
assert_eq!(clip.name(), "My Sequence Audio");
|
||||
let external: &ExternalReference = match clip.media_reference().unwrap() {
|
||||
MediaReference::ExternalReference(e) => e,
|
||||
other => panic!("expected ExternalReference, got {}", other.schema_name()),
|
||||
};
|
||||
assert_eq!(external.target_url(), "file:///tmp/My Sequence.wav");
|
||||
let available = external.available_range().unwrap();
|
||||
assert_eq!((available.duration().value(), available.duration().rate()), (0.0, 48000.0));
|
||||
assert_eq!((available.start_time().value(), available.start_time().rate()), (0.0, 48000.0));
|
||||
|
||||
let gap = track.children()[1]
|
||||
.as_gap()
|
||||
.expect("audio track child 1 is a Gap");
|
||||
assert_eq!(gap.name(), "");
|
||||
let grange = gap.source_range().unwrap();
|
||||
assert_eq!((grange.duration().value(), grange.duration().rate()), (12.0, 1.0));
|
||||
assert_eq!((grange.start_time().value(), grange.start_time().rate()), (1152.0, 24.0));
|
||||
}
|
||||
@@ -2,6 +2,7 @@ target_sources(oakcodec PRIVATE
|
||||
conform.cpp
|
||||
decoder.cpp
|
||||
encoder.cpp
|
||||
format.cpp
|
||||
frame.cpp
|
||||
proxy.cpp
|
||||
)
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
/***
|
||||
|
||||
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 "codec/format.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "encoder.h"
|
||||
#include "ffmpeg/ffmpegencoder.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
bool valid_format(int format)
|
||||
{
|
||||
return format >= 0 && format < olive::ExportFormat::k_format_count;
|
||||
}
|
||||
|
||||
bool valid_codec(int codec)
|
||||
{
|
||||
return codec >= 0 && codec < olive::ExportCodec::k_codec_count;
|
||||
}
|
||||
|
||||
// buf/size convention: returns the would-be length INCLUDING the NUL
|
||||
// (include/codec/error.h), unlike the facade which excludes it.
|
||||
int string_out(const std::string &s, char *buf, int buf_size)
|
||||
{
|
||||
int need = static_cast<int>(s.size()) + 1;
|
||||
if (buf && buf_size > 0) {
|
||||
int n = std::min(static_cast<int>(s.size()), buf_size - 1);
|
||||
memcpy(buf, s.data(), n);
|
||||
buf[n] = '\0';
|
||||
}
|
||||
return need;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/* ---- Container format / codec metadata ---------------------------------- */
|
||||
|
||||
int oakcodec_encoding_format_count(void)
|
||||
{
|
||||
return olive::ExportFormat::k_format_count;
|
||||
}
|
||||
|
||||
int oakcodec_encoding_format_name(int format, char *buf, int buf_size)
|
||||
{
|
||||
if (!valid_format(format)) {
|
||||
return OAKCODEC_E_INVALID;
|
||||
}
|
||||
return string_out(
|
||||
olive::ExportFormat::get_name(olive::ExportFormat::Format(format)), buf,
|
||||
buf_size);
|
||||
}
|
||||
|
||||
int oakcodec_encoding_format_extension(int format, char *buf, int buf_size)
|
||||
{
|
||||
if (!valid_format(format)) {
|
||||
return OAKCODEC_E_INVALID;
|
||||
}
|
||||
return string_out(
|
||||
olive::ExportFormat::get_extension(olive::ExportFormat::Format(format)),
|
||||
buf, buf_size);
|
||||
}
|
||||
|
||||
int oakcodec_encoding_format_video_codec_count(int format)
|
||||
{
|
||||
if (!valid_format(format)) {
|
||||
return OAKCODEC_E_INVALID;
|
||||
}
|
||||
return int(olive::ExportFormat::get_video_codecs(
|
||||
olive::ExportFormat::Format(format))
|
||||
.size());
|
||||
}
|
||||
|
||||
int oakcodec_encoding_format_video_codec_at(int format, int index)
|
||||
{
|
||||
if (!valid_format(format)) {
|
||||
return OAKCODEC_E_INVALID;
|
||||
}
|
||||
const auto l =
|
||||
olive::ExportFormat::get_video_codecs(olive::ExportFormat::Format(format));
|
||||
if (index < 0 || index >= int(l.size())) {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
return int(l[size_t(index)]);
|
||||
}
|
||||
|
||||
int oakcodec_encoding_format_audio_codec_count(int format)
|
||||
{
|
||||
if (!valid_format(format)) {
|
||||
return OAKCODEC_E_INVALID;
|
||||
}
|
||||
return int(olive::ExportFormat::get_audio_codecs(
|
||||
olive::ExportFormat::Format(format))
|
||||
.size());
|
||||
}
|
||||
|
||||
int oakcodec_encoding_format_audio_codec_at(int format, int index)
|
||||
{
|
||||
if (!valid_format(format)) {
|
||||
return OAKCODEC_E_INVALID;
|
||||
}
|
||||
const auto l =
|
||||
olive::ExportFormat::get_audio_codecs(olive::ExportFormat::Format(format));
|
||||
if (index < 0 || index >= int(l.size())) {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
return int(l[size_t(index)]);
|
||||
}
|
||||
|
||||
int oakcodec_encoding_format_subtitle_codec_count(int format)
|
||||
{
|
||||
if (!valid_format(format)) {
|
||||
return OAKCODEC_E_INVALID;
|
||||
}
|
||||
return int(olive::ExportFormat::get_subtitle_codecs(
|
||||
olive::ExportFormat::Format(format))
|
||||
.size());
|
||||
}
|
||||
|
||||
int oakcodec_encoding_format_subtitle_codec_at(int format, int index)
|
||||
{
|
||||
if (!valid_format(format)) {
|
||||
return OAKCODEC_E_INVALID;
|
||||
}
|
||||
const auto l = olive::ExportFormat::get_subtitle_codecs(
|
||||
olive::ExportFormat::Format(format));
|
||||
if (index < 0 || index >= int(l.size())) {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
return int(l[size_t(index)]);
|
||||
}
|
||||
|
||||
int oakcodec_encoding_codec_name(int codec, char *buf, int buf_size)
|
||||
{
|
||||
if (!valid_codec(codec)) {
|
||||
return OAKCODEC_E_INVALID;
|
||||
}
|
||||
return string_out(
|
||||
olive::ExportCodec::get_codec_name(olive::ExportCodec::Codec(codec)), buf,
|
||||
buf_size);
|
||||
}
|
||||
|
||||
int oakcodec_encoding_codec_is_still_image(int codec)
|
||||
{
|
||||
if (!valid_codec(codec)) {
|
||||
return 0;
|
||||
}
|
||||
return olive::ExportCodec::is_codec_a_still_image(
|
||||
olive::ExportCodec::Codec(codec)) ?
|
||||
1 :
|
||||
0;
|
||||
}
|
||||
|
||||
int oakcodec_encoding_codec_is_lossless(int codec)
|
||||
{
|
||||
if (!valid_codec(codec)) {
|
||||
return 0;
|
||||
}
|
||||
return olive::ExportCodec::is_codec_lossless(olive::ExportCodec::Codec(codec)) ?
|
||||
1 :
|
||||
0;
|
||||
}
|
||||
|
||||
int oakcodec_encoding_pix_fmt_count(int format, int codec)
|
||||
{
|
||||
if (!valid_format(format) || !valid_codec(codec)) {
|
||||
return OAKCODEC_E_INVALID;
|
||||
}
|
||||
return int(olive::ExportFormat::get_pixel_formats_for_codec(
|
||||
olive::ExportFormat::Format(format),
|
||||
olive::ExportCodec::Codec(codec))
|
||||
.size());
|
||||
}
|
||||
|
||||
int oakcodec_encoding_pix_fmt_at(int format, int codec, int index, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!valid_format(format) || !valid_codec(codec)) {
|
||||
return OAKCODEC_E_INVALID;
|
||||
}
|
||||
const auto l = olive::ExportFormat::get_pixel_formats_for_codec(
|
||||
olive::ExportFormat::Format(format), olive::ExportCodec::Codec(codec));
|
||||
if (index < 0 || index >= int(l.size())) {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
return string_out(l[size_t(index)], buf, buf_size);
|
||||
}
|
||||
|
||||
int oakcodec_encoding_pix_fmt_index(int codec, const char *pix_fmt)
|
||||
{
|
||||
if (!valid_codec(codec) || !pix_fmt || !pix_fmt[0]) {
|
||||
return 0;
|
||||
}
|
||||
// Mirrors the facade: query the FFmpeg encoder's list directly (the
|
||||
// pixel-format list depends on the codec alone, not the container).
|
||||
olive::FFmpegEncoder probe{ olive::EncodingParams() };
|
||||
const auto l =
|
||||
probe.get_pixel_formats_for_codec(olive::ExportCodec::Codec(codec));
|
||||
const std::string needle(pix_fmt);
|
||||
const auto it = std::find(l.begin(), l.end(), needle);
|
||||
return it != l.end() ? int(it - l.begin()) : 0;
|
||||
}
|
||||
|
||||
int oakcodec_encoding_sample_format_count(int format, int codec)
|
||||
{
|
||||
if (!valid_format(format) || !valid_codec(codec)) {
|
||||
return OAKCODEC_E_INVALID;
|
||||
}
|
||||
return int(olive::ExportFormat::get_sample_formats_for_codec(
|
||||
olive::ExportFormat::Format(format),
|
||||
olive::ExportCodec::Codec(codec))
|
||||
.size());
|
||||
}
|
||||
|
||||
int oakcodec_encoding_sample_format_at(int format, int codec, int index)
|
||||
{
|
||||
if (!valid_format(format) || !valid_codec(codec)) {
|
||||
return OAKCODEC_E_INVALID;
|
||||
}
|
||||
const auto l = olive::ExportFormat::get_sample_formats_for_codec(
|
||||
olive::ExportFormat::Format(format), olive::ExportCodec::Codec(codec));
|
||||
if (index < 0 || index >= int(l.size())) {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
return int(l[size_t(index)]);
|
||||
}
|
||||
|
||||
/* ---- Image-sequence filename helpers ------------------------------------ */
|
||||
|
||||
int oakcodec_encoding_filename_contains_digit_placeholder(const char *filename)
|
||||
{
|
||||
if (!filename) {
|
||||
return 0;
|
||||
}
|
||||
return olive::Encoder::filename_contains_digit_placeholder(filename) ? 1 :
|
||||
0;
|
||||
}
|
||||
|
||||
int oakcodec_encoding_image_sequence_digit_count(const char *filename)
|
||||
{
|
||||
if (!filename) {
|
||||
return 0;
|
||||
}
|
||||
return olive::Encoder::get_image_sequence_placeholder_digit_count(filename);
|
||||
}
|
||||
|
||||
int oakcodec_encoding_filename_remove_digit_placeholder(const char *filename,
|
||||
char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!filename) {
|
||||
return OAKCODEC_E_INVALID;
|
||||
}
|
||||
return string_out(
|
||||
olive::Encoder::filename_remove_digit_placeholder(filename), buf,
|
||||
buf_size);
|
||||
}
|
||||
Generated
+290
@@ -0,0 +1,290 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.72.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex 1.3.0",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex 2.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
|
||||
|
||||
[[package]]
|
||||
name = "ffmpeg-next"
|
||||
version = "9.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6380599799e175191eb7ffe82c97f36a2a90a36cbc54c738a903e5287d7f516a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"ffmpeg-sys-next",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ffmpeg-sys-next"
|
||||
version = "9.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b939bf79dd5949412a4b81cfe21a07f48ea21b47fcbb5f57816c8c2de5ae30b"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
"libc",
|
||||
"num_cpus",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oakcodec"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ffmpeg-next",
|
||||
"oakcore-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oakcore-rs"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "oakcodec"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Oak Video Editor media codec module (Rust)"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
# FFI discipline: panics must be catchable at every exported entry.
|
||||
panic = "unwind"
|
||||
|
||||
[dependencies]
|
||||
oakcore-rs = { path = "../../oakcore-rs" }
|
||||
# Real media decode/encode. The C++ ffmpeg_bridge library existed only to
|
||||
# absorb FFmpeg API churn; the Rust crate calls ffmpeg-next directly.
|
||||
ffmpeg-next = "9"
|
||||
@@ -0,0 +1,123 @@
|
||||
# oakcodec Rust crate
|
||||
|
||||
> Status: **implemented**. Implements `include/codec/*.h` verbatim
|
||||
> (`src/ffi/`); every export has success + failure-path tests
|
||||
> (`cargo test`: unit tests in `src/ffi/*.rs`, the contract tests in
|
||||
> `tests/`, and real-media tests in `src/realmedia_tests.rs`). The FFmpeg
|
||||
> engine is fully implemented through the [`ffmpeg-next`] crate (decode,
|
||||
> probe, audio conform, encode); the OIIO engine remains a stub in this
|
||||
> build. This crate mirrors the `src/node/rust/` template (same FFI
|
||||
> discipline, same testing layers).
|
||||
|
||||
## Scope
|
||||
|
||||
Replaces the C++ oakcodec module (`src/codec/src`, ~10k lines): CPU
|
||||
frame buffers (`Frame`), the frame pool (`FrameManager`), media
|
||||
decoders/encoders with their FFmpeg and OIIO implementations, audio
|
||||
conform and proxy generation managers, export format/codec tables,
|
||||
encoding parameters, and the background-task submit hook.
|
||||
|
||||
Public contract: `include/codec/*.h` (7 headers: frame.h, decoder.h,
|
||||
encoder.h, conform.h, proxy.h, task.h, error.h) — frozen, implemented
|
||||
verbatim by `src/ffi.rs`. Interim state (pre-M8) is documented in
|
||||
`src/codec/NOTES.md`: conform/proxy work is delegated to the global
|
||||
task submit callback and otherwise reports unavailable, never crashes
|
||||
and never blocks.
|
||||
|
||||
## Key architectural decisions (C++ → Rust mapping)
|
||||
|
||||
1. **`shared_ptr` → refcounted `RefBox` handle.** The C++ `Frame`/
|
||||
`Decoder`/`Encoder` objects are heap boxes behind the neutral
|
||||
by-value handle struct `{ctx, addref, release, abi_version}` (see
|
||||
`handle.rs`), exactly as oaknode/oakplugin do. Handles are
|
||||
deliberately duplicated per module: the function pointers always
|
||||
point into the DLL that created the object.
|
||||
2. **Inheritance → traits.** The C++ `Decoder`/`Encoder` abstract
|
||||
bases plus their FFmpeg/OIIO subclasses become a Rust trait with
|
||||
two implementors. The probe/dispatch (decide which implementation
|
||||
recognizes a file) stays in `decoder.rs`. `Encoder`'s per-codec
|
||||
`PixelFormat`/`SampleFormat` support is a trait query, not a
|
||||
virtual chain.
|
||||
3. **`Frame` owns its params by value.** `olive::Frame` wraps an
|
||||
`OakVideoParams` handle (an oakcommon by-value handle, NOT owned by
|
||||
codec) plus a `Vec<u8>` pixel buffer. In Rust the params are held as
|
||||
the oakcommon handle (refcounted through `bridge::common`) so the
|
||||
byte-level ABI stays unchanged; the buffer is a plain `Vec<u8>`.
|
||||
4. **No adapter layer.** Codec calls other modules' C ABIs directly
|
||||
(`bridge/common.rs`, `bridge/render.rs`), keeping the 2026-08
|
||||
decision recorded in NOTES.md §6. Only genuinely repeated
|
||||
conversions survive as small module-local helpers (e.g.
|
||||
`fill_render_params`, `cancel_atom_is_cancelled`).
|
||||
5. **XML stays on the C++ side.** `EncodingParams::load/save` use
|
||||
oakcommon's C++ `XmlStreamWriter/Reader` classes
|
||||
(`src/common/src/xmlutils.h`), exactly as oaknode/oakrender do —
|
||||
the one C++-to-C++ coupling the bridge cannot cover (NOTES.md §7).
|
||||
6. **Threading.** `FrameManager` keeps its background GC thread behind
|
||||
a `Mutex`; the C++ code's reliance on Qt's event thread is gone. The
|
||||
threading contract is documented per function.
|
||||
7. **Enum values are the C contract.** `ExportFormat::Format`,
|
||||
`ExportCodec::Codec`, `Interlacing`, `VideoScalingMethod`,
|
||||
`SampleFormat::Format` all stay as the raw int values the C ABI
|
||||
documents (oakengine/encoding.h), so `ffi.rs` marshals them without
|
||||
translation.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/
|
||||
lib.rs crate doc + module map
|
||||
error.rs error codes (mirrors include/codec/error.h)
|
||||
handle.rs refcounted-handle scaffolding (same pattern as node)
|
||||
frame.rs Frame (CPU pixel buffer + OakVideoParams handle)
|
||||
framemanager.rs FrameManager (buffer pool + background GC thread)
|
||||
decoder.rs Decoder trait + CodecStream + RenderMode + probe
|
||||
ffmpeg.rs FFmpegDecoder / FFmpegEncoder (ffmpeg-next)
|
||||
oiio.rs OIIODecoder / OIIOEncoder (OpenImageIO)
|
||||
oiioframebridge.rs oiioutils frame<->buffer conversion
|
||||
encoder.rs Encoder trait (abstract base)
|
||||
encodingparams.rs EncodingParams (flattened ABI POD + generate_matrix)
|
||||
exportcodec.rs ExportCodec enum + codec-name table
|
||||
exportformat.rs ExportFormat enum + extension/format table
|
||||
conformmanager.rs ConformManager (stateless, task-callback driven)
|
||||
proxymanager.rs ProxyManager (stateless, task-callback driven)
|
||||
task.rs OakCodecTaskKind / OakCodecTaskRequest / submit hook
|
||||
timecodemetadata.rs TimecodeMetadata (SMPTE/BWF parsers)
|
||||
footagedescription.rs FootageDescription (codec-internal stream desc)
|
||||
planarfiledevice.rs PlanarFileDevice (stdio plane-channel I/O)
|
||||
realmedia_tests.rs real-media tests (demo.mp4, H.264 round-trip)
|
||||
bridge/ C ABI imports: common.rs, render.rs
|
||||
ffi.rs include/codec/*.h export layer
|
||||
tests/ contract + golden tests (see test section below)
|
||||
```
|
||||
|
||||
## Hard rules for the implementer
|
||||
|
||||
1. Every `extern "C"` body goes through `handle::guard*`; no panic
|
||||
crosses FFI.
|
||||
2. The handle is the only way out of the crate; the public API never
|
||||
hands out raw `&Frame`/`&Decoder` references.
|
||||
3. Behavior parity with C++ is proven by the unchanged C ABI test
|
||||
suite (`src/codec/tests`) plus the contract tests in `tests/`.
|
||||
4. Where C++ behavior is genuinely load-bearing but ugly, port the
|
||||
behavior, not the aesthetics; leave a `// CPP-PARITY:` comment with
|
||||
the C++ file:line.
|
||||
|
||||
## Dependency policy
|
||||
|
||||
Prefer mature third-party crates (MIT/Apache-2.0/BSD, GPL-compatible)
|
||||
over hand-rolling; register each addition (name + reason) here. Large
|
||||
existing C++ libraries (OTIO, OCIO, OIIO, FFmpeg) are NEVER rewritten
|
||||
— they are consumed through their C ABI / bridge layers.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- `oakcore-rs` (path) — oakcore value types (Rational, TimeRange,
|
||||
PixelFormat/SampleFormat) mirrored as Rust enums.
|
||||
- `ffmpeg-next` 9 — the FFmpeg decode/encode engine. The C++
|
||||
`ffmpeg_bridge` library (`liboakffmpeg`) existed only to absorb FFmpeg
|
||||
API churn; the Rust crate calls `ffmpeg-next` directly (per the 2026-08
|
||||
decision that dropped the binding-library plan). `ffmpeg-next` builds
|
||||
against the system FFmpeg via `ffmpeg-sys-next` (bindgen); the
|
||||
implementation dips into `ffmpeg-sys-next` (`ffmpeg::ffi`) only for
|
||||
swscale/swresample details and channel-layout construction that the safe
|
||||
wrapper does not expose.
|
||||
@@ -0,0 +1,341 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakcommon / oakcore C ABI imports (videoparams, audioparams, rational,
|
||||
//! subtitleparams, config, filefunctions, ffmpegutils, oiioutils,
|
||||
//! colortransform).
|
||||
//!
|
||||
//! The by-value handle structs (`OakVideoParams`, `OakAudioParams`,
|
||||
//! `OakSubtitleParams`, `OakNodeBlock`) mirror the `{ctx, addref,
|
||||
//! release, abi_version}` layout from `include/common/handle.h`, so the
|
||||
//! codec module can store them by value and pass them straight across
|
||||
//! the FFI boundary. Function signatures match the public headers
|
||||
//! verbatim; symbols resolve at link time.
|
||||
//!
|
||||
//! The oakcore audio parameters use a pointer-based C ABI instead of the
|
||||
//! by-value handle convention: `oakcore_audioparams_*` take and return
|
||||
//! `OakAudioParams *` / `OakRational *` pointers (`core/include/olive/
|
||||
//! core/oakcore/audioparams.h`, `rational.h`). Those are bridged as raw
|
||||
//! pointers to the crate's handle structs; `oakcore_audioparams_time_base`
|
||||
//! returns a newly allocated rational the caller must release with
|
||||
//! `oakcore_rational_free`.
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
/// `OakVideoParams` — refcounted video-parameter handle.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct OakVideoParams {
|
||||
/// Opaque object pointer.
|
||||
pub ctx: *mut c_void,
|
||||
/// Atomically increments the reference count.
|
||||
pub addref: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// Decrements the count, destroys at zero.
|
||||
pub release: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// ABI version (see [`OAKCODEC_ABI_VERSION`]).
|
||||
pub abi_version: u32,
|
||||
}
|
||||
|
||||
/// `OakAudioParams` — refcounted audio-parameter handle.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct OakAudioParams {
|
||||
/// Opaque object pointer.
|
||||
pub ctx: *mut c_void,
|
||||
/// Atomically increments the reference count.
|
||||
pub addref: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// Decrements the count, destroys at zero.
|
||||
pub release: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// ABI version.
|
||||
pub abi_version: u32,
|
||||
}
|
||||
|
||||
/// `OakSubtitleParams` — refcounted subtitle-parameter handle.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct OakSubtitleParams {
|
||||
/// Opaque object pointer.
|
||||
pub ctx: *mut c_void,
|
||||
/// Atomically increments the reference count.
|
||||
pub addref: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// Decrements the count, destroys at zero.
|
||||
pub release: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// ABI version.
|
||||
pub abi_version: u32,
|
||||
}
|
||||
|
||||
/// `OakNodeBlock` — opaque node-block handle (owned elsewhere; codec
|
||||
/// only stores and forwards it).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct OakNodeBlock {
|
||||
/// Opaque object pointer.
|
||||
pub ctx: *mut c_void,
|
||||
/// Atomically increments the reference count.
|
||||
pub addref: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// Decrements the count, destroys at zero.
|
||||
pub release: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// ABI version.
|
||||
pub abi_version: u32,
|
||||
}
|
||||
|
||||
// The handle structs are opaque refcounted handles pointing into a C
|
||||
// library; the boxed objects are independently synchronized there, so
|
||||
// moving a handle between threads is sound.
|
||||
unsafe impl Send for OakVideoParams {}
|
||||
unsafe impl Sync for OakVideoParams {}
|
||||
unsafe impl Send for OakAudioParams {}
|
||||
unsafe impl Sync for OakAudioParams {}
|
||||
unsafe impl Send for OakSubtitleParams {}
|
||||
unsafe impl Sync for OakSubtitleParams {}
|
||||
unsafe impl Send for OakNodeBlock {}
|
||||
unsafe impl Sync for OakNodeBlock {}
|
||||
|
||||
extern "C" {
|
||||
/// `oakcommon_videoparams_init`.
|
||||
pub fn oakcommon_videoparams_init() -> OakVideoParams;
|
||||
/// `oakcommon_videoparams_init_basic`.
|
||||
pub fn oakcommon_videoparams_init_basic(width: c_int, height: c_int) -> OakVideoParams;
|
||||
/// `oakcommon_videoparams_init_with_time_base`.
|
||||
pub fn oakcommon_videoparams_init_with_time_base(
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
time_base_num: i64,
|
||||
time_base_den: i64,
|
||||
) -> OakVideoParams;
|
||||
/// `oakcommon_videoparams_free` (NULL/empty no-op).
|
||||
pub fn oakcommon_videoparams_free(params: *mut OakVideoParams);
|
||||
/// `oakcommon_videoparams_get_width`.
|
||||
pub fn oakcommon_videoparams_get_width(params: OakVideoParams) -> c_int;
|
||||
/// `oakcommon_videoparams_get_height`.
|
||||
pub fn oakcommon_videoparams_get_height(params: OakVideoParams) -> c_int;
|
||||
/// `oakcommon_videoparams_get_format`.
|
||||
pub fn oakcommon_videoparams_get_format(params: OakVideoParams) -> c_int;
|
||||
/// `oakcommon_videoparams_get_time_base` (num/den out).
|
||||
pub fn oakcommon_videoparams_get_time_base(
|
||||
params: OakVideoParams,
|
||||
out_num: *mut i64,
|
||||
out_den: *mut i64,
|
||||
) -> c_int;
|
||||
/// `oakcommon_videoparams_set_width`.
|
||||
pub fn oakcommon_videoparams_set_width(params: OakVideoParams, width: c_int);
|
||||
/// `oakcommon_videoparams_set_height`.
|
||||
pub fn oakcommon_videoparams_set_height(params: OakVideoParams, height: c_int);
|
||||
/// `oakcommon_videoparams_set_format`.
|
||||
pub fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int);
|
||||
/// `oakcommon_videoparams_get_is_valid`.
|
||||
pub fn oakcommon_videoparams_get_is_valid(params: OakVideoParams) -> c_int;
|
||||
/// `oakcommon_videoparams_equals`.
|
||||
pub fn oakcommon_videoparams_equals(a: OakVideoParams, b: OakVideoParams) -> c_int;
|
||||
/// `oakcommon_videoparams_set_time_base`.
|
||||
pub fn oakcommon_videoparams_set_time_base(
|
||||
params: OakVideoParams,
|
||||
num: i64,
|
||||
den: i64,
|
||||
);
|
||||
/// `oakcommon_videoparams_set_frame_rate`.
|
||||
pub fn oakcommon_videoparams_set_frame_rate(
|
||||
params: OakVideoParams,
|
||||
num: i64,
|
||||
den: i64,
|
||||
);
|
||||
/// `oakcommon_videoparams_set_pixel_aspect_ratio`.
|
||||
pub fn oakcommon_videoparams_set_pixel_aspect_ratio(
|
||||
params: OakVideoParams,
|
||||
num: i64,
|
||||
den: i64,
|
||||
);
|
||||
/// `oakcommon_videoparams_set_interlacing`.
|
||||
pub fn oakcommon_videoparams_set_interlacing(params: OakVideoParams, interlacing: c_int);
|
||||
/// `oakcommon_videoparams_set_duration`.
|
||||
pub fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64);
|
||||
/// `oakcommon_videoparams_set_start_time`.
|
||||
pub fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start_time: i64);
|
||||
/// `oakcommon_videoparams_set_color_range`.
|
||||
pub fn oakcommon_videoparams_set_color_range(params: OakVideoParams, color_range: c_int);
|
||||
/// `oakcommon_videoparams_set_video_type`.
|
||||
pub fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int);
|
||||
/// `oakcommon_videoparams_set_channel_count`.
|
||||
pub fn oakcommon_videoparams_set_channel_count(params: OakVideoParams, channels: c_int);
|
||||
/// `oakcommon_videoparams_set_color_primaries`.
|
||||
pub fn oakcommon_videoparams_set_color_primaries(params: OakVideoParams, primaries: c_int);
|
||||
/// `oakcommon_videoparams_set_color_transfer`.
|
||||
pub fn oakcommon_videoparams_set_color_transfer(params: OakVideoParams, transfer: c_int);
|
||||
/// `oakcommon_videoparams_set_premultiplied_alpha`.
|
||||
pub fn oakcommon_videoparams_set_premultiplied_alpha(params: OakVideoParams, premultiplied: c_int);
|
||||
/// `oakcommon_videoparams_set_enabled`.
|
||||
pub fn oakcommon_videoparams_set_enabled(params: OakVideoParams, enabled: c_int);
|
||||
/// `oakcommon_videoparams_static_get_bytes_per_pixel`.
|
||||
pub fn oakcommon_videoparams_static_get_bytes_per_pixel(format: c_int) -> c_int;
|
||||
/// `oakcommon_videoparams_frame_rate_as_time_base`.
|
||||
pub fn oakcommon_videoparams_frame_rate_as_time_base(
|
||||
frame_rate_num: i64,
|
||||
frame_rate_den: i64,
|
||||
out_num: *mut i64,
|
||||
out_den: *mut i64,
|
||||
);
|
||||
/// `oakcommon_videoparams_get_stream_index`.
|
||||
pub fn oakcommon_videoparams_get_stream_index(params: OakVideoParams) -> c_int;
|
||||
/// `oakcommon_videoparams_set_stream_index`.
|
||||
pub fn oakcommon_videoparams_set_stream_index(params: OakVideoParams, index: c_int);
|
||||
/// `oakcommon_videoparams_get_divider`.
|
||||
pub fn oakcommon_videoparams_get_divider(params: OakVideoParams) -> c_int;
|
||||
/// `oakcommon_videoparams_set_divider`.
|
||||
pub fn oakcommon_videoparams_set_divider(params: OakVideoParams, divider: c_int);
|
||||
// NOTE: the remaining video getters below take the value-style form the
|
||||
// crate's existing bridge uses (the real oakcommon headers use out-pointer
|
||||
// args); `get_frame_rate` needs both values so it keeps the out pair.
|
||||
/// `oakcommon_videoparams_get_frame_rate` (frame-rate num/den out).
|
||||
pub fn oakcommon_videoparams_get_frame_rate(
|
||||
params: OakVideoParams,
|
||||
out_num: *mut c_int,
|
||||
out_den: *mut c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_videoparams_get_duration` (time-base units).
|
||||
pub fn oakcommon_videoparams_get_duration(params: OakVideoParams) -> i64;
|
||||
/// `oakcommon_videoparams_get_channel_count`.
|
||||
pub fn oakcommon_videoparams_get_channel_count(params: OakVideoParams) -> c_int;
|
||||
/// `oakcommon_videoparams_get_color_primaries`.
|
||||
pub fn oakcommon_videoparams_get_color_primaries(params: OakVideoParams) -> c_int;
|
||||
/// `oakcommon_videoparams_get_color_transfer`.
|
||||
pub fn oakcommon_videoparams_get_color_transfer(params: OakVideoParams) -> c_int;
|
||||
/// `oakcommon_videoparams_get_interlacing` (`Interlacing` value).
|
||||
pub fn oakcommon_videoparams_get_interlacing(params: OakVideoParams) -> c_int;
|
||||
/// `oakcore_audioparams_create` (pointer-based; timebase 1/sample_rate).
|
||||
pub fn oakcore_audioparams_create(
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
format: c_int,
|
||||
) -> *mut OakAudioParams;
|
||||
/// `oakcore_audioparams_free` (NULL no-op).
|
||||
pub fn oakcore_audioparams_free(params: *mut OakAudioParams);
|
||||
/// `oakcore_audioparams_sample_rate`.
|
||||
pub fn oakcore_audioparams_sample_rate(params: *const OakAudioParams) -> c_int;
|
||||
/// `oakcore_audioparams_set_sample_rate`.
|
||||
pub fn oakcore_audioparams_set_sample_rate(params: *mut OakAudioParams, sample_rate: c_int);
|
||||
/// `oakcore_audioparams_channel_layout`.
|
||||
pub fn oakcore_audioparams_channel_layout(params: *const OakAudioParams) -> u64;
|
||||
/// `oakcore_audioparams_set_channel_layout`.
|
||||
pub fn oakcore_audioparams_set_channel_layout(params: *mut OakAudioParams, layout: u64);
|
||||
/// `oakcore_audioparams_set_time_base`.
|
||||
pub fn oakcore_audioparams_set_time_base(
|
||||
params: *mut OakAudioParams,
|
||||
num: c_int,
|
||||
den: c_int,
|
||||
);
|
||||
/// `oakcore_audioparams_set_format`.
|
||||
pub fn oakcore_audioparams_set_format(params: *mut OakAudioParams, format: c_int);
|
||||
/// `oakcore_audioparams_set_stream_index`.
|
||||
pub fn oakcore_audioparams_set_stream_index(params: *mut OakAudioParams, index: c_int);
|
||||
/// `oakcore_audioparams_set_duration`.
|
||||
pub fn oakcore_audioparams_set_duration(params: *mut OakAudioParams, duration: i64);
|
||||
/// `oakcore_audioparams_channel_count`.
|
||||
pub fn oakcore_audioparams_channel_count(params: *const OakAudioParams) -> c_int;
|
||||
/// `oakcore_audioparams_format`.
|
||||
pub fn oakcore_audioparams_format(params: *const OakAudioParams) -> c_int;
|
||||
/// `oakcore_audioparams_stream_index`.
|
||||
pub fn oakcore_audioparams_stream_index(params: *const OakAudioParams) -> c_int;
|
||||
/// `oakcore_audioparams_duration`.
|
||||
pub fn oakcore_audioparams_duration(params: *const OakAudioParams) -> i64;
|
||||
/// `oakcore_audioparams_is_valid`.
|
||||
pub fn oakcore_audioparams_is_valid(params: *const OakAudioParams) -> c_int;
|
||||
/// `oakcore_audioparams_time_base` (newly allocated rational; caller
|
||||
/// releases with `oakcore_rational_free`).
|
||||
pub fn oakcore_audioparams_time_base(params: *const OakAudioParams) -> *mut c_void;
|
||||
/// `oakcore_rational_numerator`.
|
||||
pub fn oakcore_rational_numerator(rational: *const c_void) -> c_int;
|
||||
/// `oakcore_rational_denominator`.
|
||||
pub fn oakcore_rational_denominator(rational: *const c_void) -> c_int;
|
||||
/// `oakcore_rational_free` (NULL no-op).
|
||||
pub fn oakcore_rational_free(rational: *mut c_void);
|
||||
/// `oakcommon_subtitleparams_get_stream_index`.
|
||||
pub fn oakcommon_subtitleparams_get_stream_index(params: OakSubtitleParams) -> c_int;
|
||||
/// `oakcommon_subtitleparams_generate_ass_header`.
|
||||
pub fn oakcommon_subtitleparams_generate_ass_header(
|
||||
params: OakSubtitleParams,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
);
|
||||
/// `oakcommon_subtitleparams_add_subtitle`.
|
||||
pub fn oakcommon_subtitleparams_add_subtitle(params: OakSubtitleParams, text: *const c_char);
|
||||
/// `oakcommon_config_get_int`.
|
||||
pub fn oakcommon_config_get_int(group: *const c_char, key: *const c_char, default: c_int) -> c_int;
|
||||
/// `oakcommon_config_get_bool`.
|
||||
pub fn oakcommon_config_get_bool(group: *const c_char, key: *const c_char, default: c_int) -> c_int;
|
||||
/// `oakcommon_config_get` (two-stage string access).
|
||||
pub fn oakcommon_config_get(
|
||||
group: *const c_char,
|
||||
key: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_filefunctions_init`.
|
||||
pub fn oakcommon_filefunctions_init();
|
||||
/// `oakcommon_filefunctions_get_configuration_location` (two-stage).
|
||||
pub fn oakcommon_filefunctions_get_configuration_location(
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_filefunctions_get_unique_file_identifier`.
|
||||
pub fn oakcommon_filefunctions_get_unique_file_identifier(path: *const c_char) -> i64;
|
||||
/// `oakcommon_filefunctions_get_application_path` (two-stage).
|
||||
pub fn oakcommon_filefunctions_get_application_path(
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_filefunctions_free` (frees an internally cached string).
|
||||
pub fn oakcommon_filefunctions_free(ptr: *mut c_void);
|
||||
/// `oakcommon_colortransform_init_output`.
|
||||
pub fn oakcommon_colortransform_init_output(
|
||||
src_colorspace: c_int,
|
||||
src_trc: c_int,
|
||||
dst_colorspace: c_int,
|
||||
dst_trc: c_int,
|
||||
premultiplied: c_int,
|
||||
chroma_coeffs: *const c_void,
|
||||
) -> OakVideoParams;
|
||||
/// `oakcommon_colortransform_get_output`.
|
||||
pub fn oakcommon_colortransform_get_output(params: OakVideoParams, out: *mut OakVideoParams);
|
||||
/// `oakcommon_colortransform_free`.
|
||||
pub fn oakcommon_colortransform_free(params: *mut OakVideoParams);
|
||||
/// `oakcommon_ffmpegutils_get_native_sample_format`.
|
||||
pub fn oakcommon_ffmpegutils_get_native_sample_format(sample_format: c_int) -> c_int;
|
||||
/// `oakcommon_ffmpegutils_get_compatible_pixel_format`.
|
||||
pub fn oakcommon_ffmpegutils_get_compatible_pixel_format(format: c_int) -> c_int;
|
||||
/// `oakcommon_ffmpegutils_get_ffmpeg_pixel_format`.
|
||||
pub fn oakcommon_ffmpegutils_get_ffmpeg_pixel_format(format: c_int) -> c_int;
|
||||
/// `oakcommon_ffmpegutils_get_ffmpeg_sample_format`.
|
||||
pub fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(format: c_int) -> c_int;
|
||||
/// `oakcommon_ffmpegutils_get_compatible_bridge_pixel_format`.
|
||||
pub fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(format: c_int) -> c_int;
|
||||
/// `oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space`.
|
||||
pub fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(format: c_int) -> c_int;
|
||||
/// `oakcommon_oiioutils_init`.
|
||||
pub fn oakcommon_oiioutils_init();
|
||||
/// `oakcommon_oiioutils_get_oiio_base_type_from_format`.
|
||||
pub fn oakcommon_oiioutils_get_oiio_base_type_from_format(format: c_int) -> c_int;
|
||||
/// `oakcommon_oiioutils_get_format_from_oiio_basetype`.
|
||||
pub fn oakcommon_oiioutils_get_format_from_oiio_basetype(basetype: c_int) -> c_int;
|
||||
/// `oakcommon_oiioutils_get_pixel_aspect_ratio` (num/den out).
|
||||
pub fn oakcommon_oiioutils_get_pixel_aspect_ratio(
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
out_num: *mut c_int,
|
||||
out_den: *mut c_int,
|
||||
) -> c_int;
|
||||
/// `oakcommon_oiioutils_free`.
|
||||
pub fn oakcommon_oiioutils_free();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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/>.
|
||||
|
||||
//! C ABI imports from the other oak modules.
|
||||
//!
|
||||
//! The codec module links against oakcommon and oakrender at the C ABI.
|
||||
//! Every signature below mirrors the corresponding public header
|
||||
//! verbatim and is resolved at link time. The by-value handle structs
|
||||
//! (`OakVideoParams`, `OakRenderTexture`, …) are `#[repr(C)]` mirrors of
|
||||
//! the `{ctx, addref, release, abi_version}` layout so the codec crate
|
||||
//! can hold and hand them across the FFI boundary without translation.
|
||||
|
||||
pub mod common;
|
||||
pub mod render;
|
||||
|
||||
// In-memory mocks for the oakcommon/oakrender C ABI so the crate links
|
||||
// and is testable under `cargo test` (where those dylibs are absent).
|
||||
#[cfg(test)]
|
||||
pub mod test_stubs;
|
||||
@@ -0,0 +1,190 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakrender C ABI imports (display textures, renderers, cancel atoms).
|
||||
//!
|
||||
//! The OIIO/FFmpeg decoders push frames to a `DisplayTexture` and poll a
|
||||
//! `CancelAtom`; both are oakrender refcounted handles with the standard
|
||||
//! `{ctx, addref, release, abi_version}` layout. `oakrender_video_params`
|
||||
//! is a flattened POD the decoders construct to describe the frame.
|
||||
|
||||
use std::ffi::{c_char, c_int, c_void};
|
||||
|
||||
/// `OakRenderTexture` — refcounted GPU texture handle.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct OakRenderTexture {
|
||||
/// Opaque object pointer.
|
||||
pub ctx: *mut c_void,
|
||||
/// Atomically increments the reference count.
|
||||
pub addref: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// Decrements the count, destroys at zero.
|
||||
pub release: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// ABI version.
|
||||
pub abi_version: u32,
|
||||
}
|
||||
|
||||
/// `OakCancelAtom` — refcounted cancellation atom handle.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct OakCancelAtom {
|
||||
/// Opaque object pointer.
|
||||
pub ctx: *mut c_void,
|
||||
/// Atomically increments the reference count.
|
||||
pub addref: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// Decrements the count, destroys at zero.
|
||||
pub release: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// ABI version.
|
||||
pub abi_version: u32,
|
||||
}
|
||||
|
||||
/// `OakRenderRenderer` — refcounted display-renderer handle.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct OakRenderRenderer {
|
||||
/// Opaque object pointer.
|
||||
pub ctx: *mut c_void,
|
||||
/// Atomically increments the reference count.
|
||||
pub addref: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// Decrements the count, destroys at zero.
|
||||
pub release: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// ABI version.
|
||||
pub abi_version: u32,
|
||||
}
|
||||
|
||||
/// `OakCodecFrame` — refcounted CPU-frame handle shared with oakrender.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct OakCodecFrame {
|
||||
/// Opaque object pointer.
|
||||
pub ctx: *mut c_void,
|
||||
/// Atomically increments the reference count.
|
||||
pub addref: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// Decrements the count, destroys at zero.
|
||||
pub release: Option<unsafe extern "C" fn(*mut c_void)>,
|
||||
/// ABI version.
|
||||
pub abi_version: u32,
|
||||
}
|
||||
|
||||
// Refcounted opaque handles; thread-safe in the C library.
|
||||
unsafe impl Send for OakRenderTexture {}
|
||||
unsafe impl Sync for OakRenderTexture {}
|
||||
unsafe impl Send for OakCancelAtom {}
|
||||
unsafe impl Sync for OakCancelAtom {}
|
||||
unsafe impl Send for OakRenderRenderer {}
|
||||
unsafe impl Sync for OakRenderRenderer {}
|
||||
unsafe impl Send for OakCodecFrame {}
|
||||
unsafe impl Sync for OakCodecFrame {}
|
||||
|
||||
/// `oakrender_video_params` — flattened POD of `olive::VideoParams`
|
||||
/// passed into oakrender; see `include/render/renderer.h`.
|
||||
#[repr(C)]
|
||||
pub struct oakrender_video_params {
|
||||
/// Width in pixels.
|
||||
pub width: c_int,
|
||||
/// Height in pixels.
|
||||
pub height: c_int,
|
||||
/// Frame-duration numerator (e.g. 1001/30000 s).
|
||||
pub time_base_num: c_int,
|
||||
/// Frame-duration denominator.
|
||||
pub time_base_den: c_int,
|
||||
/// `olive::PixelFormat::Format`.
|
||||
pub format: c_int,
|
||||
/// Pixel-aspect numerator.
|
||||
pub pixel_aspect_num: c_int,
|
||||
/// Pixel-aspect denominator.
|
||||
pub pixel_aspect_den: c_int,
|
||||
/// `olive::VideoParams::Interlacing`.
|
||||
pub interlacing: c_int,
|
||||
/// `olive::VideoParams::ColorRange`.
|
||||
pub color_range: c_int,
|
||||
/// Preview-resolution divider (1 = full).
|
||||
pub divider: c_int,
|
||||
/// `olive::VideoParams::Type` (0 = video).
|
||||
pub video_type: c_int,
|
||||
/// 0/1 premultiplied alpha.
|
||||
pub premultiplied_alpha: c_int,
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
/// `oakrender_cancelatom_init`.
|
||||
pub fn oakrender_cancelatom_init() -> OakCancelAtom;
|
||||
/// `oakrender_cancelatom_free` (NULL/empty no-op).
|
||||
pub fn oakrender_cancelatom_free(atom: *mut OakCancelAtom);
|
||||
/// `oakrender_cancelatom_is_cancelled`.
|
||||
pub fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom) -> c_int;
|
||||
/// `oakrender_cancelatom_heard_cancel`.
|
||||
pub fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom) -> c_int;
|
||||
/// `oakrender_cancelatom_cancel`.
|
||||
pub fn oakrender_cancelatom_cancel(atom: OakCancelAtom);
|
||||
/// `oakrender_cancelatom_get_native`.
|
||||
pub fn oakrender_cancelatom_get_native(atom: OakCancelAtom) -> *mut c_void;
|
||||
/// `oakrender_display_texture_create`.
|
||||
pub fn oakrender_display_texture_create(
|
||||
renderer: OakRenderRenderer,
|
||||
params: *const oakrender_video_params,
|
||||
data: *const c_void,
|
||||
linesize: c_int,
|
||||
) -> OakRenderTexture;
|
||||
/// `oakrender_display_texture_retain`.
|
||||
pub fn oakrender_display_texture_retain(texture: OakRenderTexture) -> OakRenderTexture;
|
||||
/// `oakrender_display_texture_free` (NULL/empty no-op).
|
||||
pub fn oakrender_display_texture_free(texture: *mut OakRenderTexture);
|
||||
/// `oakrender_display_texture_upload`.
|
||||
pub fn oakrender_display_texture_upload(texture: OakRenderTexture) -> c_int;
|
||||
/// `oakrender_display_texture_download`.
|
||||
pub fn oakrender_display_texture_download(
|
||||
texture: OakRenderTexture,
|
||||
pixels: *mut c_void,
|
||||
linesize: c_int,
|
||||
) -> c_int;
|
||||
/// `oakrender_display_texture_get_params`.
|
||||
pub fn oakrender_display_texture_get_params(
|
||||
texture: OakRenderTexture,
|
||||
out: *mut oakrender_video_params,
|
||||
) -> c_int;
|
||||
/// `oakrender_display_texture_id`.
|
||||
pub fn oakrender_display_texture_id(texture: OakRenderTexture) -> c_int;
|
||||
/// `oakrender_display_texture_is_dummy`.
|
||||
pub fn oakrender_display_texture_is_dummy(texture: OakRenderTexture) -> c_int;
|
||||
/// `oakrender_display_texture_get_frame` (two-stage frame access).
|
||||
pub fn oakrender_display_texture_get_frame(
|
||||
texture: OakRenderTexture,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
/// `oakrender_codec_frame_width`.
|
||||
pub fn oakrender_codec_frame_width(frame: OakCodecFrame) -> c_int;
|
||||
/// `oakrender_codec_frame_height`.
|
||||
pub fn oakrender_codec_frame_height(frame: OakCodecFrame) -> c_int;
|
||||
/// `oakrender_codec_frame_fb_format`.
|
||||
pub fn oakrender_codec_frame_fb_format(frame: OakCodecFrame) -> c_int;
|
||||
/// `oakrender_codec_frame_free` (NULL/empty no-op).
|
||||
pub fn oakrender_codec_frame_free(frame: *mut OakCodecFrame);
|
||||
/// `oakrender_codec_frame_allocate`.
|
||||
pub fn oakrender_codec_frame_allocate(frame: OakCodecFrame) -> c_int;
|
||||
/// `oakrender_codec_frame_linesize_bytes`.
|
||||
pub fn oakrender_codec_frame_linesize_bytes(frame: OakCodecFrame) -> c_int;
|
||||
/// `oakrender_codec_frame_is_allocated`.
|
||||
pub fn oakrender_codec_frame_is_allocated(frame: OakCodecFrame) -> c_int;
|
||||
/// `oakrender_display_renderer_blit_color_managed`.
|
||||
pub fn oakrender_display_renderer_blit_color_managed(
|
||||
renderer: OakRenderRenderer,
|
||||
job: *const c_void,
|
||||
dst_texture: OakRenderTexture,
|
||||
params: *const oakrender_video_params,
|
||||
) -> c_int;
|
||||
}
|
||||
@@ -0,0 +1,999 @@
|
||||
// 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/>.
|
||||
|
||||
//! `#[cfg(test)]` in-memory mocks for the oakcommon / oakrender C ABI.
|
||||
//!
|
||||
//! The real symbols live in `liboakcommon` / `liboakrender` and are only
|
||||
//! linked when the crate is built against those dylibs (ctest). Under
|
||||
//! `cargo test` those libraries are not linked, so `extern "C"` call
|
||||
//! sites in the crate would otherwise fail to resolve. These
|
||||
//! `#[no_mangle] extern "C"` definitions provide the symbols and back the
|
||||
//! two stateful pieces the crate actually reads (video params and cancel
|
||||
//! atoms) with real in-memory state, so `Frame`, `FootageDescription`
|
||||
//! and friends are meaningfully testable. Everything else returns a
|
||||
//! deterministic neutral value.
|
||||
//!
|
||||
//! Kept strictly under `#[cfg(test)]`; never compiled into a real build.
|
||||
|
||||
#![allow(dead_code, unused_variables)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{c_char, c_int, c_void, CStr};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::bridge::common::{
|
||||
OakAudioParams, OakNodeBlock, OakSubtitleParams, OakVideoParams,
|
||||
};
|
||||
use crate::bridge::render::{OakCancelAtom, OakCodecFrame, OakRenderRenderer, OakRenderTexture};
|
||||
use crate::handle::OAKCODEC_ABI_VERSION;
|
||||
|
||||
/// Per-`OakVideoParams` backing state.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
struct MockParams {
|
||||
width: i32,
|
||||
height: i32,
|
||||
format: i32,
|
||||
time_base_num: i64,
|
||||
time_base_den: i64,
|
||||
stream_index: i32,
|
||||
divider: i32,
|
||||
frame_rate_num: i32,
|
||||
frame_rate_den: i32,
|
||||
duration: i64,
|
||||
channel_count: i32,
|
||||
color_primaries: i32,
|
||||
color_trc: i32,
|
||||
interlacing: i32,
|
||||
pixel_aspect_num: i32,
|
||||
pixel_aspect_den: i32,
|
||||
start_time: i64,
|
||||
color_range: i32,
|
||||
video_type: i32,
|
||||
premultiplied_alpha: i32,
|
||||
enabled: i32,
|
||||
}
|
||||
|
||||
fn params_store() -> &'static Mutex<HashMap<usize, MockParams>> {
|
||||
static S: OnceLock<Mutex<HashMap<usize, MockParams>>> = OnceLock::new();
|
||||
S.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
/// Build a handle whose `ctx` owns a leaked `MockParams`; the map entry
|
||||
/// keeps the storage alive until `free` is called.
|
||||
fn new_params_handle(p: MockParams) -> OakVideoParams {
|
||||
let raw = Box::into_raw(Box::new(p.clone()));
|
||||
params_store().lock().unwrap().insert(raw as usize, p);
|
||||
OakVideoParams {
|
||||
ctx: raw as *mut c_void,
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: OAKCODEC_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
fn params_ref(ctx: *mut c_void) -> Option<&'static mut MockParams> {
|
||||
let store = params_store().lock().unwrap();
|
||||
store.get(&(ctx as usize))?;
|
||||
drop(store);
|
||||
// SAFETY: entries are only removed by oakcommon_videoparams_free while
|
||||
// the caller still holds the handle, so the box outlives this borrow.
|
||||
Some(unsafe { &mut *(ctx as *mut MockParams) })
|
||||
}
|
||||
|
||||
fn params_get(ctx: *mut c_void) -> MockParams {
|
||||
let store = params_store().lock().unwrap();
|
||||
store
|
||||
.get(&(ctx as usize))
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn params_set(ctx: *mut c_void, f: impl FnOnce(&mut MockParams)) {
|
||||
let mut store = params_store().lock().unwrap();
|
||||
if let Some(p) = store.get_mut(&(ctx as usize)) {
|
||||
f(p);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// oakcommon_videoparams_*
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_init() -> OakVideoParams {
|
||||
new_params_handle(MockParams {
|
||||
channel_count: 4, // internal RGBA pipeline layout
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_init_basic(width: c_int, height: c_int) -> OakVideoParams {
|
||||
new_params_handle(MockParams {
|
||||
width,
|
||||
height,
|
||||
divider: 1,
|
||||
channel_count: 4, // internal RGBA pipeline layout
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_init_with_time_base(
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
time_base_num: i64,
|
||||
time_base_den: i64,
|
||||
) -> OakVideoParams {
|
||||
new_params_handle(MockParams {
|
||||
width,
|
||||
height,
|
||||
time_base_num,
|
||||
time_base_den,
|
||||
divider: 1,
|
||||
channel_count: 4, // internal RGBA pipeline layout
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_free(params: *mut OakVideoParams) {
|
||||
if params.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe {
|
||||
let ctx = (*params).ctx;
|
||||
params_store().lock().unwrap().remove(&(ctx as usize));
|
||||
if !ctx.is_null() {
|
||||
drop(Box::from_raw(ctx as *mut MockParams));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_get_width(params: OakVideoParams) -> c_int {
|
||||
params_get(params.ctx).width
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_get_height(params: OakVideoParams) -> c_int {
|
||||
params_get(params.ctx).height
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_get_format(params: OakVideoParams) -> c_int {
|
||||
params_get(params.ctx).format
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_get_time_base(
|
||||
params: OakVideoParams,
|
||||
out_num: *mut i64,
|
||||
out_den: *mut i64,
|
||||
) -> c_int {
|
||||
let p = params_get(params.ctx);
|
||||
if !out_num.is_null() {
|
||||
unsafe { *out_num = p.time_base_num };
|
||||
}
|
||||
if !out_den.is_null() {
|
||||
unsafe { *out_den = p.time_base_den };
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_width(params: OakVideoParams, width: c_int) {
|
||||
params_set(params.ctx, |p| p.width = width);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_height(params: OakVideoParams, height: c_int) {
|
||||
params_set(params.ctx, |p| p.height = height);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_format(params: OakVideoParams, format: c_int) {
|
||||
params_set(params.ctx, |p| p.format = format);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_get_is_valid(params: OakVideoParams) -> c_int {
|
||||
let p = params_get(params.ctx);
|
||||
((p.width > 0) && (p.height > 0)) as c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_equals(a: OakVideoParams, b: OakVideoParams) -> c_int {
|
||||
(params_get(a.ctx) == params_get(b.ctx)) as c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_static_get_bytes_per_pixel(format: c_int) -> c_int {
|
||||
// U10 packs to 4 bytes; U8 to 1; U16/F16 to 2; F32 to 4.
|
||||
match format {
|
||||
0 => 1, // U8
|
||||
1 => 4, // U10
|
||||
2 => 2, // U16
|
||||
3 => 2, // F16
|
||||
4 => 4, // F32
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_frame_rate_as_time_base(
|
||||
frame_rate_num: i64,
|
||||
frame_rate_den: i64,
|
||||
out_num: *mut i64,
|
||||
out_den: *mut i64,
|
||||
) {
|
||||
if !out_num.is_null() {
|
||||
unsafe { *out_num = frame_rate_den };
|
||||
}
|
||||
if !out_den.is_null() {
|
||||
unsafe { *out_den = frame_rate_num };
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_get_stream_index(params: OakVideoParams) -> c_int {
|
||||
params_get(params.ctx).stream_index
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_stream_index(params: OakVideoParams, index: c_int) {
|
||||
params_set(params.ctx, |p| p.stream_index = index);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_get_divider(params: OakVideoParams) -> c_int {
|
||||
params_get(params.ctx).divider
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_divider(params: OakVideoParams, divider: c_int) {
|
||||
params_set(params.ctx, |p| p.divider = divider);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_get_frame_rate(
|
||||
params: OakVideoParams,
|
||||
out_num: *mut c_int,
|
||||
out_den: *mut c_int,
|
||||
) -> c_int {
|
||||
let p = params_get(params.ctx);
|
||||
if !out_num.is_null() {
|
||||
unsafe { *out_num = p.frame_rate_num };
|
||||
}
|
||||
if !out_den.is_null() {
|
||||
unsafe { *out_den = p.frame_rate_den };
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_get_duration(params: OakVideoParams) -> i64 {
|
||||
params_get(params.ctx).duration
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_get_channel_count(params: OakVideoParams) -> c_int {
|
||||
params_get(params.ctx).channel_count
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_get_color_primaries(params: OakVideoParams) -> c_int {
|
||||
params_get(params.ctx).color_primaries
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_get_color_transfer(params: OakVideoParams) -> c_int {
|
||||
params_get(params.ctx).color_trc
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_get_interlacing(params: OakVideoParams) -> c_int {
|
||||
params_get(params.ctx).interlacing
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_time_base(
|
||||
params: OakVideoParams,
|
||||
num: i64,
|
||||
den: i64,
|
||||
) {
|
||||
params_set(params.ctx, |p| {
|
||||
p.time_base_num = num;
|
||||
p.time_base_den = den;
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_frame_rate(
|
||||
params: OakVideoParams,
|
||||
num: i64,
|
||||
den: i64,
|
||||
) {
|
||||
params_set(params.ctx, |p| {
|
||||
p.frame_rate_num = num as i32;
|
||||
p.frame_rate_den = den as i32;
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_pixel_aspect_ratio(
|
||||
params: OakVideoParams,
|
||||
num: i64,
|
||||
den: i64,
|
||||
) {
|
||||
params_set(params.ctx, |p| {
|
||||
p.pixel_aspect_num = num as i32;
|
||||
p.pixel_aspect_den = den as i32;
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_interlacing(params: OakVideoParams, interlacing: c_int) {
|
||||
params_set(params.ctx, |p| p.interlacing = interlacing);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_duration(params: OakVideoParams, duration: i64) {
|
||||
params_set(params.ctx, |p| p.duration = duration);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_start_time(params: OakVideoParams, start_time: i64) {
|
||||
params_set(params.ctx, |p| p.start_time = start_time);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_color_range(params: OakVideoParams, color_range: c_int) {
|
||||
params_set(params.ctx, |p| p.color_range = color_range);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_video_type(params: OakVideoParams, video_type: c_int) {
|
||||
params_set(params.ctx, |p| p.video_type = video_type);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_channel_count(params: OakVideoParams, channels: c_int) {
|
||||
params_set(params.ctx, |p| p.channel_count = channels);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_color_primaries(params: OakVideoParams, primaries: c_int) {
|
||||
params_set(params.ctx, |p| p.color_primaries = primaries);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_color_transfer(params: OakVideoParams, transfer: c_int) {
|
||||
params_set(params.ctx, |p| p.color_trc = transfer);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_premultiplied_alpha(
|
||||
params: OakVideoParams,
|
||||
premultiplied: c_int,
|
||||
) {
|
||||
params_set(params.ctx, |p| p.premultiplied_alpha = premultiplied);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_videoparams_set_enabled(params: OakVideoParams, enabled: c_int) {
|
||||
params_set(params.ctx, |p| p.enabled = enabled);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// oakcore_audioparams_* / oakcore_rational_* (pointer-based in-memory state)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Per-`OakAudioParams` backing state. The real oakcore C ABI is
|
||||
/// pointer-based (`core/include/olive/core/oakcore/audioparams.h`), so
|
||||
/// these stubs own a boxed struct and return its raw pointer.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
struct MockAudioParams {
|
||||
sample_rate: i32,
|
||||
channel_layout: u64,
|
||||
format: i32,
|
||||
stream_index: i32,
|
||||
duration: i64,
|
||||
time_base_num: i32,
|
||||
time_base_den: i32,
|
||||
}
|
||||
|
||||
fn audio_params_store() -> &'static Mutex<HashMap<usize, MockAudioParams>> {
|
||||
static S: OnceLock<Mutex<HashMap<usize, MockAudioParams>>> = OnceLock::new();
|
||||
S.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
fn audio_params_get(ctx: *const c_void) -> MockAudioParams {
|
||||
let store = audio_params_store().lock().unwrap();
|
||||
store
|
||||
.get(&(ctx as usize))
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Per-`OakRational` backing state (an owned `(num, den)` pair).
|
||||
fn rational_store() -> &'static Mutex<HashMap<usize, (i32, i32)>> {
|
||||
static S: OnceLock<Mutex<HashMap<usize, (i32, i32)>>> = OnceLock::new();
|
||||
S.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_create(
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
format: c_int,
|
||||
) -> *mut OakAudioParams {
|
||||
// The timebase starts at 1/sample_rate, mirroring the real header.
|
||||
let p = MockAudioParams {
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
format,
|
||||
stream_index: 0,
|
||||
duration: 0,
|
||||
time_base_num: 1,
|
||||
time_base_den: sample_rate,
|
||||
};
|
||||
let raw = Box::into_raw(Box::new(p.clone()));
|
||||
audio_params_store().lock().unwrap().insert(raw as usize, p);
|
||||
raw as *mut OakAudioParams
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_free(params: *mut OakAudioParams) {
|
||||
if params.is_null() {
|
||||
return;
|
||||
}
|
||||
audio_params_store()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove(&(params as usize));
|
||||
// SAFETY: `params` was produced by `oakcore_audioparams_create` as a
|
||||
// boxed `MockAudioParams`; we hold the only reference after removal.
|
||||
unsafe { drop(Box::from_raw(params as *mut MockAudioParams)) };
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_sample_rate(params: *const OakAudioParams) -> c_int {
|
||||
audio_params_get(params as *const c_void).sample_rate
|
||||
}
|
||||
|
||||
fn audio_params_set(ctx: *mut c_void, f: impl FnOnce(&mut MockAudioParams)) {
|
||||
let mut store = audio_params_store().lock().unwrap();
|
||||
if let Some(p) = store.get_mut(&(ctx as usize)) {
|
||||
f(p);
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_set_sample_rate(
|
||||
params: *mut OakAudioParams,
|
||||
sample_rate: c_int,
|
||||
) {
|
||||
audio_params_set(params as *mut c_void, |p| p.sample_rate = sample_rate);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_channel_layout(params: *const OakAudioParams) -> u64 {
|
||||
audio_params_get(params as *const c_void).channel_layout
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_set_channel_layout(
|
||||
params: *mut OakAudioParams,
|
||||
layout: u64,
|
||||
) {
|
||||
audio_params_set(params as *mut c_void, |p| p.channel_layout = layout);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_set_time_base(
|
||||
params: *mut OakAudioParams,
|
||||
num: c_int,
|
||||
den: c_int,
|
||||
) {
|
||||
audio_params_set(params as *mut c_void, |p| {
|
||||
p.time_base_num = num;
|
||||
p.time_base_den = den;
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_set_format(params: *mut OakAudioParams, format: c_int) {
|
||||
audio_params_set(params as *mut c_void, |p| p.format = format);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_set_stream_index(
|
||||
params: *mut OakAudioParams,
|
||||
index: c_int,
|
||||
) {
|
||||
audio_params_set(params as *mut c_void, |p| p.stream_index = index);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_set_duration(params: *mut OakAudioParams, duration: i64) {
|
||||
audio_params_set(params as *mut c_void, |p| p.duration = duration);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_channel_count(params: *const OakAudioParams) -> c_int {
|
||||
audio_params_get(params as *const c_void)
|
||||
.channel_layout
|
||||
.count_ones() as c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_format(params: *const OakAudioParams) -> c_int {
|
||||
audio_params_get(params as *const c_void).format
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_stream_index(params: *const OakAudioParams) -> c_int {
|
||||
audio_params_get(params as *const c_void).stream_index
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_duration(params: *const OakAudioParams) -> i64 {
|
||||
audio_params_get(params as *const c_void).duration
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_is_valid(params: *const OakAudioParams) -> c_int {
|
||||
let p = audio_params_get(params as *const c_void);
|
||||
(p.sample_rate > 0 && p.channel_layout != 0 && p.format >= 0) as c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_audioparams_time_base(params: *const OakAudioParams) -> *mut c_void {
|
||||
let p = audio_params_get(params as *const c_void);
|
||||
let r = (p.time_base_num, p.time_base_den);
|
||||
let raw = Box::into_raw(Box::new(r));
|
||||
rational_store().lock().unwrap().insert(raw as usize, r);
|
||||
raw as *mut c_void
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_rational_numerator(rational: *const c_void) -> c_int {
|
||||
rational_store()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&(rational as usize))
|
||||
.map(|r| r.0)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_rational_denominator(rational: *const c_void) -> c_int {
|
||||
rational_store()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get(&(rational as usize))
|
||||
.map(|r| r.1)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcore_rational_free(rational: *mut c_void) {
|
||||
if rational.is_null() {
|
||||
return;
|
||||
}
|
||||
rational_store().lock().unwrap().remove(&(rational as usize));
|
||||
// SAFETY: `rational` was produced by `oakcore_audioparams_time_base` as a
|
||||
// boxed `(i32, i32)` pair; we hold the only reference after removal.
|
||||
unsafe { drop(Box::from_raw(rational as *mut (i32, i32))) };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// oakcommon_subtitleparams_*
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_subtitleparams_get_stream_index(_params: OakSubtitleParams) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_subtitleparams_generate_ass_header(
|
||||
_params: OakSubtitleParams,
|
||||
_width: c_int,
|
||||
_height: c_int,
|
||||
) {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_subtitleparams_add_subtitle(
|
||||
_params: OakSubtitleParams,
|
||||
_text: *const c_char,
|
||||
) {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// oakcommon_config_*
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_config_get_int(
|
||||
_group: *const c_char,
|
||||
_key: *const c_char,
|
||||
default: c_int,
|
||||
) -> c_int {
|
||||
default
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_config_get_bool(
|
||||
_group: *const c_char,
|
||||
_key: *const c_char,
|
||||
default: c_int,
|
||||
) -> c_int {
|
||||
default
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_config_get(
|
||||
_group: *const c_char,
|
||||
_key: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
// Empty value: report the size needed (1 for NUL) and write NUL.
|
||||
if buf_size <= 0 {
|
||||
return 1;
|
||||
}
|
||||
if !buf.is_null() {
|
||||
unsafe { *buf = 0 };
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// oakcommon_filefunctions_*
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static FAKE_PATH: &[u8] = b"/mock/config/oak\0";
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_filefunctions_init() {}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_filefunctions_get_configuration_location(
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
copy_cstr(FAKE_PATH, buf, buf_size)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_filefunctions_get_unique_file_identifier(path: *const c_char) -> i64 {
|
||||
if path.is_null() {
|
||||
return 0;
|
||||
}
|
||||
unsafe { CStr::from_ptr(path) }
|
||||
.to_bytes()
|
||||
.iter()
|
||||
.fold(14695981039346656037u64, |acc, &b| (acc ^ b as u64).wrapping_mul(1099511628211))
|
||||
as i64
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_filefunctions_get_application_path(
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
copy_cstr(b"/mock/app/oak\0", buf, buf_size)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_filefunctions_free(_ptr: *mut c_void) {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// oakcommon_colortransform_*
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_colortransform_init_output(
|
||||
_src_colorspace: c_int,
|
||||
_src_trc: c_int,
|
||||
_dst_colorspace: c_int,
|
||||
_dst_trc: c_int,
|
||||
_premultiplied: c_int,
|
||||
_chroma_coeffs: *const c_void,
|
||||
) -> OakVideoParams {
|
||||
oakcommon_videoparams_init()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_colortransform_get_output(params: OakVideoParams, out: *mut OakVideoParams) {
|
||||
if !out.is_null() {
|
||||
unsafe { *out = params.clone() };
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_colortransform_free(params: *mut OakVideoParams) {
|
||||
oakcommon_videoparams_free(params);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// oakcommon_ffmpegutils_* (pure enum mapping; identity is a safe default)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_ffmpegutils_get_native_sample_format(sample_format: c_int) -> c_int {
|
||||
sample_format
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_ffmpegutils_get_compatible_pixel_format(format: c_int) -> c_int {
|
||||
format
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_ffmpegutils_get_ffmpeg_pixel_format(format: c_int) -> c_int {
|
||||
format
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_ffmpegutils_get_ffmpeg_sample_format(format: c_int) -> c_int {
|
||||
format
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_ffmpegutils_get_compatible_bridge_pixel_format(format: c_int) -> c_int {
|
||||
format
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(format: c_int) -> c_int {
|
||||
format
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// oakcommon_oiioutils_*
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_oiioutils_init() {}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_oiioutils_get_oiio_base_type_from_format(format: c_int) -> c_int {
|
||||
format
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_oiioutils_get_format_from_oiio_basetype(basetype: c_int) -> c_int {
|
||||
basetype
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_oiioutils_get_pixel_aspect_ratio(
|
||||
_width: c_int,
|
||||
_height: c_int,
|
||||
out_num: *mut c_int,
|
||||
out_den: *mut c_int,
|
||||
) -> c_int {
|
||||
if !out_num.is_null() {
|
||||
unsafe { *out_num = 1 };
|
||||
}
|
||||
if !out_den.is_null() {
|
||||
unsafe { *out_den = 1 };
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakcommon_oiioutils_free() {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// oakrender_cancelatom_* (real in-memory cancel state)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static CANCEL_FLAGS: OnceLock<Mutex<HashMap<usize, bool>>> = OnceLock::new();
|
||||
|
||||
fn cancel_flags() -> &'static Mutex<HashMap<usize, bool>> {
|
||||
CANCEL_FLAGS.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_cancelatom_init() -> OakCancelAtom {
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(1);
|
||||
let id = COUNTER.fetch_add(1, Ordering::SeqCst) as usize;
|
||||
cancel_flags().lock().unwrap().insert(id, false);
|
||||
OakCancelAtom {
|
||||
ctx: id as *mut c_void,
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: OAKCODEC_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_cancelatom_free(atom: *mut OakCancelAtom) {
|
||||
if atom.is_null() {
|
||||
return;
|
||||
}
|
||||
unsafe { cancel_flags().lock().unwrap().remove(&((*atom).ctx as usize)) };
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_cancelatom_is_cancelled(atom: OakCancelAtom) -> c_int {
|
||||
(*cancel_flags().lock().unwrap().get(&(atom.ctx as usize)).unwrap_or(&false)) as c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_cancelatom_heard_cancel(atom: OakCancelAtom) -> c_int {
|
||||
oakrender_cancelatom_is_cancelled(atom)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_cancelatom_cancel(atom: OakCancelAtom) {
|
||||
if let Some(f) = cancel_flags().lock().unwrap().get_mut(&(atom.ctx as usize)) {
|
||||
*f = true;
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_cancelatom_get_native(atom: OakCancelAtom) -> *mut c_void {
|
||||
atom.ctx
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// oakrender_display_texture_* / codec_frame_* (neutral)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_display_texture_create(
|
||||
_renderer: OakRenderRenderer,
|
||||
_params: *const crate::bridge::render::oakrender_video_params,
|
||||
_data: *const c_void,
|
||||
_linesize: c_int,
|
||||
) -> OakRenderTexture {
|
||||
OakRenderTexture {
|
||||
ctx: std::ptr::null_mut(),
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: OAKCODEC_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_display_texture_retain(texture: OakRenderTexture) -> OakRenderTexture {
|
||||
texture
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_display_texture_free(_texture: *mut OakRenderTexture) {}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_display_texture_upload(_texture: OakRenderTexture) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_display_texture_download(
|
||||
_texture: OakRenderTexture,
|
||||
_pixels: *mut c_void,
|
||||
_linesize: c_int,
|
||||
) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_display_texture_get_params(
|
||||
_texture: OakRenderTexture,
|
||||
_out: *mut crate::bridge::render::oakrender_video_params,
|
||||
) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_display_texture_id(_texture: OakRenderTexture) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_display_texture_is_dummy(_texture: OakRenderTexture) -> c_int {
|
||||
1
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_display_texture_get_frame(
|
||||
_texture: OakRenderTexture,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
if buf_size <= 0 {
|
||||
return 1;
|
||||
}
|
||||
if !buf.is_null() {
|
||||
unsafe { *buf = 0 };
|
||||
}
|
||||
1
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_codec_frame_width(_frame: OakCodecFrame) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_codec_frame_height(_frame: OakCodecFrame) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_codec_frame_fb_format(_frame: OakCodecFrame) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_codec_frame_free(_frame: *mut OakCodecFrame) {}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_codec_frame_allocate(_frame: OakCodecFrame) -> c_int {
|
||||
1
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_codec_frame_linesize_bytes(_frame: OakCodecFrame) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_codec_frame_is_allocated(_frame: OakCodecFrame) -> c_int {
|
||||
1
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn oakrender_display_renderer_blit_color_managed(
|
||||
_renderer: OakRenderRenderer,
|
||||
_job: *const c_void,
|
||||
_dst_texture: OakRenderTexture,
|
||||
_params: *const crate::bridge::render::oakrender_video_params,
|
||||
) -> c_int {
|
||||
0
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Two-stage string copy helper used by the mock getters. Returns the
|
||||
/// total size needed (including the trailing NUL), or truncates the buffer
|
||||
/// and writes a NUL terminator when the buffer is too small.
|
||||
fn copy_cstr(src: &[u8], buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
let needed = src.len() as c_int;
|
||||
if buf.is_null() || buf_size < needed {
|
||||
return needed;
|
||||
}
|
||||
unsafe {
|
||||
for (i, &b) in src.iter().enumerate() {
|
||||
*buf.add(i) = b as c_char;
|
||||
}
|
||||
}
|
||||
needed
|
||||
}
|
||||
|
||||
// Silence unused-import warnings when the by-value handle types are not
|
||||
// referenced by every build; they are part of the mock's public surface.
|
||||
#[allow(unused)]
|
||||
fn _keep(_: OakAudioParams, _: OakNodeBlock) {}
|
||||
@@ -0,0 +1,341 @@
|
||||
// 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/>.
|
||||
|
||||
//! `olive::ConformManager` — pcm waveform cache files for fast scrubbing.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/conformmanager.h`. Stateless (NOTES.md): actual
|
||||
//! conform work is delegated to the global task submit callback
|
||||
//! ([`crate::task`]); with no registrar the state queries report
|
||||
//! `Unavailable`. Deterministic per-channel filenames derive from the
|
||||
//! source + target audio params.
|
||||
|
||||
use std::ffi::CString;
|
||||
use std::path::Path;
|
||||
|
||||
/// Conform state of one audio stream.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum ConformState {
|
||||
/// Conform files exist.
|
||||
Exists = 0,
|
||||
/// Conform is being generated.
|
||||
Generating = 1,
|
||||
/// No task registrar; conform unavailable.
|
||||
Unavailable = 2,
|
||||
}
|
||||
|
||||
/// `olive::ConformManager` — stateless conform query/produce manager.
|
||||
pub struct ConformManager;
|
||||
|
||||
impl ConformManager {
|
||||
/// The process-wide ConformManager singleton.
|
||||
pub fn instance() -> &'static ConformManager {
|
||||
static INSTANCE: ConformManager = ConformManager;
|
||||
&INSTANCE
|
||||
}
|
||||
|
||||
/// Query (and when possible start) the conform of one audio stream.
|
||||
///
|
||||
/// `wait != 0` treats a post-submit miss as `Unavailable`; `wait == 0`
|
||||
/// reports it as `Generating`. Without a task registrar the result is
|
||||
/// always `Unavailable`.
|
||||
pub fn get_conform_state(
|
||||
&self,
|
||||
cache_path: &str,
|
||||
source_filename: &str,
|
||||
stream_index: i32,
|
||||
sample_rate: i32,
|
||||
channel_layout: u64,
|
||||
sample_format: i32,
|
||||
wait: bool,
|
||||
) -> crate::error::Result<ConformState> {
|
||||
let filenames = conform_filenames(
|
||||
cache_path,
|
||||
source_filename,
|
||||
stream_index,
|
||||
sample_rate,
|
||||
sample_format,
|
||||
channel_layout,
|
||||
);
|
||||
|
||||
// Return existing conform if it exists.
|
||||
if all_conforms_exist(&filenames) {
|
||||
return Ok(ConformState::Exists);
|
||||
}
|
||||
|
||||
// Interim state (pre-M8): no task system, conform cannot be generated.
|
||||
if !crate::task::task_submit_is_registered() {
|
||||
return Ok(ConformState::Unavailable);
|
||||
}
|
||||
|
||||
// The task owns the ".working" temporary names and the rename to the
|
||||
// final per-channel filenames on success; output_filename carries the
|
||||
// first channel's final path and the task derives the siblings.
|
||||
let req = crate::task::TaskRequest {
|
||||
kind: crate::task::TaskKind::Conform,
|
||||
input_filename: source_filename,
|
||||
output_filename: filenames.first().map(String::as_str).unwrap_or(""),
|
||||
stream_index,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
sample_format,
|
||||
proxy_width: 0,
|
||||
proxy_height: 0,
|
||||
};
|
||||
|
||||
// Interim simplification: submission is synchronous — we always wait
|
||||
// for the submit to return, regardless of `wait`.
|
||||
if crate::task::submit_task(&req).is_err() {
|
||||
return Ok(ConformState::Unavailable);
|
||||
}
|
||||
|
||||
if all_conforms_exist(&filenames) {
|
||||
return Ok(ConformState::Exists);
|
||||
}
|
||||
|
||||
if wait {
|
||||
// Synchronous wait already happened and the conform still does not
|
||||
// exist: report the wait as failed.
|
||||
return Ok(ConformState::Unavailable);
|
||||
}
|
||||
|
||||
Ok(ConformState::Generating)
|
||||
}
|
||||
|
||||
/// Number of conform (pcm) files for the given stream/params — one per
|
||||
/// channel; 0 on invalid arguments.
|
||||
pub fn get_conform_filename_count(
|
||||
&self,
|
||||
_cache_path: &str,
|
||||
_source_filename: &str,
|
||||
_stream_index: i32,
|
||||
_sample_rate: i32,
|
||||
channel_layout: u64,
|
||||
_sample_format: i32,
|
||||
) -> usize {
|
||||
channel_layout.count_ones() as usize
|
||||
}
|
||||
|
||||
/// The `index`-th conform filename.
|
||||
pub fn get_conform_filename(
|
||||
&self,
|
||||
cache_path: &str,
|
||||
source_filename: &str,
|
||||
stream_index: i32,
|
||||
sample_rate: i32,
|
||||
channel_layout: u64,
|
||||
sample_format: i32,
|
||||
index: usize,
|
||||
) -> crate::error::Result<String> {
|
||||
let filenames = conform_filenames(
|
||||
cache_path,
|
||||
source_filename,
|
||||
stream_index,
|
||||
sample_rate,
|
||||
sample_format,
|
||||
channel_layout,
|
||||
);
|
||||
filenames
|
||||
.get(index)
|
||||
.cloned()
|
||||
.ok_or(crate::error::Error::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
/// Deterministic conform base name plus per-channel pcm filenames, mirroring
|
||||
/// `ConformManager::get_conformed_filename`: one file per channel under
|
||||
/// `cache_path`, named `<identifier>-<stream>.<rate>.<format>.<layout>.<i>.pcm`.
|
||||
fn conform_filenames(
|
||||
cache_path: &str,
|
||||
source_filename: &str,
|
||||
stream_index: i32,
|
||||
sample_rate: i32,
|
||||
sample_format: i32,
|
||||
channel_layout: u64,
|
||||
) -> Vec<String> {
|
||||
let count = channel_layout.count_ones() as usize;
|
||||
let base = format!(
|
||||
"{}-{}.{}.{}.{}",
|
||||
unique_file_identifier(source_filename),
|
||||
stream_index,
|
||||
sample_rate,
|
||||
sample_format,
|
||||
channel_layout,
|
||||
);
|
||||
|
||||
let mut out = Vec::with_capacity(count);
|
||||
for i in 0..count {
|
||||
let p = Path::new(cache_path).join(format!("{}.{}.pcm", base, i));
|
||||
out.push(p.to_string_lossy().into_owned());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge
|
||||
/// returns a 64-bit id directly).
|
||||
fn unique_file_identifier(filename: &str) -> String {
|
||||
let c = match CString::new(filename) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return String::new(),
|
||||
};
|
||||
// # Safety: `c` is a valid NUL-terminated C string alive for the call.
|
||||
let id = unsafe {
|
||||
crate::bridge::common::oakcommon_filefunctions_get_unique_file_identifier(c.as_ptr())
|
||||
};
|
||||
format!("{}", id)
|
||||
}
|
||||
|
||||
/// True when every conform filename already exists on disk.
|
||||
fn all_conforms_exist(filenames: &[String]) -> bool {
|
||||
filenames.iter().all(|f| Path::new(f).exists())
|
||||
}
|
||||
|
||||
/// Shared test support: serializes access to the global task-submit registry
|
||||
/// (unit tests run in parallel and would otherwise clear each other's
|
||||
/// registration) and provides a callback that accepts any task.
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_util {
|
||||
use crate::error::OAKCODEC_OK;
|
||||
use crate::task::OakCodecTaskRequest;
|
||||
|
||||
/// Serializes every test that mutates the task-submit registry.
|
||||
pub static REG_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
|
||||
/// A task-submit callback that accepts every request (no-op).
|
||||
pub unsafe extern "C" fn accept_cb(
|
||||
_req: *const OakCodecTaskRequest,
|
||||
_ud: *mut std::ffi::c_void,
|
||||
) -> i32 {
|
||||
OAKCODEC_OK
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn temp_subdir(name: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"oakcodec_conform_{}_{}",
|
||||
name,
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
dir.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
fn fnv1a64(bytes: &[u8]) -> u64 {
|
||||
let mut h: u64 = 14695981039346656037;
|
||||
for &b in bytes {
|
||||
h ^= b as u64;
|
||||
h = h.wrapping_mul(1099511628211);
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unique_identifier_matches_bridge_hash() {
|
||||
// The test stub computes an FNV-1a-64 of the path bytes.
|
||||
let expected = format!("{}", fnv1a64(b"media.mp4") as i64);
|
||||
assert_eq!(unique_file_identifier("media.mp4"), expected);
|
||||
// Deterministic: same input, same id.
|
||||
assert_eq!(
|
||||
unique_file_identifier("media.mp4"),
|
||||
unique_file_identifier("media.mp4")
|
||||
);
|
||||
// Different input, different id.
|
||||
assert_ne!(
|
||||
unique_file_identifier("media.mp4"),
|
||||
unique_file_identifier("other.mp4")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filename_count_from_channel_layout() {
|
||||
let m = ConformManager::instance();
|
||||
assert_eq!(m.get_conform_filename_count("c", "s", 0, 48000, 0x3, 0), 2); // stereo
|
||||
assert_eq!(m.get_conform_filename_count("c", "s", 0, 48000, 0x4, 0), 1); // mono
|
||||
assert_eq!(m.get_conform_filename_count("c", "s", 0, 48000, 0, 0), 0); // invalid
|
||||
assert_eq!(
|
||||
m.get_conform_filename_count("c", "s", 0, 48000, 0x60F, 0),
|
||||
6
|
||||
); // 5.1
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conform_filename_derivation_and_range() {
|
||||
let m = ConformManager::instance();
|
||||
let cache = temp_subdir("names");
|
||||
let id = fnv1a64(b"media.mp4") as i64;
|
||||
let base = format!("{}-0.48000.0.3", id);
|
||||
let f0 = m
|
||||
.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 0)
|
||||
.unwrap();
|
||||
let f1 = m
|
||||
.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 1)
|
||||
.unwrap();
|
||||
assert_eq!(f0, format!("{}/{}.0.pcm", cache, base));
|
||||
assert_eq!(f1, format!("{}/{}.1.pcm", cache, base));
|
||||
// Out of range.
|
||||
assert!(matches!(
|
||||
m.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, 5),
|
||||
Err(crate::error::Error::NotFound)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_conform_state_unavailable_without_registrar() {
|
||||
let _g = super::test_util::REG_LOCK.lock().unwrap();
|
||||
// Ensure no registrar is left over.
|
||||
crate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
let cache = temp_subdir("unavail");
|
||||
let s = ConformManager::instance()
|
||||
.get_conform_state(&cache, "missing.mp4", 0, 48000, 0x3, 0, false)
|
||||
.unwrap();
|
||||
assert_eq!(s, ConformState::Unavailable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_conform_state_exists_when_files_present() {
|
||||
let cache = temp_subdir("exists");
|
||||
let m = ConformManager::instance();
|
||||
for i in 0..2 {
|
||||
let f = m
|
||||
.get_conform_filename(&cache, "media.mp4", 0, 48000, 0x3, 0, i)
|
||||
.unwrap();
|
||||
std::fs::write(&f, b"pcm").unwrap();
|
||||
}
|
||||
let s = m
|
||||
.get_conform_state(&cache, "media.mp4", 0, 48000, 0x3, 0, false)
|
||||
.unwrap();
|
||||
assert_eq!(s, ConformState::Exists);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_conform_state_generating_when_registered() {
|
||||
let _g = super::test_util::REG_LOCK.lock().unwrap();
|
||||
crate::task::set_task_submit_cb_extern(
|
||||
Some(super::test_util::accept_cb),
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
let cache = temp_subdir("generating");
|
||||
let s = ConformManager::instance()
|
||||
.get_conform_state(&cache, "missing.mp4", 0, 48000, 0x3, 0, false)
|
||||
.unwrap();
|
||||
crate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
assert_eq!(s, ConformState::Generating);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,720 @@
|
||||
// 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/>.
|
||||
|
||||
//! `olive::Decoder` and its supporting types — the media-decoder trait.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/decoder.h`. The C++ abstract base plus its
|
||||
//! FFmpeg/OIIO subclasses become the [`Decoder`] trait (decision 2 in
|
||||
//! README.md); probe/dispatch lives on the registry functions at the
|
||||
//! bottom of this module. Audio is handled in raw interleaved-float
|
||||
//! buffers matching the C ABI, not `oakcore_rs::SampleBuffer` (which the
|
||||
//! crate does not export).
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use oakcore_rs::{Rational, TimeRange};
|
||||
|
||||
use crate::bridge::render::{OakCancelAtom, OakRenderTexture};
|
||||
use crate::footagedescription::FootageDescription;
|
||||
use crate::frame::Frame;
|
||||
|
||||
/// `oakcodec_video_stream_info` — POD probe output describing one video
|
||||
/// stream; see `include/codec/decoder.h`.
|
||||
#[repr(C)]
|
||||
pub struct OakCodecVideoStreamInfo {
|
||||
/// Stream index.
|
||||
pub stream_index: i32,
|
||||
/// Width in pixels.
|
||||
pub width: i32,
|
||||
/// Height in pixels.
|
||||
pub height: i32,
|
||||
/// Frame-rate numerator.
|
||||
pub frame_rate_num: i32,
|
||||
/// Frame-rate denominator.
|
||||
pub frame_rate_den: i32,
|
||||
/// Stream length in time-base units.
|
||||
pub duration_ts: i64,
|
||||
/// Time-base numerator (seconds per time-base unit).
|
||||
pub time_base_num: i32,
|
||||
/// Time-base denominator.
|
||||
pub time_base_den: i32,
|
||||
/// Native delivery `OakPixelFormat`.
|
||||
pub format: i32,
|
||||
/// Plane channel count.
|
||||
pub channel_count: i32,
|
||||
/// ISO/IEC 23001-8 color-primaries code point (0 = unknown).
|
||||
pub color_primaries: i32,
|
||||
/// ISO/IEC 23001-8 color-transfer code point (0 = unknown).
|
||||
pub color_trc: i32,
|
||||
/// 1 when the stream is interlaced.
|
||||
pub interlaced: i32,
|
||||
}
|
||||
|
||||
/// `oakcodec_audio_stream_info` — POD probe output describing one audio
|
||||
/// stream; see `include/codec/decoder.h`.
|
||||
#[repr(C)]
|
||||
pub struct OakCodecAudioStreamInfo {
|
||||
/// Stream index.
|
||||
pub stream_index: i32,
|
||||
/// Sample rate (Hz).
|
||||
pub sample_rate: i32,
|
||||
/// ffmpeg-style channel mask (e.g. 0x3 = stereo).
|
||||
pub channel_layout: u64,
|
||||
/// Channel count.
|
||||
pub channel_count: i32,
|
||||
/// Stream length in time-base units.
|
||||
pub duration_ts: i64,
|
||||
/// Time-base numerator.
|
||||
pub time_base_num: i32,
|
||||
/// Time-base denominator.
|
||||
pub time_base_den: i32,
|
||||
}
|
||||
|
||||
/// Local replacement for `render/rendermodes.h` (oakrender C API has no
|
||||
/// render-mode counterpart). Values mirror engine/render/rendermodes.h:
|
||||
/// k_offline = 0, k_online = 1.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum RenderMode {
|
||||
/// Offline / background render.
|
||||
Offline = 0,
|
||||
/// Online / real-time render.
|
||||
Online = 1,
|
||||
}
|
||||
|
||||
/// "Don't force a color range" sentinel for
|
||||
/// [`RetrieveVideoParams::force_range`] (the actual ranges are the
|
||||
/// `OAKCOMMON_COLOR_RANGE_*` values).
|
||||
pub const K_COLOR_RANGE_DEFAULT: i32 = -1;
|
||||
|
||||
/// `Decoder::RetrieveVideoParams` — what a video retrieve call needs.
|
||||
pub struct RetrieveVideoParams {
|
||||
/// Stream to read from.
|
||||
pub stream: CodecStream,
|
||||
/// Timestamp, rational seconds.
|
||||
pub time: Rational,
|
||||
/// Length of footage before the start (for early-seek semantics).
|
||||
pub length: TimeRange,
|
||||
/// Color range override; [`K_COLOR_RANGE_DEFAULT`] means "don't force".
|
||||
pub force_range: i32,
|
||||
/// Image sequence: bake the frame number into the filename.
|
||||
pub is_image_sequence: bool,
|
||||
/// Image sequence digit count (derived from the filename).
|
||||
pub image_sequence_digits: i32,
|
||||
/// Image sequence number to substitute.
|
||||
pub image_sequence_number: i64,
|
||||
/// Render mode (drives texture-path choices in the implementations).
|
||||
pub mode: RenderMode,
|
||||
/// Frame alpha channel is premultiplied.
|
||||
pub alpha_is_premultiplied: bool,
|
||||
}
|
||||
|
||||
/// `Decoder::RetrieveAudioStatus` — outcome of an audio retrieve.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RetrieveAudioStatus {
|
||||
/// Data written to the destination buffer.
|
||||
Success,
|
||||
/// The requested range is outside the footage.
|
||||
InvalidRange,
|
||||
/// The stream does not support audio.
|
||||
Unsupported,
|
||||
/// Media requires a conform that could not be produced.
|
||||
ConformNeeded,
|
||||
/// A decoder-level error occurred.
|
||||
Error,
|
||||
}
|
||||
|
||||
/// `Decoder::RetrieveState`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RetrieveState {
|
||||
/// Ready to decode.
|
||||
Ready,
|
||||
/// Failed to open the stream.
|
||||
FailedToOpen,
|
||||
/// The stream index could not be located.
|
||||
IndexUnavailable,
|
||||
}
|
||||
|
||||
/// `Decoder::CodecStream` — identifies one (filename, stream) pair plus an
|
||||
/// optional associated timeline block.
|
||||
///
|
||||
/// The block is an opaque `OakNodeBlock` handle that codec only stores and
|
||||
/// compares, never dereferences or retains (borrowed pointer).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CodecStream {
|
||||
filename: String,
|
||||
stream: i32,
|
||||
block: Option<crate::bridge::common::OakNodeBlock>,
|
||||
}
|
||||
|
||||
impl CodecStream {
|
||||
/// Empty, invalid stream.
|
||||
pub fn new() -> Self {
|
||||
CodecStream {
|
||||
filename: String::new(),
|
||||
stream: -1,
|
||||
block: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// New stream for `(filename, stream)` with an optional block.
|
||||
pub fn with_block(
|
||||
filename: String,
|
||||
stream: i32,
|
||||
block: Option<crate::bridge::common::OakNodeBlock>,
|
||||
) -> Self {
|
||||
CodecStream {
|
||||
filename,
|
||||
stream,
|
||||
block,
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-empty filename and non-negative stream index.
|
||||
pub fn is_valid(&self) -> bool {
|
||||
!self.filename.is_empty() && self.stream >= 0
|
||||
}
|
||||
|
||||
/// The file exists on disk.
|
||||
pub fn exists(&self) -> bool {
|
||||
Path::new(&self.filename).exists()
|
||||
}
|
||||
|
||||
/// Reset to the empty stream.
|
||||
pub fn reset(&mut self) {
|
||||
self.filename.clear();
|
||||
self.stream = -1;
|
||||
self.block = None;
|
||||
}
|
||||
|
||||
/// Source filename.
|
||||
pub fn filename(&self) -> &str {
|
||||
&self.filename
|
||||
}
|
||||
|
||||
/// Stream index within the source.
|
||||
pub fn stream(&self) -> i32 {
|
||||
self.stream
|
||||
}
|
||||
|
||||
/// Associated timeline block (borrowed; only compared, never used).
|
||||
pub fn block(&self) -> Option<crate::bridge::common::OakNodeBlock> {
|
||||
self.block.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// `olive::Decoder` — abstraction over external media decoding.
|
||||
///
|
||||
/// Implementations are [`crate::ffmpeg::FFmpegDecoder`] and
|
||||
/// [`crate::oiio::OIIODecoder`]. The trait surface mirrors the C++
|
||||
/// abstract base; the refcounted handle that backs the public API wraps an
|
||||
/// `Arc<dyn Decoder>`.
|
||||
pub trait Decoder: Send + Sync {
|
||||
/// Unique decoder id ("ffmpeg"/"oiio").
|
||||
fn id(&self) -> String;
|
||||
|
||||
/// Whether this decoder supports video streams.
|
||||
fn supports_video(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether this decoder supports audio streams.
|
||||
fn supports_audio(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether this decoder can read the given file (static probe).
|
||||
fn probe(
|
||||
&self,
|
||||
filename: &str,
|
||||
cancelled: Option<&OakCancelAtom>,
|
||||
) -> Option<FootageDescription>;
|
||||
|
||||
/// Open `stream` for decoding. Thread-safe.
|
||||
fn open(&self, stream: &CodecStream) -> crate::error::Result<()>;
|
||||
|
||||
/// Close the currently open stream (safe when closed).
|
||||
fn close(&self) -> crate::error::Result<()>;
|
||||
|
||||
/// The currently open stream (locked accessor).
|
||||
fn stream(&self) -> CodecStream;
|
||||
|
||||
/// Retrieve a video frame into CPU memory.
|
||||
fn retrieve_video_frame(
|
||||
&self,
|
||||
p: &RetrieveVideoParams,
|
||||
) -> crate::error::Result<Arc<Frame>>;
|
||||
|
||||
/// Retrieve a video frame as a render texture (owned by caller).
|
||||
fn retrieve_video(&self, p: &RetrieveVideoParams) -> crate::error::Result<OakRenderTexture>;
|
||||
|
||||
/// Retrieve interleaved audio covering `range` into `dest` (floats).
|
||||
fn retrieve_audio(
|
||||
&self,
|
||||
dest: &mut [f32],
|
||||
range: &TimeRange,
|
||||
sample_rate: i32,
|
||||
channel_layout: u64,
|
||||
) -> crate::error::Result<RetrieveAudioStatus>;
|
||||
|
||||
/// Conform the open stream's audio into per-channel pcm files.
|
||||
///
|
||||
/// `sample_rate` / `channel_layout` / `sample_format` describe the
|
||||
/// target audio format (`sample_format` is a
|
||||
/// `olive::core::SampleFormat::Format` value). The C++ side builds its
|
||||
/// `core::AudioParams` from these three — mirroring the C ABI
|
||||
/// `oakcodec_decoder_conform_audio` argument list.
|
||||
fn conform_audio(
|
||||
&self,
|
||||
output_filenames: &[String],
|
||||
sample_rate: i32,
|
||||
channel_layout: u64,
|
||||
sample_format: i32,
|
||||
cancelled: Option<&OakCancelAtom>,
|
||||
) -> crate::error::Result<()>;
|
||||
|
||||
/// Offset of the audio start relative to the video (rational seconds).
|
||||
fn get_audio_start_offset(&self) -> Rational {
|
||||
// C++ default `virtual Rational get_audio_start_offset() const { return 0; }`
|
||||
Rational::new(0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Placeholder decoder used by the built-in probe registry.
|
||||
///
|
||||
/// Reports the correct id and capability flags so id-based dispatch
|
||||
/// (`create_from_id`) works, but every media operation is unimplemented
|
||||
/// and returns `None` / an error. Used for the OIIO entry, whose Rust
|
||||
/// implementation (`crate::oiio::OIIODecoder`) is still a dylib stub; the
|
||||
/// FFmpeg entry is the real [`crate::ffmpeg::FFmpegDecoder`].
|
||||
struct UnimplementedDecoder {
|
||||
id: &'static str,
|
||||
video: bool,
|
||||
audio: bool,
|
||||
}
|
||||
|
||||
impl UnimplementedDecoder {
|
||||
fn new(id: &'static str, video: bool, audio: bool) -> Self {
|
||||
UnimplementedDecoder { id, video, audio }
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder for UnimplementedDecoder {
|
||||
fn id(&self) -> String {
|
||||
self.id.to_string()
|
||||
}
|
||||
|
||||
fn supports_video(&self) -> bool {
|
||||
self.video
|
||||
}
|
||||
|
||||
fn supports_audio(&self) -> bool {
|
||||
self.audio
|
||||
}
|
||||
|
||||
fn probe(
|
||||
&self,
|
||||
_filename: &str,
|
||||
_cancelled: Option<&OakCancelAtom>,
|
||||
) -> Option<FootageDescription> {
|
||||
None
|
||||
}
|
||||
|
||||
fn open(&self, _stream: &CodecStream) -> crate::error::Result<()> {
|
||||
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
|
||||
}
|
||||
|
||||
fn close(&self) -> crate::error::Result<()> {
|
||||
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
|
||||
}
|
||||
|
||||
fn stream(&self) -> CodecStream {
|
||||
CodecStream::new()
|
||||
}
|
||||
|
||||
fn retrieve_video_frame(
|
||||
&self,
|
||||
_p: &RetrieveVideoParams,
|
||||
) -> crate::error::Result<Arc<Frame>> {
|
||||
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
|
||||
}
|
||||
|
||||
fn retrieve_video(
|
||||
&self,
|
||||
_p: &RetrieveVideoParams,
|
||||
) -> crate::error::Result<OakRenderTexture> {
|
||||
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
|
||||
}
|
||||
|
||||
fn retrieve_audio(
|
||||
&self,
|
||||
_dest: &mut [f32],
|
||||
_range: &TimeRange,
|
||||
_sample_rate: i32,
|
||||
_channel_layout: u64,
|
||||
) -> crate::error::Result<RetrieveAudioStatus> {
|
||||
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
|
||||
}
|
||||
|
||||
fn conform_audio(
|
||||
&self,
|
||||
_output_filenames: &[String],
|
||||
_sample_rate: i32,
|
||||
_channel_layout: u64,
|
||||
_sample_format: i32,
|
||||
_cancelled: Option<&OakCancelAtom>,
|
||||
) -> crate::error::Result<()> {
|
||||
Err(crate::error::Error::Failed("decoder not yet implemented".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// `Decoder::create_from_id` — instantiate a decoder by id, or `None`.
|
||||
pub fn create_from_id(id: &str) -> Option<Arc<dyn Decoder>> {
|
||||
if id.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
receive_list_of_all_decoders()
|
||||
.into_iter()
|
||||
.find(|d| d.id() == id)
|
||||
}
|
||||
|
||||
/// Test-injected decoder registry (see [`set_test_decoders`]); empty when
|
||||
/// not injected, in which case the built-in list below is used.
|
||||
static TEST_DECODERS: OnceLock<Mutex<Vec<Arc<dyn Decoder>>>> = OnceLock::new();
|
||||
|
||||
/// Serializes every test that reads the built-in decoder registry. The ffi
|
||||
/// decoder tests inject through `crate::ffi::lock_tests()` (the shared
|
||||
/// `TEST_LOCK`), so the registry assertions below take that same lock to
|
||||
/// never race with an injected list.
|
||||
#[cfg(test)]
|
||||
fn registry_guard() -> std::sync::MutexGuard<'static, ()> {
|
||||
crate::ffi::lock_tests()
|
||||
}
|
||||
|
||||
/// Replace the decoder registry with `list`; pass an empty list to restore
|
||||
/// the built-in decoders.
|
||||
///
|
||||
/// Test/extension support (the C ABI has no way to register a decoder, so
|
||||
/// the contract tests drive the probe/dispatch paths through a fake
|
||||
/// decoder). Hidden from docs; never called by production code.
|
||||
#[doc(hidden)]
|
||||
pub fn set_test_decoders(list: Vec<Arc<dyn Decoder>>) {
|
||||
let store = TEST_DECODERS.get_or_init(|| Mutex::new(Vec::new()));
|
||||
*store.lock().unwrap() = list;
|
||||
}
|
||||
|
||||
/// `Decoder::receive_list_of_all_decoders` — all registered decoders.
|
||||
///
|
||||
/// Order is probe priority, mirroring C++: OIIO (more specific) before
|
||||
/// FFmpeg (format-agnostic fallback). The OIIO entry is an
|
||||
/// [`UnimplementedDecoder`] stub (the OIIO engine is not ported); the
|
||||
/// FFmpeg entry is the real [`crate::ffmpeg::FFmpegDecoder`]. When tests
|
||||
/// injected a non-empty list via [`set_test_decoders`], that list takes
|
||||
/// precedence.
|
||||
pub fn receive_list_of_all_decoders() -> Vec<Arc<dyn Decoder>> {
|
||||
if let Some(store) = TEST_DECODERS.get() {
|
||||
let injected = store.lock().unwrap();
|
||||
if !injected.is_empty() {
|
||||
return injected.clone();
|
||||
}
|
||||
}
|
||||
vec![
|
||||
Arc::new(UnimplementedDecoder::new("oiio", false, false)),
|
||||
Arc::new(crate::ffmpeg::FFmpegDecoder::new()),
|
||||
]
|
||||
}
|
||||
|
||||
/// Image-sequence filename heuristics (static).
|
||||
///
|
||||
/// Replace the trailing digit run of the filename stem with the
|
||||
/// zero-padded decimal representation of `number` (keeps the same digit
|
||||
/// count), mirroring `Decoder::transform_image_sequence_file_name`.
|
||||
pub fn transform_image_sequence_file_name(filename: &str, number: i64) -> String {
|
||||
let digit_count = get_image_sequence_digit_count(filename) as usize;
|
||||
|
||||
let path = Path::new(filename);
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or(filename);
|
||||
|
||||
// QFileInfo::completeBaseName(): filename up to the first '.'.
|
||||
let original_basename = match file_name.find('.') {
|
||||
Some(dot) => &file_name[..dot],
|
||||
None => file_name,
|
||||
};
|
||||
|
||||
// New stem = original stem minus the trailing digit run, plus the
|
||||
// zero-padded number (`snprintf("%0*lld", digit_count, number)`).
|
||||
let cut = original_basename.len().saturating_sub(digit_count);
|
||||
let new_basename = format!(
|
||||
"{}{:0width$}",
|
||||
&original_basename[..cut],
|
||||
number,
|
||||
width = digit_count
|
||||
);
|
||||
|
||||
// Replace every occurrence of the original stem in the filename.
|
||||
let mut new_filename = file_name.to_string();
|
||||
let mut pos = 0;
|
||||
while let Some(rel) = new_filename[pos..].find(original_basename) {
|
||||
let start = pos + rel;
|
||||
let end = start + original_basename.len();
|
||||
new_filename.replace_range(start..end, &new_basename);
|
||||
pos = start + new_basename.len();
|
||||
}
|
||||
|
||||
match path.parent() {
|
||||
Some(parent) if !parent.as_os_str().is_empty() => {
|
||||
Path::new(parent).join(&new_filename).to_string_lossy().into_owned()
|
||||
}
|
||||
_ => new_filename,
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of trailing digits in the filename stem (0 = not a sequence).
|
||||
pub fn get_image_sequence_digit_count(filename: &str) -> i32 {
|
||||
let file_name = Path::new(filename)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or(filename);
|
||||
|
||||
// QFileInfo::completeBaseName(): filename up to the first '.'.
|
||||
let stem = match file_name.find('.') {
|
||||
Some(dot) => &file_name[..dot],
|
||||
None => file_name,
|
||||
};
|
||||
|
||||
let mut count: i32 = 0;
|
||||
for ch in stem.chars().rev() {
|
||||
if ch.is_ascii_digit() {
|
||||
count += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
/// Numeric value of the trailing digits (0 when there are none).
|
||||
///
|
||||
/// Mirrors C++ `Decoder::get_image_sequence_index`, which slices the
|
||||
/// trailing digit run (`basename.substr(basename.size() - digit_count)`) and
|
||||
/// passes it to `strtoll`. Because that slice is empty when there are no
|
||||
/// trailing digits (digit_count == 0) and all-digits otherwise, the value is
|
||||
/// the parsed number, or `0` for a non-sequence.
|
||||
pub fn get_image_sequence_index(filename: &str) -> i64 {
|
||||
let digit_count = get_image_sequence_digit_count(filename) as usize;
|
||||
|
||||
let file_name = Path::new(filename)
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or(filename);
|
||||
|
||||
// QFileInfo::completeBaseName(): filename up to the first '.'.
|
||||
let stem = match file_name.find('.') {
|
||||
Some(dot) => &file_name[..dot],
|
||||
None => file_name,
|
||||
};
|
||||
|
||||
// Trailing digit run (empty when the stem has no trailing digits).
|
||||
let start = stem.len().saturating_sub(digit_count);
|
||||
let number_only = &stem[start..];
|
||||
|
||||
// `strtoll(..., base 10)`: the slice is empty-or-digits, so a plain
|
||||
// decimal parse with 0 on failure reproduces the C++ result.
|
||||
number_only.parse::<i64>().unwrap_or(0)
|
||||
}
|
||||
|
||||
/// The `k_any_timecode` rational constant.
|
||||
///
|
||||
/// C++ `const Rational Decoder::k_any_timecode = RATIONAL_MIN;`, which the
|
||||
/// i32 reduction cap normalizes to `-2147483647/1`.
|
||||
pub fn k_any_timecode() -> Rational {
|
||||
Rational::new(-2147483647, 1)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn codec_stream_new_is_invalid() {
|
||||
let s = CodecStream::new();
|
||||
assert!(!s.is_valid());
|
||||
assert!(s.filename().is_empty());
|
||||
assert_eq!(s.stream(), -1);
|
||||
assert_eq!(s.block(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_stream_with_block_is_valid() {
|
||||
let s = CodecStream::with_block("video.mov".to_string(), 1, None);
|
||||
assert!(s.is_valid());
|
||||
assert_eq!(s.filename(), "video.mov");
|
||||
assert_eq!(s.stream(), 1);
|
||||
|
||||
// Negative stream index is invalid regardless of filename.
|
||||
let bad = CodecStream::with_block("video.mov".to_string(), -1, None);
|
||||
assert!(!bad.is_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_stream_reset_clears() {
|
||||
let mut s = CodecStream::with_block("video.mov".to_string(), 2, None);
|
||||
s.reset();
|
||||
assert!(!s.is_valid());
|
||||
assert!(s.filename().is_empty());
|
||||
assert_eq!(s.stream(), -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn digit_count_counts_trailing_digits() {
|
||||
assert_eq!(get_image_sequence_digit_count("frame_0001.png"), 4);
|
||||
assert_eq!(get_image_sequence_digit_count("frame.png"), 0);
|
||||
assert_eq!(get_image_sequence_digit_count("img000.jpg"), 3);
|
||||
// Digits before the final char are not trailing digits.
|
||||
assert_eq!(get_image_sequence_digit_count("a1b.png"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_sequence_index_parses_number() {
|
||||
assert_eq!(get_image_sequence_index("frame_0001.png"), 1);
|
||||
assert_eq!(get_image_sequence_index("img012.jpg"), 12);
|
||||
assert_eq!(get_image_sequence_index("0009.png"), 9);
|
||||
// No trailing digits: the sliced run is empty, so the value is 0.
|
||||
assert_eq!(get_image_sequence_index("frame.png"), 0);
|
||||
assert_eq!(get_image_sequence_index("12abc.png"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_image_sequence_substitutes_number() {
|
||||
assert_eq!(
|
||||
transform_image_sequence_file_name("frame_0001.png", 5),
|
||||
"frame_0005.png"
|
||||
);
|
||||
assert_eq!(
|
||||
transform_image_sequence_file_name("dir/img012.jpg", 7),
|
||||
"dir/img007.jpg"
|
||||
);
|
||||
// No digit run: number appended with no padding (C++ behavior).
|
||||
assert_eq!(
|
||||
transform_image_sequence_file_name("frame.png", 3),
|
||||
"frame3.png"
|
||||
);
|
||||
// All-digit stem: whole run is replaced.
|
||||
assert_eq!(
|
||||
transform_image_sequence_file_name("0001.png", 7),
|
||||
"0007.png"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn k_any_timecode_is_rational_min() {
|
||||
let tc = k_any_timecode();
|
||||
assert_eq!(tc.numerator(), -2147483647);
|
||||
assert_eq!(tc.denominator(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_lists_oiio_then_ffmpeg() {
|
||||
let _g = registry_guard();
|
||||
let list = receive_list_of_all_decoders();
|
||||
let ids: Vec<String> = list.iter().map(|d| d.id()).collect();
|
||||
// Probe priority: OIIO (specific) first, FFmpeg (fallback) last.
|
||||
assert_eq!(ids, vec!["oiio".to_string(), "ffmpeg".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_from_id_matches_registry() {
|
||||
let _g = registry_guard();
|
||||
assert!(create_from_id("ffmpeg").is_some());
|
||||
assert!(create_from_id("oiio").is_some());
|
||||
assert_eq!(create_from_id("ffmpeg").unwrap().id(), "ffmpeg");
|
||||
assert_eq!(create_from_id("oiio").unwrap().id(), "oiio");
|
||||
// Unknown and empty ids return None.
|
||||
assert!(create_from_id("nope").is_none());
|
||||
assert!(create_from_id("").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_unimplemented {
|
||||
use super::*;
|
||||
|
||||
fn builtin(id: &str) -> Arc<dyn Decoder> {
|
||||
create_from_id(id).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffmpeg_builtin_fails_on_missing_media_and_closes() {
|
||||
let _g = registry_guard();
|
||||
let d = builtin("ffmpeg");
|
||||
assert!(d.supports_video());
|
||||
assert!(d.supports_audio());
|
||||
// A nonexistent file cannot be probed or opened.
|
||||
assert!(d.probe("x.mp4", None).is_none());
|
||||
|
||||
let s = CodecStream::with_block("x.mp4".to_string(), 0, None);
|
||||
assert!(d.open(&s).is_err());
|
||||
// C++ parity: a failed open leaves the decoder closed.
|
||||
assert_eq!(d.stream().filename(), "");
|
||||
assert!(d.close().is_ok());
|
||||
|
||||
let p = RetrieveVideoParams {
|
||||
stream: CodecStream::new(),
|
||||
time: Rational::new(0, 1),
|
||||
length: TimeRange::default(),
|
||||
force_range: K_COLOR_RANGE_DEFAULT,
|
||||
is_image_sequence: false,
|
||||
image_sequence_digits: 0,
|
||||
image_sequence_number: 0,
|
||||
mode: RenderMode::Offline,
|
||||
alpha_is_premultiplied: false,
|
||||
};
|
||||
assert!(d.retrieve_video_frame(&p).is_err());
|
||||
assert!(d.retrieve_video(&p).is_err());
|
||||
let mut dest = [0f32; 4];
|
||||
assert!(d
|
||||
.retrieve_audio(
|
||||
&mut dest,
|
||||
&TimeRange::new(Rational::new(0, 1), Rational::new(1, 1)),
|
||||
48000,
|
||||
0x3
|
||||
)
|
||||
.is_err());
|
||||
assert!(d
|
||||
.conform_audio(&["a.pcm".to_string()], 48000, 0x3, 10, None)
|
||||
.is_err());
|
||||
|
||||
// OIIO reports no media capabilities.
|
||||
let o = builtin("oiio");
|
||||
assert!(!o.supports_video());
|
||||
assert!(!o.supports_audio());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_audio_start_offset_defaults_to_zero() {
|
||||
let _g = registry_guard();
|
||||
let d = builtin("ffmpeg");
|
||||
let off = d.get_audio_start_offset();
|
||||
assert_eq!(off.numerator(), 0);
|
||||
assert_eq!(off.denominator(), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// 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/>.
|
||||
|
||||
//! `olive::Encoder` — abstract base for media encoders.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/encoder.h`. Implementations are
|
||||
//! [`crate::ffmpeg::FFmpegEncoder`] and [`crate::oiio::OIIOEncoder`]. The
|
||||
//! workflow (C ABI encoder.h) is: fill an `EncodingParams` → init → open →
|
||||
//! write_video/audio/subtitle → flush. The trait mirrors the C++ virtual
|
||||
//! surface.
|
||||
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use oakcore_rs::{PixelFormat, SampleFormat};
|
||||
|
||||
use crate::encodingparams::EncodingParams;
|
||||
use crate::frame::Frame;
|
||||
|
||||
/// `olive::Encoder` — encoder trait. Backs the refcounted encoder handle.
|
||||
pub trait Encoder: Send + Sync {
|
||||
/// Unique encoder id.
|
||||
fn id(&self) -> String;
|
||||
|
||||
/// Whether this encoder writes video.
|
||||
fn supports_video(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether this encoder writes audio.
|
||||
fn supports_audio(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether this encoder writes subtitles.
|
||||
fn supports_subtitles(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether this encoder writes an image sequence.
|
||||
fn supports_image_sequences(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether this encoder is deterministic for a given config
|
||||
/// (used for cache keys).
|
||||
fn is_configurable(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Configure the encoder (per-codec options like `crf`).
|
||||
fn configure(&self, params: &EncodingParams) -> crate::error::Result<()>;
|
||||
|
||||
/// Open the output file and write headers.
|
||||
fn open(&self) -> crate::error::Result<()>;
|
||||
|
||||
/// Close the output (write trailer); idempotent.
|
||||
fn close(&self) -> crate::error::Result<()>;
|
||||
|
||||
/// Encode one video frame (converts delivery pixel format internally).
|
||||
fn write_video(&self, frame: &Frame) -> crate::error::Result<()>;
|
||||
|
||||
/// Encode interleaved float audio samples.
|
||||
fn write_audio(&self, samples: &[f32], frame_count: i32) -> crate::error::Result<()>;
|
||||
|
||||
/// Encode one subtitle entry (times in seconds).
|
||||
fn write_subtitle(
|
||||
&self,
|
||||
text: &str,
|
||||
in_seconds: f64,
|
||||
out_seconds: f64,
|
||||
) -> crate::error::Result<()>;
|
||||
|
||||
/// Flush encoders, write the trailer, close the file.
|
||||
fn flush(&self) -> crate::error::Result<()>;
|
||||
|
||||
/// The pixel format the encoder wants frames in (or `None`).
|
||||
fn desired_pixel_format(&self) -> Option<PixelFormat>;
|
||||
|
||||
/// The sample format the encoder wants audio in (or `None`).
|
||||
fn desired_sample_format(&self) -> Option<SampleFormat>;
|
||||
|
||||
/// The configured output filename.
|
||||
fn filename(&self) -> String;
|
||||
|
||||
/// Human-readable detail of the last failed operation (empty when
|
||||
/// none). Mirrors the C++ `Encoder::get_error()` used by
|
||||
/// `oakcodec_encoder_last_error`.
|
||||
fn get_error(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Test-injected encoder registry (see [`set_test_encoders`]); empty when
|
||||
/// not injected, in which case [`create_from_params`] falls back to the
|
||||
/// built-in format mapping.
|
||||
static TEST_ENCODERS: OnceLock<Mutex<Vec<Arc<dyn Encoder>>>> = OnceLock::new();
|
||||
|
||||
/// Replace the encoder registry with `list`; pass an empty list to restore
|
||||
/// the built-in behavior.
|
||||
///
|
||||
/// Test/extension support (the C ABI has no way to register an encoder, so
|
||||
/// the contract tests drive the encode state machine through a fake
|
||||
/// encoder). Hidden from docs; never called by production code.
|
||||
#[doc(hidden)]
|
||||
pub fn set_test_encoders(list: Vec<Arc<dyn Encoder>>) {
|
||||
let store = TEST_ENCODERS.get_or_init(|| Mutex::new(Vec::new()));
|
||||
*store.lock().unwrap() = list;
|
||||
}
|
||||
|
||||
/// `Encoder::create_from_params` — instantiate an encoder for `params`.
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// `src/codec/src/encoder.cpp` `create_from_params` → `create_from_format`
|
||||
/// picks the FFmpeg/OIIO implementation from `params.format` (DNxHD,
|
||||
/// Matroska, QuickTime, MPEG-4 video/audio, WAV, AIFF, MP3, FLAC, Ogg,
|
||||
/// WebM, SRT → FFmpeg; OpenEXR, PNG, TIFF → OIIO; anything else → `None`).
|
||||
/// A non-empty test-injected list (see [`set_test_encoders`]) wins over the
|
||||
/// built-in mapping. The concrete implementations are dylib stubs whose
|
||||
/// `open()` fails with a clear message, so an initialized encoder handle is
|
||||
/// always constructible for a recognized format.
|
||||
pub fn create_from_params(params: &EncodingParams) -> Option<Arc<dyn Encoder>> {
|
||||
if let Some(store) = TEST_ENCODERS.get() {
|
||||
let injected = store.lock().unwrap();
|
||||
if !injected.is_empty() {
|
||||
return injected.first().cloned();
|
||||
}
|
||||
}
|
||||
match encoder_type_from_format(params.format) {
|
||||
Some(EncoderType::FFmpeg) => {
|
||||
Some(Arc::new(crate::ffmpeg::FFmpegEncoder::with_params(params.clone())))
|
||||
}
|
||||
Some(EncoderType::OIIO) => {
|
||||
Some(Arc::new(crate::oiio::OIIOEncoder { params: params.clone() }))
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `Encoder::Type` mirror (`encoder.cpp` `get_type_from_format`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum EncoderType {
|
||||
/// `k_encoder_type_f_fmpeg`.
|
||||
FFmpeg,
|
||||
/// `k_encoder_type_oiio`.
|
||||
OIIO,
|
||||
}
|
||||
|
||||
/// `Encoder::get_type_from_format` — the implementation family for an
|
||||
/// `ExportFormat::Format` int; `None` for unknown/`Count`.
|
||||
fn encoder_type_from_format(format: i32) -> Option<EncoderType> {
|
||||
match format {
|
||||
// FFmpeg-backed containers.
|
||||
0 | 1 | 2 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 => Some(EncoderType::FFmpeg),
|
||||
// OIIO-backed still-image formats.
|
||||
3 | 5 | 6 => Some(EncoderType::OIIO),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Range `(start, end)` of a "[#####]" digit placeholder beginning at
|
||||
/// `pos` (`bytes[pos] == '['`), or `None`.
|
||||
///
|
||||
/// CPP-PARITY: `encoder.cpp` `k_image_sequence_contains_digits` regex
|
||||
/// `\[[#]+\]` — a `[`, one or more `#`, then `]`.
|
||||
fn placeholder_range(bytes: &[u8], pos: usize) -> Option<(usize, usize)> {
|
||||
if bytes.get(pos) != Some(&b'[') {
|
||||
return None;
|
||||
}
|
||||
let mut j = pos + 1;
|
||||
while bytes.get(j) == Some(&b'#') {
|
||||
j += 1;
|
||||
}
|
||||
if j > pos + 1 && bytes.get(j) == Some(&b']') {
|
||||
Some((pos, j + 1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// `Encoder::filename_contains_digit_placeholder` — whether `filename`
|
||||
/// contains a "[#####]" digit placeholder.
|
||||
///
|
||||
/// CPP-PARITY: `encoder.cpp:137` (`std::regex_search` on
|
||||
/// `k_image_sequence_contains_digits`).
|
||||
pub fn filename_contains_digit_placeholder(filename: &str) -> bool {
|
||||
let bytes = filename.as_bytes();
|
||||
(0..bytes.len()).any(|i| placeholder_range(bytes, i).is_some())
|
||||
}
|
||||
|
||||
/// `Encoder::get_image_sequence_placeholder_digit_count` — number of `#` in
|
||||
/// the filename's "[#####]" placeholder; 0 when none.
|
||||
///
|
||||
/// CPP-PARITY: `encoder.cpp:119` — the C++ finds the first
|
||||
/// `k_image_sequence_contains_digits` match and counts its `#`s, which is
|
||||
/// exactly the match length minus the two brackets.
|
||||
pub fn image_sequence_placeholder_digit_count(filename: &str) -> i32 {
|
||||
let bytes = filename.as_bytes();
|
||||
for i in 0..bytes.len() {
|
||||
if let Some((start, end)) = placeholder_range(bytes, i) {
|
||||
return (end - start - 2) as i32;
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// `Encoder::filename_remove_digit_placeholder` — `filename` with every
|
||||
/// "[#####]" placeholder removed; an optional single separator char
|
||||
/// (`-`, `.`, ` `, `_`) immediately before the placeholder goes with it.
|
||||
///
|
||||
/// CPP-PARITY: `encoder.cpp:142` (`std::regex_replace` on
|
||||
/// `k_image_sequence_remove_digits` = `[\-\.\ \_]?\[[#]+\]`, empty
|
||||
/// replacement, all matches).
|
||||
pub fn filename_remove_digit_placeholder(filename: &str) -> String {
|
||||
let bytes = filename.as_bytes();
|
||||
let mut out = Vec::with_capacity(bytes.len());
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
// A separator is consumed only when a placeholder follows it.
|
||||
let ph_start = match bytes[i] {
|
||||
b'-' | b'.' | b' ' | b'_' if placeholder_range(bytes, i + 1).is_some() => {
|
||||
i + 1
|
||||
}
|
||||
_ => i,
|
||||
};
|
||||
match placeholder_range(bytes, ph_start) {
|
||||
Some((_, end)) => i = end,
|
||||
None => {
|
||||
out.push(bytes[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
String::from_utf8(out).unwrap_or_else(|_| filename.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn create_from_params_maps_formats() {
|
||||
let mut p = EncodingParams::default();
|
||||
|
||||
// FFmpeg-backed containers.
|
||||
for fmt in [0, 1, 2, 4, 7, 8, 9, 10, 11, 12, 13, 14] {
|
||||
p.format = fmt;
|
||||
let e = create_from_params(&p).expect("format {fmt}");
|
||||
assert_eq!(e.id(), "ffmpeg", "format {fmt}");
|
||||
}
|
||||
// OIIO-backed still images.
|
||||
for fmt in [3, 5, 6] {
|
||||
p.format = fmt;
|
||||
let e = create_from_params(&p).expect("format {fmt}");
|
||||
assert_eq!(e.id(), "oiio", "format {fmt}");
|
||||
}
|
||||
// Unknown / Count -> None (C++ `k_encoder_type_none`).
|
||||
p.format = 15;
|
||||
assert!(create_from_params(&p).is_none());
|
||||
p.format = -1;
|
||||
assert!(create_from_params(&p).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_error_defaults_to_empty() {
|
||||
let e = UnimplementedDummy;
|
||||
assert_eq!(e.get_error(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_sequence_placeholder_helpers() {
|
||||
// contains: "[#####]" style placeholder only.
|
||||
assert!(filename_contains_digit_placeholder("/tmp/out_[#####].png"));
|
||||
assert!(filename_contains_digit_placeholder("out[#].png"));
|
||||
assert!(!filename_contains_digit_placeholder("/tmp/out.png"));
|
||||
assert!(!filename_contains_digit_placeholder("out[####.png"));
|
||||
assert!(!filename_contains_digit_placeholder("out[].png"));
|
||||
|
||||
// digit count: number of '#' in the first placeholder.
|
||||
assert_eq!(image_sequence_placeholder_digit_count("/tmp/out_[#####].png"), 5);
|
||||
assert_eq!(image_sequence_placeholder_digit_count("out[#].png"), 1);
|
||||
assert_eq!(image_sequence_placeholder_digit_count("a[##]b[####]c"), 2);
|
||||
assert_eq!(image_sequence_placeholder_digit_count("/tmp/out.png"), 0);
|
||||
|
||||
// remove: separator char before the placeholder goes with it.
|
||||
assert_eq!(filename_remove_digit_placeholder("/tmp/out_[#####].png"), "/tmp/out.png");
|
||||
assert_eq!(filename_remove_digit_placeholder("out[###].png"), "out.png");
|
||||
assert_eq!(filename_remove_digit_placeholder("a_[#]b_[###]c"), "abc");
|
||||
assert_eq!(filename_remove_digit_placeholder("/tmp/out.png"), "/tmp/out.png");
|
||||
}
|
||||
|
||||
struct UnimplementedDummy;
|
||||
|
||||
impl Encoder for UnimplementedDummy {
|
||||
fn id(&self) -> String {
|
||||
"dummy".to_string()
|
||||
}
|
||||
fn configure(&self, _p: &EncodingParams) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn open(&self) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn close(&self) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn write_video(&self, _f: &Frame) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn write_audio(&self, _s: &[f32], _c: i32) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn write_subtitle(&self, _t: &str, _i: f64, _o: f64) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn flush(&self) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn desired_pixel_format(&self) -> Option<PixelFormat> {
|
||||
None
|
||||
}
|
||||
fn desired_sample_format(&self) -> Option<SampleFormat> {
|
||||
None
|
||||
}
|
||||
fn filename(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,70 @@
|
||||
// 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/>.
|
||||
|
||||
//! Error codes, mirroring `include/codec/error.h` verbatim; project-wide
|
||||
//! -MMCCCC scheme (module registry in include/common/error.h), pass-through untranslated.
|
||||
|
||||
/// Success.
|
||||
pub const OAKCODEC_OK: i32 = 0;
|
||||
/// Null handle or invalid argument.
|
||||
pub const OAKCODEC_E_INVALID: i32 = -50001;
|
||||
/// Call not valid in the current state.
|
||||
pub const OAKCODEC_E_STATE: i32 = -50002;
|
||||
/// The underlying operation failed.
|
||||
pub const OAKCODEC_E_FAILED: i32 = -50003;
|
||||
/// Index out of range / entry not found.
|
||||
pub const OAKCODEC_E_NOT_FOUND: i32 = -50004;
|
||||
/// Allocation failed.
|
||||
pub const OAKCODEC_E_NOMEM: i32 = -50005;
|
||||
/// The operation was cancelled.
|
||||
pub const OAKCODEC_E_CANCELLED: i32 = -50006;
|
||||
|
||||
/// Current ABI version stamped into every oakcodec handle.
|
||||
pub const OAKCODEC_ABI_VERSION: u32 = 1;
|
||||
|
||||
/// Crate-internal result type; the FFI layer maps it to the codes.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Crate-internal error.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Null handle or invalid argument.
|
||||
Invalid,
|
||||
/// Wrong state.
|
||||
State,
|
||||
/// Operation failed (context string is log-only).
|
||||
Failed(String),
|
||||
/// Not found.
|
||||
NotFound,
|
||||
/// Out of memory.
|
||||
NoMem,
|
||||
/// The operation was cancelled.
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Map to the public error code.
|
||||
pub fn code(&self) -> i32 {
|
||||
match self {
|
||||
Error::Invalid => OAKCODEC_E_INVALID,
|
||||
Error::State => OAKCODEC_E_STATE,
|
||||
Error::Failed(_) => OAKCODEC_E_FAILED,
|
||||
Error::NotFound => OAKCODEC_E_NOT_FOUND,
|
||||
Error::NoMem => OAKCODEC_E_NOMEM,
|
||||
Error::Cancelled => OAKCODEC_E_CANCELLED,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
// 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/>.
|
||||
|
||||
//! `olive::ExportCodec` — the export codec enum and name table.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/exportcodec.h`. The enum is the raw int contract
|
||||
//! the C ABI documents (oakengine/encoding.h); values must NOT be
|
||||
//! reordered (they are used in serialized files).
|
||||
|
||||
/// `olive::ExportCodec::Codec`. Only append (never insert/reorder); the
|
||||
/// integer values are part of the file format.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum Codec {
|
||||
/// Apple ProRes.
|
||||
DNxHD = 0,
|
||||
/// H.264.
|
||||
H264 = 1,
|
||||
/// H.264 with RGB color.
|
||||
H264RGB = 2,
|
||||
/// H.265/HEVC.
|
||||
H265 = 3,
|
||||
/// OpenEXR.
|
||||
OpenEXR = 4,
|
||||
/// PNG.
|
||||
PNG = 5,
|
||||
/// Apple ProRes.
|
||||
ProRes = 6,
|
||||
/// CineForm.
|
||||
CineForm = 7,
|
||||
/// TIFF.
|
||||
TIFF = 8,
|
||||
/// VP9.
|
||||
VP9 = 9,
|
||||
/// MPEG-2.
|
||||
MP2 = 10,
|
||||
/// MPEG-3.
|
||||
MP3 = 11,
|
||||
/// AAC.
|
||||
AAC = 12,
|
||||
/// Uncompressed PCM.
|
||||
PCM = 13,
|
||||
/// Opus.
|
||||
Opus = 14,
|
||||
/// Vorbis.
|
||||
Vorbis = 15,
|
||||
/// FLAC.
|
||||
FLAC = 16,
|
||||
/// SubRip subtitles.
|
||||
SRT = 17,
|
||||
/// AV1.
|
||||
AV1 = 18,
|
||||
/// Sentinel count (not a real codec).
|
||||
Count = 19,
|
||||
}
|
||||
|
||||
impl Codec {
|
||||
/// Convert an `i32` code to a [`Codec`]; `None` outside the valid
|
||||
/// `0..=18` range (incl. `Count`).
|
||||
pub fn from_i32(v: i32) -> Option<Codec> {
|
||||
match v {
|
||||
0 => Some(Codec::DNxHD),
|
||||
1 => Some(Codec::H264),
|
||||
2 => Some(Codec::H264RGB),
|
||||
3 => Some(Codec::H265),
|
||||
4 => Some(Codec::OpenEXR),
|
||||
5 => Some(Codec::PNG),
|
||||
6 => Some(Codec::ProRes),
|
||||
7 => Some(Codec::CineForm),
|
||||
8 => Some(Codec::TIFF),
|
||||
9 => Some(Codec::VP9),
|
||||
10 => Some(Codec::MP2),
|
||||
11 => Some(Codec::MP3),
|
||||
12 => Some(Codec::AAC),
|
||||
13 => Some(Codec::PCM),
|
||||
14 => Some(Codec::Opus),
|
||||
15 => Some(Codec::Vorbis),
|
||||
16 => Some(Codec::FLAC),
|
||||
17 => Some(Codec::SRT),
|
||||
18 => Some(Codec::AV1),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable name for a codec.
|
||||
///
|
||||
/// CPP-PARITY: `src/codec/src/exportcodec.cpp` `get_codec_name`; unknown /
|
||||
/// `Count` returns "Unknown".
|
||||
pub fn get_codec_name(c: Codec) -> String {
|
||||
match c {
|
||||
Codec::DNxHD => "DNxHD".to_string(),
|
||||
Codec::H264 => "H.264".to_string(),
|
||||
Codec::H264RGB => "H.264 RGB".to_string(),
|
||||
Codec::H265 => "H.265".to_string(),
|
||||
Codec::OpenEXR => "OpenEXR".to_string(),
|
||||
Codec::PNG => "PNG".to_string(),
|
||||
Codec::ProRes => "ProRes".to_string(),
|
||||
Codec::CineForm => "Cineform".to_string(),
|
||||
Codec::TIFF => "TIFF".to_string(),
|
||||
Codec::VP9 => "VP9".to_string(),
|
||||
Codec::MP2 => "MP2".to_string(),
|
||||
Codec::MP3 => "MP3".to_string(),
|
||||
Codec::AAC => "AAC".to_string(),
|
||||
Codec::PCM => "PCM (Uncompressed)".to_string(),
|
||||
Codec::Opus => "Opus".to_string(),
|
||||
Codec::Vorbis => "Vorbis".to_string(),
|
||||
Codec::FLAC => "FLAC".to_string(),
|
||||
Codec::SRT => "SubRip SRT".to_string(),
|
||||
Codec::AV1 => "AV1".to_string(),
|
||||
Codec::Count => "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the codec produces a still image.
|
||||
///
|
||||
/// CPP-PARITY: `src/codec/src/exportcodec.cpp` `is_codec_a_still_image`;
|
||||
/// OpenEXR / PNG / TIFF are still images, everything else is not.
|
||||
pub fn is_codec_a_still_image(c: Codec) -> bool {
|
||||
match c {
|
||||
Codec::OpenEXR | Codec::PNG | Codec::TIFF => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the codec is lossless.
|
||||
///
|
||||
/// CPP-PARITY: `src/codec/src/exportcodec.cpp` `is_codec_lossless`;
|
||||
/// only PCM and FLAC are lossless.
|
||||
pub fn is_codec_lossless(c: Codec) -> bool {
|
||||
match c {
|
||||
Codec::PCM | Codec::FLAC => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_i32_maps_valid_and_rejects_others() {
|
||||
for v in 0..=18 {
|
||||
assert!(Codec::from_i32(v).is_some(), "codec {v}");
|
||||
}
|
||||
assert!(Codec::from_i32(19).is_none()); // Count
|
||||
assert!(Codec::from_i32(-1).is_none());
|
||||
assert!(Codec::from_i32(99).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_name_mapping() {
|
||||
assert_eq!(Codec::get_codec_name(Codec::DNxHD), "DNxHD");
|
||||
assert_eq!(Codec::get_codec_name(Codec::H264), "H.264");
|
||||
assert_eq!(Codec::get_codec_name(Codec::H264RGB), "H.264 RGB");
|
||||
assert_eq!(Codec::get_codec_name(Codec::H265), "H.265");
|
||||
assert_eq!(Codec::get_codec_name(Codec::OpenEXR), "OpenEXR");
|
||||
assert_eq!(Codec::get_codec_name(Codec::PNG), "PNG");
|
||||
assert_eq!(Codec::get_codec_name(Codec::ProRes), "ProRes");
|
||||
assert_eq!(Codec::get_codec_name(Codec::CineForm), "Cineform");
|
||||
assert_eq!(Codec::get_codec_name(Codec::TIFF), "TIFF");
|
||||
assert_eq!(Codec::get_codec_name(Codec::VP9), "VP9");
|
||||
assert_eq!(Codec::get_codec_name(Codec::MP2), "MP2");
|
||||
assert_eq!(Codec::get_codec_name(Codec::MP3), "MP3");
|
||||
assert_eq!(Codec::get_codec_name(Codec::AAC), "AAC");
|
||||
assert_eq!(Codec::get_codec_name(Codec::PCM), "PCM (Uncompressed)");
|
||||
assert_eq!(Codec::get_codec_name(Codec::Opus), "Opus");
|
||||
assert_eq!(Codec::get_codec_name(Codec::Vorbis), "Vorbis");
|
||||
assert_eq!(Codec::get_codec_name(Codec::FLAC), "FLAC");
|
||||
assert_eq!(Codec::get_codec_name(Codec::SRT), "SubRip SRT");
|
||||
assert_eq!(Codec::get_codec_name(Codec::AV1), "AV1");
|
||||
assert_eq!(Codec::get_codec_name(Codec::Count), "Unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn still_image_and_lossless_flags() {
|
||||
assert!(Codec::is_codec_a_still_image(Codec::OpenEXR));
|
||||
assert!(Codec::is_codec_a_still_image(Codec::PNG));
|
||||
assert!(Codec::is_codec_a_still_image(Codec::TIFF));
|
||||
assert!(!Codec::is_codec_a_still_image(Codec::H264));
|
||||
assert!(!Codec::is_codec_a_still_image(Codec::PCM));
|
||||
|
||||
assert!(Codec::is_codec_lossless(Codec::PCM));
|
||||
assert!(Codec::is_codec_lossless(Codec::FLAC));
|
||||
assert!(!Codec::is_codec_lossless(Codec::MP3));
|
||||
assert!(!Codec::is_codec_lossless(Codec::H264));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
// 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/>.
|
||||
|
||||
//! `olive::ExportFormat` — the export container-format enum and tables.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/exportformat.h`. As with `ExportCodec`, the enum
|
||||
//! values are the serialized-file contract and must never be reordered.
|
||||
|
||||
use oakcore_rs::SampleFormat;
|
||||
|
||||
use crate::exportcodec::Codec;
|
||||
|
||||
/// `olive::ExportFormat::Format`. Only append (never insert/reorder).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum Format {
|
||||
/// QuickTime DNxHD.
|
||||
DNxHD = 0,
|
||||
/// Matroska.
|
||||
Matroska = 1,
|
||||
/// MPEG-4 video.
|
||||
MPEG4Video = 2,
|
||||
/// OpenEXR.
|
||||
OpenEXR = 3,
|
||||
/// QuickTime.
|
||||
QuickTime = 4,
|
||||
/// PNG.
|
||||
PNG = 5,
|
||||
/// TIFF.
|
||||
TIFF = 6,
|
||||
/// WAV.
|
||||
WAV = 7,
|
||||
/// AIFF.
|
||||
AIFF = 8,
|
||||
/// MP3.
|
||||
MP3 = 9,
|
||||
/// FLAC.
|
||||
FLAC = 10,
|
||||
/// Ogg.
|
||||
Ogg = 11,
|
||||
/// WebM.
|
||||
WebM = 12,
|
||||
/// SubRip.
|
||||
SRT = 13,
|
||||
/// MPEG-4 audio.
|
||||
MPEG4Audio = 14,
|
||||
/// Sentinel count (not a real format).
|
||||
Count = 15,
|
||||
}
|
||||
|
||||
impl Format {
|
||||
/// Convert an `i32` code to a [`Format`]; `None` outside the valid
|
||||
/// `0..=14` range (incl. `Count`).
|
||||
pub fn from_i32(v: i32) -> Option<Format> {
|
||||
match v {
|
||||
0 => Some(Format::DNxHD),
|
||||
1 => Some(Format::Matroska),
|
||||
2 => Some(Format::MPEG4Video),
|
||||
3 => Some(Format::OpenEXR),
|
||||
4 => Some(Format::QuickTime),
|
||||
5 => Some(Format::PNG),
|
||||
6 => Some(Format::TIFF),
|
||||
7 => Some(Format::WAV),
|
||||
8 => Some(Format::AIFF),
|
||||
9 => Some(Format::MP3),
|
||||
10 => Some(Format::FLAC),
|
||||
11 => Some(Format::Ogg),
|
||||
12 => Some(Format::WebM),
|
||||
13 => Some(Format::SRT),
|
||||
14 => Some(Format::MPEG4Audio),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-readable name for a format.
|
||||
///
|
||||
/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_name`; unknown /
|
||||
/// `Count` returns "Unknown".
|
||||
pub fn get_name(f: Format) -> String {
|
||||
match f {
|
||||
Format::DNxHD => "DNxHD".to_string(),
|
||||
Format::Matroska => "Matroska Video".to_string(),
|
||||
Format::MPEG4Video => "MPEG-4 Video".to_string(),
|
||||
Format::MPEG4Audio => "MPEG-4 Audio".to_string(),
|
||||
Format::OpenEXR => "OpenEXR".to_string(),
|
||||
Format::PNG => "PNG".to_string(),
|
||||
Format::TIFF => "TIFF".to_string(),
|
||||
Format::QuickTime => "QuickTime".to_string(),
|
||||
Format::WAV => "Wave Audio".to_string(),
|
||||
Format::AIFF => "AIFF".to_string(),
|
||||
Format::MP3 => "MP3".to_string(),
|
||||
Format::FLAC => "FLAC".to_string(),
|
||||
Format::Ogg => "Ogg".to_string(),
|
||||
Format::WebM => "WebM".to_string(),
|
||||
Format::SRT => "SubRip SRT".to_string(),
|
||||
Format::Count => "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// File extension for a format (e.g. "mp4").
|
||||
///
|
||||
/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_extension`;
|
||||
/// `Count` returns an empty string.
|
||||
pub fn get_extension(f: Format) -> String {
|
||||
match f {
|
||||
Format::DNxHD => "mxf".to_string(),
|
||||
Format::Matroska => "mkv".to_string(),
|
||||
Format::MPEG4Video => "mp4".to_string(),
|
||||
Format::MPEG4Audio => "m4a".to_string(),
|
||||
Format::OpenEXR => "exr".to_string(),
|
||||
Format::PNG => "png".to_string(),
|
||||
Format::TIFF => "tiff".to_string(),
|
||||
Format::QuickTime => "mov".to_string(),
|
||||
Format::WAV => "wav".to_string(),
|
||||
Format::AIFF => "aiff".to_string(),
|
||||
Format::MP3 => "mp3".to_string(),
|
||||
Format::FLAC => "flac".to_string(),
|
||||
Format::Ogg => "ogg".to_string(),
|
||||
Format::WebM => "webm".to_string(),
|
||||
Format::SRT => "srt".to_string(),
|
||||
Format::Count => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Codecs that can carry video in this format.
|
||||
///
|
||||
/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_video_codecs`.
|
||||
pub fn get_video_codecs(f: Format) -> Vec<Codec> {
|
||||
match f {
|
||||
Format::DNxHD => vec![Codec::DNxHD],
|
||||
Format::Matroska => vec![Codec::H264, Codec::H264RGB, Codec::H265, Codec::VP9],
|
||||
Format::MPEG4Video => vec![Codec::H264, Codec::H264RGB, Codec::H265],
|
||||
Format::OpenEXR => vec![Codec::OpenEXR],
|
||||
Format::PNG => vec![Codec::PNG],
|
||||
Format::TIFF => vec![Codec::TIFF],
|
||||
Format::QuickTime => vec![
|
||||
Codec::H264,
|
||||
Codec::H264RGB,
|
||||
Codec::H265,
|
||||
Codec::ProRes,
|
||||
Codec::CineForm,
|
||||
],
|
||||
Format::WebM => vec![Codec::AV1, Codec::VP9],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Codecs that can carry audio in this format.
|
||||
///
|
||||
/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_audio_codecs`.
|
||||
pub fn get_audio_codecs(f: Format) -> Vec<Codec> {
|
||||
match f {
|
||||
// Video/audio formats.
|
||||
Format::DNxHD => vec![Codec::PCM],
|
||||
Format::Matroska => vec![
|
||||
Codec::AAC,
|
||||
Codec::MP2,
|
||||
Codec::MP3,
|
||||
Codec::PCM,
|
||||
Codec::Vorbis,
|
||||
Codec::Opus,
|
||||
Codec::FLAC,
|
||||
],
|
||||
Format::MPEG4Video | Format::MPEG4Audio => {
|
||||
vec![Codec::AAC, Codec::MP2, Codec::MP3]
|
||||
}
|
||||
Format::QuickTime => vec![Codec::AAC, Codec::MP2, Codec::MP3, Codec::PCM],
|
||||
Format::WebM => vec![
|
||||
Codec::Opus,
|
||||
Codec::AAC,
|
||||
Codec::MP2,
|
||||
Codec::MP3,
|
||||
Codec::PCM,
|
||||
Codec::Vorbis,
|
||||
],
|
||||
// Audio-only formats.
|
||||
Format::WAV => vec![Codec::PCM],
|
||||
Format::AIFF => vec![Codec::PCM],
|
||||
Format::MP3 => vec![Codec::MP3],
|
||||
Format::FLAC => vec![Codec::FLAC],
|
||||
Format::Ogg => vec![Codec::Opus, Codec::Vorbis, Codec::PCM],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Codecs that can carry subtitles in this format.
|
||||
///
|
||||
/// CPP-PARITY: `src/codec/src/exportformat.cpp` `get_subtitle_codecs`;
|
||||
/// only Matroska and SRT support the SRT subtitle codec.
|
||||
pub fn get_subtitle_codecs(f: Format) -> Vec<Codec> {
|
||||
match f {
|
||||
Format::Matroska | Format::SRT => vec![Codec::SRT],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pixel formats the given codec supports in this format.
|
||||
///
|
||||
/// The C++ side computes this by instantiating the format's encoder and
|
||||
/// asking the FFmpeg bridge (`fb_encoder_codec_get_pixel_formats`); the
|
||||
/// base `Encoder::get_pixel_formats_for_codec` returns an empty list.
|
||||
/// There is no pure-Rust table, so this returns an empty list (matching
|
||||
/// the C++ base default). CPP-PARITY: `exportformat.cpp:222`.
|
||||
pub fn get_pixel_formats_for_codec(_f: Format, _c: Codec) -> Vec<i32> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Sample formats the given codec supports in this format.
|
||||
///
|
||||
/// CPP-PARITY: `ffmpegencoder.cpp:175`. PCM is pure (signed-16 first so
|
||||
/// the export dialog's default matches FFmpeg's); all other codecs query
|
||||
/// the FFmpeg bridge and return empty here (see
|
||||
/// `get_pixel_formats_for_codec`).
|
||||
pub fn get_sample_formats_for_codec(_f: Format, c: Codec) -> Vec<SampleFormat> {
|
||||
if c == Codec::PCM {
|
||||
vec![
|
||||
SampleFormat::S16,
|
||||
SampleFormat::U8,
|
||||
SampleFormat::S32,
|
||||
SampleFormat::S64,
|
||||
SampleFormat::F32,
|
||||
SampleFormat::F64,
|
||||
]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn format_name_mapping() {
|
||||
assert_eq!(Format::get_name(Format::DNxHD), "DNxHD");
|
||||
assert_eq!(Format::get_name(Format::Matroska), "Matroska Video");
|
||||
assert_eq!(Format::get_name(Format::MPEG4Video), "MPEG-4 Video");
|
||||
assert_eq!(Format::get_name(Format::MPEG4Audio), "MPEG-4 Audio");
|
||||
assert_eq!(Format::get_name(Format::OpenEXR), "OpenEXR");
|
||||
assert_eq!(Format::get_name(Format::PNG), "PNG");
|
||||
assert_eq!(Format::get_name(Format::TIFF), "TIFF");
|
||||
assert_eq!(Format::get_name(Format::QuickTime), "QuickTime");
|
||||
assert_eq!(Format::get_name(Format::WAV), "Wave Audio");
|
||||
assert_eq!(Format::get_name(Format::AIFF), "AIFF");
|
||||
assert_eq!(Format::get_name(Format::MP3), "MP3");
|
||||
assert_eq!(Format::get_name(Format::FLAC), "FLAC");
|
||||
assert_eq!(Format::get_name(Format::Ogg), "Ogg");
|
||||
assert_eq!(Format::get_name(Format::WebM), "WebM");
|
||||
assert_eq!(Format::get_name(Format::SRT), "SubRip SRT");
|
||||
assert_eq!(Format::get_name(Format::Count), "Unknown");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_extension_mapping() {
|
||||
let cases = [
|
||||
(Format::DNxHD, "mxf"),
|
||||
(Format::Matroska, "mkv"),
|
||||
(Format::MPEG4Video, "mp4"),
|
||||
(Format::MPEG4Audio, "m4a"),
|
||||
(Format::OpenEXR, "exr"),
|
||||
(Format::PNG, "png"),
|
||||
(Format::TIFF, "tiff"),
|
||||
(Format::QuickTime, "mov"),
|
||||
(Format::WAV, "wav"),
|
||||
(Format::AIFF, "aiff"),
|
||||
(Format::MP3, "mp3"),
|
||||
(Format::FLAC, "flac"),
|
||||
(Format::Ogg, "ogg"),
|
||||
(Format::WebM, "webm"),
|
||||
(Format::SRT, "srt"),
|
||||
];
|
||||
for (f, ext) in cases {
|
||||
assert_eq!(Format::get_extension(f), ext, "{:?}", f);
|
||||
}
|
||||
assert_eq!(Format::get_extension(Format::Count), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_i32_maps_valid_and_rejects_others() {
|
||||
for v in 0..=14 {
|
||||
assert!(Format::from_i32(v).is_some(), "format {v}");
|
||||
}
|
||||
assert!(Format::from_i32(15).is_none()); // Count
|
||||
assert!(Format::from_i32(-1).is_none());
|
||||
assert!(Format::from_i32(99).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn video_codec_tables() {
|
||||
assert_eq!(Format::get_video_codecs(Format::DNxHD), vec![Codec::DNxHD]);
|
||||
assert_eq!(
|
||||
Format::get_video_codecs(Format::Matroska),
|
||||
vec![Codec::H264, Codec::H264RGB, Codec::H265, Codec::VP9]
|
||||
);
|
||||
assert_eq!(
|
||||
Format::get_video_codecs(Format::MPEG4Video),
|
||||
vec![Codec::H264, Codec::H264RGB, Codec::H265]
|
||||
);
|
||||
assert_eq!(Format::get_video_codecs(Format::OpenEXR), vec![Codec::OpenEXR]);
|
||||
assert_eq!(Format::get_video_codecs(Format::PNG), vec![Codec::PNG]);
|
||||
assert_eq!(Format::get_video_codecs(Format::TIFF), vec![Codec::TIFF]);
|
||||
assert_eq!(
|
||||
Format::get_video_codecs(Format::QuickTime),
|
||||
vec![Codec::H264, Codec::H264RGB, Codec::H265, Codec::ProRes, Codec::CineForm]
|
||||
);
|
||||
assert_eq!(Format::get_video_codecs(Format::WebM), vec![Codec::AV1, Codec::VP9]);
|
||||
// Formats without video codecs.
|
||||
assert!(Format::get_video_codecs(Format::WAV).is_empty());
|
||||
assert!(Format::get_video_codecs(Format::MP3).is_empty());
|
||||
assert!(Format::get_video_codecs(Format::SRT).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_codec_tables() {
|
||||
assert_eq!(Format::get_audio_codecs(Format::WAV), vec![Codec::PCM]);
|
||||
assert_eq!(Format::get_audio_codecs(Format::AIFF), vec![Codec::PCM]);
|
||||
assert_eq!(Format::get_audio_codecs(Format::MP3), vec![Codec::MP3]);
|
||||
assert_eq!(Format::get_audio_codecs(Format::FLAC), vec![Codec::FLAC]);
|
||||
assert_eq!(
|
||||
Format::get_audio_codecs(Format::Ogg),
|
||||
vec![Codec::Opus, Codec::Vorbis, Codec::PCM]
|
||||
);
|
||||
assert_eq!(Format::get_audio_codecs(Format::DNxHD), vec![Codec::PCM]);
|
||||
assert!(Format::get_audio_codecs(Format::PNG).is_empty());
|
||||
assert!(Format::get_audio_codecs(Format::SRT).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subtitle_and_codec_capability_tables() {
|
||||
assert_eq!(Format::get_subtitle_codecs(Format::Matroska), vec![Codec::SRT]);
|
||||
assert_eq!(Format::get_subtitle_codecs(Format::SRT), vec![Codec::SRT]);
|
||||
assert!(Format::get_subtitle_codecs(Format::MPEG4Video).is_empty());
|
||||
|
||||
// Pixel-format query is a bridge-dependent default (empty list).
|
||||
assert!(Format::get_pixel_formats_for_codec(Format::MPEG4Video, Codec::H264).is_empty());
|
||||
|
||||
// PCM exposes its native sample formats; other codecs query the
|
||||
// FFmpeg bridge and get an empty list here.
|
||||
assert_eq!(
|
||||
Format::get_sample_formats_for_codec(Format::WAV, Codec::PCM),
|
||||
vec![
|
||||
SampleFormat::S16,
|
||||
SampleFormat::U8,
|
||||
SampleFormat::S32,
|
||||
SampleFormat::S64,
|
||||
SampleFormat::F32,
|
||||
SampleFormat::F64,
|
||||
]
|
||||
);
|
||||
assert!(Format::get_sample_formats_for_codec(Format::MPEG4Video, Codec::AAC).is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
// 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/codec/conform.h` exports.
|
||||
//!
|
||||
//! Complete inventory: create_instance / destroy_instance / get_state /
|
||||
//! filename_count / filename_at. `OAKCODEC_CONFORM_*` macros are the
|
||||
//! states.
|
||||
//!
|
||||
//! # CPP-PARITY
|
||||
//! The C++ `c_api/conform.cpp` reports `OAKCODEC_E_STATE` when the
|
||||
//! singleton is absent (`!ConformManager::instance()`); the Rust
|
||||
//! [`crate::conformmanager::ConformManager::instance`] is a lazy
|
||||
//! `'static` singleton that can never be absent, so that branch cannot
|
||||
//! trigger.
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::conformmanager::{ConformManager, ConformState};
|
||||
use crate::handle;
|
||||
|
||||
/// `OAKCODEC_CONFORM_EXISTS`.
|
||||
pub const OAKCODEC_CONFORM_EXISTS: c_int = 0;
|
||||
/// `OAKCODEC_CONFORM_GENERATING`.
|
||||
pub const OAKCODEC_CONFORM_GENERATING: c_int = 1;
|
||||
/// `OAKCODEC_CONFORM_UNAVAILABLE`.
|
||||
pub const OAKCODEC_CONFORM_UNAVAILABLE: c_int = 2;
|
||||
|
||||
/// `oakcodec_conform_create_instance`: create the singleton (always
|
||||
/// present here, so a no-op).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_conform_create_instance() -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let _ = ConformManager::instance();
|
||||
crate::error::OAKCODEC_OK
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_conform_destroy_instance`: destroy the singleton (the Rust
|
||||
/// manager is stateless, so a no-op).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_conform_destroy_instance() -> c_int {
|
||||
handle::guard_raw(|| crate::error::OAKCODEC_OK)
|
||||
}
|
||||
|
||||
/// `oakcodec_conform_get_state`: query the conform state of one audio
|
||||
/// stream.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_conform_get_state(
|
||||
cache_path: *const c_char,
|
||||
source_filename: *const c_char,
|
||||
stream_index: c_int,
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
sample_format: c_int,
|
||||
wait: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let cache = match crate::ffi::c_str(cache_path) {
|
||||
Some(c) if !c.is_empty() => c,
|
||||
_ => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
let source = match crate::ffi::c_str(source_filename) {
|
||||
Some(s) if !s.is_empty() => s,
|
||||
_ => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
let m = ConformManager::instance();
|
||||
match m.get_conform_state(
|
||||
&cache,
|
||||
&source,
|
||||
stream_index,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
sample_format,
|
||||
wait != 0,
|
||||
) {
|
||||
Ok(ConformState::Exists) => OAKCODEC_CONFORM_EXISTS,
|
||||
Ok(ConformState::Generating) => OAKCODEC_CONFORM_GENERATING,
|
||||
Ok(ConformState::Unavailable) | Err(_) => OAKCODEC_CONFORM_UNAVAILABLE,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_conform_filename_count`: number of conform files for the
|
||||
/// given stream/params; 0 on invalid arguments.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_conform_filename_count(
|
||||
cache_path: *const c_char,
|
||||
source_filename: *const c_char,
|
||||
stream_index: c_int,
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
sample_format: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let cache = match crate::ffi::c_str(cache_path) {
|
||||
Some(c) if !c.is_empty() => c,
|
||||
_ => return 0,
|
||||
};
|
||||
let source = match crate::ffi::c_str(source_filename) {
|
||||
Some(s) if !s.is_empty() => s,
|
||||
_ => return 0,
|
||||
};
|
||||
let m = ConformManager::instance();
|
||||
m.get_conform_filename_count(
|
||||
&cache,
|
||||
&source,
|
||||
stream_index,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
sample_format,
|
||||
) as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_conform_filename_at`: the `index`-th conform filename
|
||||
/// (two-stage string).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_conform_filename_at(
|
||||
cache_path: *const c_char,
|
||||
source_filename: *const c_char,
|
||||
stream_index: c_int,
|
||||
sample_rate: c_int,
|
||||
channel_layout: u64,
|
||||
sample_format: c_int,
|
||||
index: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let cache = match crate::ffi::c_str(cache_path) {
|
||||
Some(c) if !c.is_empty() => c,
|
||||
_ => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
let source = match crate::ffi::c_str(source_filename) {
|
||||
Some(s) if !s.is_empty() => s,
|
||||
_ => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
let m = ConformManager::instance();
|
||||
match m.get_conform_filename(
|
||||
&cache,
|
||||
&source,
|
||||
stream_index,
|
||||
sample_rate,
|
||||
channel_layout,
|
||||
sample_format,
|
||||
index as usize,
|
||||
) {
|
||||
Ok(f) => super::string_out(&f, buf, buf_size),
|
||||
Err(crate::error::Error::NotFound) => crate::error::OAKCODEC_E_NOT_FOUND,
|
||||
Err(_) => crate::error::OAKCODEC_E_FAILED,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conformmanager::test_util::REG_LOCK;
|
||||
use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_NOT_FOUND};
|
||||
|
||||
fn cstr(s: &str) -> std::ffi::CString {
|
||||
std::ffi::CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
fn temp_cache(name: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!("oakcodec_ffi_conform_{}_{}", name, std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
dir.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_destroy_instance_ok() {
|
||||
assert_eq!(unsafe { oakcodec_conform_create_instance() }, crate::error::OAKCODEC_OK);
|
||||
assert_eq!(unsafe { oakcodec_conform_destroy_instance() }, crate::error::OAKCODEC_OK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_state_maps_states() {
|
||||
let _g = REG_LOCK.lock().unwrap();
|
||||
// No registrar and no files -> UNAVAILABLE.
|
||||
let cache = cstr(&temp_cache("state"));
|
||||
let src = cstr("media.mp4");
|
||||
let rc = unsafe { oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0) };
|
||||
assert_eq!(rc, OAKCODEC_CONFORM_UNAVAILABLE);
|
||||
|
||||
// Invalid arguments -> E_INVALID.
|
||||
let rc = unsafe { oakcodec_conform_get_state(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0, 0) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
let empty = cstr("");
|
||||
let rc = unsafe { oakcodec_conform_get_state(empty.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
|
||||
// Write the conform files -> EXISTS.
|
||||
let m = ConformManager::instance();
|
||||
for i in 0..2 {
|
||||
let f = m
|
||||
.get_conform_filename(&temp_cache("state"), "media.mp4", 0, 48000, 0x3, 0, i)
|
||||
.unwrap();
|
||||
std::fs::write(&f, b"pcm").unwrap();
|
||||
}
|
||||
let rc = unsafe { oakcodec_conform_get_state(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0) };
|
||||
assert_eq!(rc, OAKCODEC_CONFORM_EXISTS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filename_count_and_at() {
|
||||
let cache = cstr(&temp_cache("names"));
|
||||
let src = cstr("media.mp4");
|
||||
|
||||
// Stereo -> 2 files.
|
||||
let rc = unsafe { oakcodec_conform_filename_count(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0) };
|
||||
assert_eq!(rc, 2);
|
||||
|
||||
// Invalid args -> 0 (not an error).
|
||||
let rc = unsafe { oakcodec_conform_filename_count(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0) };
|
||||
assert_eq!(rc, 0);
|
||||
|
||||
// filename_at round-trips the deterministic name.
|
||||
let mut buf = [0i8; 512];
|
||||
let rc = unsafe { oakcodec_conform_filename_at(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 0, buf.as_mut_ptr(), 512) };
|
||||
assert!(rc > 0);
|
||||
let name = crate::ffi::c_str(buf.as_ptr()).unwrap();
|
||||
assert!(name.ends_with(".0.pcm"));
|
||||
|
||||
// Out-of-range index -> E_NOT_FOUND.
|
||||
let rc = unsafe { oakcodec_conform_filename_at(cache.as_ptr(), src.as_ptr(), 0, 48000, 0x3, 0, 5, buf.as_mut_ptr(), 512) };
|
||||
assert_eq!(rc, OAKCODEC_E_NOT_FOUND);
|
||||
|
||||
// Invalid args -> E_INVALID.
|
||||
let rc = unsafe { oakcodec_conform_filename_at(std::ptr::null(), src.as_ptr(), 0, 48000, 0x3, 0, 0, buf.as_mut_ptr(), 512) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,781 @@
|
||||
// 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/codec/encoder.h` exports.
|
||||
//!
|
||||
//! Complete inventory: init / free / set_video_option / open / write_video
|
||||
//! / write_audio / write_subtitle / flush / last_error /
|
||||
//! get_desired_pixel_format / export_format_get_extension /
|
||||
//! encoding_generate_matrix.
|
||||
//!
|
||||
//! # CPP-PARITY
|
||||
//! The C++ `c_api/encoder.cpp` box holds an `olive::Encoder` plus the
|
||||
//! flattened `EncodingParams`; the Rust equivalent boxes `Mutex<EncoderBox>`
|
||||
//! and carries an extra `last_error` field because the Rust `Encoder` trait
|
||||
//! has no `get_error()` (the C++ reads the message off the encoder). The
|
||||
//! `oakcodec_encoding_params` POD below mirrors the header verbatim; note
|
||||
//! that the crate's `EncodingParams::video_pixel_format` / `audio_sample_format`
|
||||
//! are typed enums, so `to_native` converts the int fields.
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use oakcore_rs::PixelFormat;
|
||||
|
||||
use crate::encodingparams::EncodingParams;
|
||||
use crate::encoder::Encoder;
|
||||
use crate::handle::{self, CHandle};
|
||||
|
||||
/// `oakcodec_encoding_params` — flattened POD mirror of `include/codec/
|
||||
/// encoder.h` (all fields; a zeroed struct describes an all-tracks-disabled
|
||||
/// configuration). Field names and order mirror the header verbatim.
|
||||
#[allow(missing_docs)]
|
||||
#[repr(C)]
|
||||
pub struct oakcodec_encoding_params {
|
||||
pub filename: [u8; 1024],
|
||||
pub format: c_int,
|
||||
pub video_enabled: c_int,
|
||||
pub video_codec: c_int,
|
||||
pub video_width: c_int,
|
||||
pub video_height: c_int,
|
||||
pub video_time_base_num: c_int,
|
||||
pub video_time_base_den: c_int,
|
||||
pub video_pixel_format: c_int,
|
||||
pub video_interlacing: c_int,
|
||||
pub video_pixel_aspect_num: c_int,
|
||||
pub video_pixel_aspect_den: c_int,
|
||||
pub video_bit_rate: i64,
|
||||
pub video_min_bit_rate: i64,
|
||||
pub video_max_bit_rate: i64,
|
||||
pub video_buffer_size: i64,
|
||||
pub video_threads: c_int,
|
||||
pub video_pix_fmt: [u8; 64],
|
||||
pub video_is_image_sequence: c_int,
|
||||
pub video_scaling_method: c_int,
|
||||
pub audio_enabled: c_int,
|
||||
pub audio_codec: c_int,
|
||||
pub audio_sample_rate: c_int,
|
||||
pub audio_channel_layout: u64,
|
||||
pub audio_sample_format: c_int,
|
||||
pub audio_bit_rate: i64,
|
||||
pub subtitles_enabled: c_int,
|
||||
pub subtitles_codec: c_int,
|
||||
pub subtitles_are_sidecar: c_int,
|
||||
pub subtitles_sidecar_format: c_int,
|
||||
pub color_transform_output: [u8; 256],
|
||||
pub export_length_num: c_int,
|
||||
pub export_length_den: c_int,
|
||||
pub has_custom_range: c_int,
|
||||
pub custom_range_in_num: i64,
|
||||
pub custom_range_in_den: i64,
|
||||
pub custom_range_out_num: i64,
|
||||
pub custom_range_out_den: i64,
|
||||
}
|
||||
|
||||
/// Box behind an encoder handle (`EncoderBox` in `c_api/encoder.cpp`).
|
||||
struct EncoderBox {
|
||||
encoder: Option<Arc<dyn Encoder>>,
|
||||
params: EncodingParams,
|
||||
/// Per-codec video options set via `oakcodec_encoder_set_video_option`
|
||||
/// between init and open. Kept here (not in [`EncodingParams`], which
|
||||
/// is a byte-exact mirror of the C POD).
|
||||
video_opts: Vec<(String, String)>,
|
||||
open: bool,
|
||||
flushed: bool,
|
||||
/// Last error detail (the C++ reads it off the encoder's `get_error`).
|
||||
last_error: String,
|
||||
}
|
||||
|
||||
/// Convert an `OakPixelFormat` int code to a [`PixelFormat`].
|
||||
fn pixel_format_from_i32(v: c_int) -> PixelFormat {
|
||||
match v {
|
||||
0 => PixelFormat::U8,
|
||||
1 => PixelFormat::U10,
|
||||
2 => PixelFormat::U16,
|
||||
3 => PixelFormat::F16,
|
||||
4 => PixelFormat::F32,
|
||||
_ => PixelFormat::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
/// Flatten the C POD into the crate's [`EncodingParams`]
|
||||
/// (`to_native` in `c_api/encoder.cpp`).
|
||||
fn to_native(p: &oakcodec_encoding_params) -> EncodingParams {
|
||||
let mut n = EncodingParams::default();
|
||||
n.filename = p.filename;
|
||||
n.format = p.format;
|
||||
|
||||
n.video_enabled = p.video_enabled;
|
||||
n.video_codec = p.video_codec;
|
||||
n.video_width = p.video_width;
|
||||
n.video_height = p.video_height;
|
||||
n.video_time_base_num = p.video_time_base_num;
|
||||
n.video_time_base_den = p.video_time_base_den;
|
||||
n.video_pixel_format = pixel_format_from_i32(p.video_pixel_format);
|
||||
n.video_interlacing = p.video_interlacing;
|
||||
n.video_pixel_aspect_num = p.video_pixel_aspect_num;
|
||||
n.video_pixel_aspect_den = p.video_pixel_aspect_den;
|
||||
n.video_bit_rate = p.video_bit_rate;
|
||||
n.video_min_bit_rate = p.video_min_bit_rate;
|
||||
n.video_max_bit_rate = p.video_max_bit_rate;
|
||||
n.video_buffer_size = p.video_buffer_size;
|
||||
n.video_threads = p.video_threads;
|
||||
n.video_pix_fmt = p.video_pix_fmt;
|
||||
n.video_is_image_sequence = p.video_is_image_sequence;
|
||||
n.video_scaling_method = crate::encodingparams::scaling_from_i32(p.video_scaling_method);
|
||||
|
||||
n.audio_enabled = p.audio_enabled;
|
||||
n.audio_codec = p.audio_codec;
|
||||
n.audio_sample_rate = p.audio_sample_rate;
|
||||
n.audio_channel_layout = p.audio_channel_layout;
|
||||
n.audio_sample_format = crate::encodingparams::sample_format_from_i32(p.audio_sample_format);
|
||||
n.audio_bit_rate = p.audio_bit_rate;
|
||||
|
||||
n.subtitles_enabled = p.subtitles_enabled;
|
||||
n.subtitles_codec = p.subtitles_codec;
|
||||
n.subtitles_are_sidecar = p.subtitles_are_sidecar;
|
||||
n.subtitles_sidecar_format = p.subtitles_sidecar_format;
|
||||
|
||||
n.color_transform_output = p.color_transform_output;
|
||||
n.export_length_num = p.export_length_num;
|
||||
n.export_length_den = p.export_length_den;
|
||||
n.has_custom_range = p.has_custom_range;
|
||||
n.custom_range_in_num = p.custom_range_in_num;
|
||||
n.custom_range_in_den = p.custom_range_in_den;
|
||||
n.custom_range_out_num = p.custom_range_out_num;
|
||||
n.custom_range_out_den = p.custom_range_out_den;
|
||||
n
|
||||
}
|
||||
|
||||
/// `EncodingParams::is_valid` — the C++ `src/codec/src/encoder.h` checks
|
||||
/// only that at least one track is enabled.
|
||||
fn is_valid(p: &EncodingParams) -> bool {
|
||||
p.video_enabled != 0 || p.audio_enabled != 0 || p.subtitles_enabled != 0
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_init`: create an encoder for `params` (count 1);
|
||||
/// empty handle when the configuration is invalid.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_init(params: *const oakcodec_encoding_params) -> CHandle {
|
||||
handle::guard_handle(|| {
|
||||
if params.is_null() {
|
||||
return Ok(CHandle::null());
|
||||
}
|
||||
let native = to_native(unsafe { &*params });
|
||||
if !is_valid(&native) {
|
||||
return Ok(CHandle::null());
|
||||
}
|
||||
Ok(handle::make_owned(Mutex::new(EncoderBox {
|
||||
encoder: None,
|
||||
params: native,
|
||||
video_opts: Vec::new(),
|
||||
open: false,
|
||||
flushed: false,
|
||||
last_error: String::new(),
|
||||
})))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_free`: NULL/empty no-op; nulls `ctx` afterwards.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_free(encoder: *mut CHandle) {
|
||||
handle::guard_void(|| super::free_handle(encoder));
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_set_video_option`: set a per-codec video option
|
||||
/// (e.g. "crf"); only valid between init and open.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_set_video_option(
|
||||
encoder: CHandle,
|
||||
key: *const c_char,
|
||||
value: *const c_char,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b = super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let key = match crate::ffi::c_str(key) {
|
||||
Some(k) => k,
|
||||
None => return Err(crate::error::Error::Invalid),
|
||||
};
|
||||
let mut b = b.lock().unwrap();
|
||||
if b.open {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
let value = crate::ffi::c_str(value).unwrap_or_default();
|
||||
// `EncodingParams::set_video_option` replaces an existing key.
|
||||
b.video_opts.retain(|(k, _)| k != &key);
|
||||
b.video_opts.push((key, value));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_open`: create the encoder for the configured params,
|
||||
/// apply the video options and open the output.
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// The C++ `open()` calls `create_from_params` (which applies the options
|
||||
/// internally) then `open()`. The Rust trait separates `configure`, so it
|
||||
/// is invoked between the two; the box's `video_opts` (set between init
|
||||
/// and open) are stored for future wiring but not yet passed to
|
||||
/// `configure` (no trait channel carries them in the interim).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_open(encoder: CHandle) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b = super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let mut b = b.lock().unwrap();
|
||||
if b.open {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
let e = match crate::encoder::create_from_params(&b.params) {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
b.last_error = "failed to create encoder".to_string();
|
||||
return Err(crate::error::Error::Failed("failed to create encoder".to_string()));
|
||||
}
|
||||
};
|
||||
if e.configure(&b.params).is_err() {
|
||||
b.last_error = "failed to configure encoder".to_string();
|
||||
return Err(crate::error::Error::Failed("failed to configure encoder".to_string()));
|
||||
}
|
||||
if e.open().is_err() {
|
||||
b.last_error = "failed to open stream".to_string();
|
||||
return Err(crate::error::Error::Failed("failed to open stream".to_string()));
|
||||
}
|
||||
b.encoder = Some(e);
|
||||
b.open = true;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_write_video`: encode one video frame.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_write_video(encoder: CHandle, frame: CHandle) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b = super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let f = super::get_box::<Mutex<crate::frame::Frame>>(&frame)
|
||||
.ok_or(crate::error::Error::Invalid)?;
|
||||
let e = {
|
||||
let b = b.lock().unwrap();
|
||||
if !b.open || b.flushed || b.encoder.is_none() {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
b.encoder.as_ref().unwrap().clone()
|
||||
};
|
||||
let f = f.lock().unwrap();
|
||||
e.write_video(&f)
|
||||
.map_err(|_| crate::error::Error::Failed("write_video failed".to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_write_audio`: encode interleaved float audio samples.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_write_audio(
|
||||
encoder: CHandle,
|
||||
samples: *const f32,
|
||||
frame_count: c_int,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b = super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
if (samples.is_null() && frame_count > 0) || frame_count < 0 {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let (slice, enc) = {
|
||||
let b = b.lock().unwrap();
|
||||
if !b.open || b.flushed || b.encoder.is_none() {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
let channels = b.params.audio_channel_layout.count_ones();
|
||||
if channels == 0 {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
let sample_count = (frame_count as usize).wrapping_mul(channels as usize);
|
||||
let slice: &[f32] = if samples.is_null() {
|
||||
&[]
|
||||
} else {
|
||||
// SAFETY: the caller guarantees `samples` holds
|
||||
// `frame_count * channels` floats.
|
||||
unsafe { std::slice::from_raw_parts(samples, sample_count) }
|
||||
};
|
||||
let e = b.encoder.as_ref().unwrap().clone();
|
||||
(slice, e)
|
||||
};
|
||||
enc.write_audio(slice, frame_count)
|
||||
.map_err(|_| crate::error::Error::Failed("write_audio failed".to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_write_subtitle`: encode one subtitle entry.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_write_subtitle(
|
||||
encoder: CHandle,
|
||||
text: *const c_char,
|
||||
in_seconds: f64,
|
||||
out_seconds: f64,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b = super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let text = match crate::ffi::c_str(text) {
|
||||
Some(t) => t,
|
||||
None => return Err(crate::error::Error::Invalid),
|
||||
};
|
||||
let e = {
|
||||
let b = b.lock().unwrap();
|
||||
if !b.open || b.flushed || b.encoder.is_none() {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
b.encoder.as_ref().unwrap().clone()
|
||||
};
|
||||
e.write_subtitle(&text, in_seconds, out_seconds)
|
||||
.map_err(|_| crate::error::Error::Failed("write_subtitle failed".to_string()))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_flush`: flush the encoders, write the trailer and
|
||||
/// close the file. Idempotent.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_flush(encoder: CHandle) -> c_int {
|
||||
handle::guard(|| {
|
||||
let b = super::get_box::<Mutex<EncoderBox>>(&encoder).ok_or(crate::error::Error::Invalid)?;
|
||||
let mut b = b.lock().unwrap();
|
||||
if !b.open {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
if b.flushed {
|
||||
return Ok(());
|
||||
}
|
||||
let e = b.encoder.as_ref().unwrap().clone();
|
||||
// The C++ ignores the close() result; the Rust interim surfaces it.
|
||||
e.close()
|
||||
.map_err(|_| crate::error::Error::Failed("close failed".to_string()))?;
|
||||
b.flushed = true;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_last_error` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_last_error(
|
||||
encoder: CHandle,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match super::get_box::<Mutex<EncoderBox>>(&encoder) {
|
||||
Some(b) => {
|
||||
let b = b.lock().unwrap();
|
||||
super::string_out(&b.last_error, buf, buf_size)
|
||||
}
|
||||
None => super::string_out("", buf, buf_size),
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoder_get_desired_pixel_format`: the pixel format the
|
||||
/// encoder wants frames in, or -1 when unknown; `OAKCODEC_E_INVALID` for
|
||||
/// an empty/invalid encoder.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoder_get_desired_pixel_format(encoder: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let b = match super::get_box::<Mutex<EncoderBox>>(&encoder) {
|
||||
Some(b) => b,
|
||||
None => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
let e = match &b.lock().unwrap().encoder {
|
||||
Some(e) => e.clone(),
|
||||
None => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
match e.desired_pixel_format() {
|
||||
Some(p) => p as c_int,
|
||||
None => -1,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_export_format_get_extension` (two-stage); unknown formats
|
||||
/// yield the empty string.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_export_format_get_extension(
|
||||
format: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let ext = match crate::exportformat::Format::from_i32(format) {
|
||||
Some(f) => crate::exportformat::Format::get_extension(f),
|
||||
None => String::new(),
|
||||
};
|
||||
super::string_out(&ext, buf, buf_size)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_generate_matrix`: scaling matrix for a scaling
|
||||
/// method, row-major 4x4 `double` into `out_matrix[16]`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_generate_matrix(
|
||||
method: c_int,
|
||||
src_width: c_int,
|
||||
src_height: c_int,
|
||||
dst_width: c_int,
|
||||
dst_height: c_int,
|
||||
out_matrix: *mut f64,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
if out_matrix.is_null() {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let mut m = [0.0f64; 16];
|
||||
crate::encodingparams::EncodingParams::generate_matrix(
|
||||
crate::encodingparams::scaling_from_i32(method),
|
||||
src_width,
|
||||
src_height,
|
||||
dst_width,
|
||||
dst_height,
|
||||
&mut m,
|
||||
);
|
||||
// SAFETY: the caller guarantees `out_matrix` holds 16 doubles.
|
||||
unsafe { std::ptr::copy_nonoverlapping(m.as_ptr(), out_matrix, 16) };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bridge::common::oakcommon_videoparams_init_basic;
|
||||
use crate::encoder::set_test_encoders;
|
||||
use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_STATE};
|
||||
use crate::ffi::frame::{oakcodec_frame_allocate, oakcodec_frame_free, oakcodec_frame_init_with_params};
|
||||
|
||||
fn cstr(s: &str) -> std::ffi::CString {
|
||||
std::ffi::CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
fn zeroed_params() -> oakcodec_encoding_params {
|
||||
unsafe { std::mem::zeroed() }
|
||||
}
|
||||
|
||||
fn valid_params() -> oakcodec_encoding_params {
|
||||
let mut p = zeroed_params();
|
||||
p.filename = {
|
||||
let mut f = [0u8; 1024];
|
||||
let name = b"out.mp4";
|
||||
f[..name.len()].copy_from_slice(name);
|
||||
f
|
||||
};
|
||||
p.format = 2; // MPEG-4
|
||||
p.video_enabled = 1;
|
||||
p.video_codec = 3;
|
||||
p.video_width = 1920;
|
||||
p.video_height = 1080;
|
||||
p.video_time_base_num = 1;
|
||||
p.video_time_base_den = 30;
|
||||
p.video_pixel_format = 0; // U8
|
||||
p.audio_enabled = 1;
|
||||
p.audio_codec = 4;
|
||||
p.audio_sample_rate = 48000;
|
||||
p.audio_channel_layout = 0x3;
|
||||
p.audio_sample_format = 10; // f32 packed
|
||||
p
|
||||
}
|
||||
|
||||
/// Fake encoder that accepts every operation.
|
||||
struct FakeEncoder {
|
||||
id: &'static str,
|
||||
}
|
||||
|
||||
impl Encoder for FakeEncoder {
|
||||
fn id(&self) -> String {
|
||||
self.id.to_string()
|
||||
}
|
||||
fn supports_video(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn supports_audio(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn supports_subtitles(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn configure(&self, _p: &EncodingParams) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn open(&self) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn close(&self) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn write_video(&self, _frame: &crate::frame::Frame) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn write_audio(&self, _samples: &[f32], _frame_count: i32) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn write_subtitle(
|
||||
&self,
|
||||
_text: &str,
|
||||
_in_seconds: f64,
|
||||
_out_seconds: f64,
|
||||
) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn flush(&self) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn desired_pixel_format(&self) -> Option<PixelFormat> {
|
||||
Some(PixelFormat::U8)
|
||||
}
|
||||
fn desired_sample_format(&self) -> Option<oakcore_rs::SampleFormat> {
|
||||
None
|
||||
}
|
||||
fn filename(&self) -> String {
|
||||
"out.mp4".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_open_write_flush_golden() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]);
|
||||
|
||||
let p = valid_params();
|
||||
let before = handle::alive_count();
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(handle::alive_count(), before + 1);
|
||||
|
||||
// set_video_option between init and open.
|
||||
let key = cstr("crf");
|
||||
let val = cstr("18");
|
||||
let rc = unsafe { oakcodec_encoder_set_video_option(h, key.as_ptr(), val.as_ptr()) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
|
||||
let rc = unsafe { oakcodec_encoder_open(h) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
|
||||
// write_video with a real frame handle.
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(16, 16) };
|
||||
let mut fh = unsafe { oakcodec_frame_init_with_params(params) };
|
||||
assert_eq!(unsafe { oakcodec_frame_allocate(fh) }, crate::error::OAKCODEC_OK);
|
||||
let rc = unsafe { oakcodec_encoder_write_video(h, fh) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
|
||||
// write_audio: stereo interleaved floats.
|
||||
let mut samples = [0f32; 64];
|
||||
let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 32) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
|
||||
// write_subtitle.
|
||||
let text = cstr("hello");
|
||||
let rc = unsafe { oakcodec_encoder_write_subtitle(h, text.as_ptr(), 0.0, 2.5) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
|
||||
// desired pixel format from the fake.
|
||||
assert_eq!(unsafe { oakcodec_encoder_get_desired_pixel_format(h) }, 0); // U8
|
||||
|
||||
// flush is idempotent.
|
||||
let rc = unsafe { oakcodec_encoder_flush(h) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
let rc = unsafe { oakcodec_encoder_flush(h) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
|
||||
// Writes after flush -> E_STATE.
|
||||
let rc = unsafe { oakcodec_encoder_write_video(h, fh) };
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 32) };
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
let rc = unsafe { oakcodec_encoder_write_subtitle(h, text.as_ptr(), 0.0, 1.0) };
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
|
||||
unsafe { oakcodec_frame_free(&mut fh) };
|
||||
unsafe { oakcodec_encoder_free(&mut h) };
|
||||
assert!(h.is_null());
|
||||
assert_eq!(handle::alive_count(), before);
|
||||
set_test_encoders(Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_invalid_config_and_null_params() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
set_test_encoders(Vec::new());
|
||||
|
||||
// NULL params -> empty handle.
|
||||
let mut h = unsafe { oakcodec_encoder_init(std::ptr::null()) };
|
||||
assert!(h.is_null());
|
||||
|
||||
// All tracks disabled -> empty handle (is_valid).
|
||||
let p = zeroed_params();
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert!(h.is_null());
|
||||
|
||||
// Audio-only config is valid.
|
||||
let mut p = zeroed_params();
|
||||
p.format = 7; // WAV
|
||||
p.audio_enabled = 1;
|
||||
p.audio_sample_rate = 44100;
|
||||
p.audio_channel_layout = 0x4;
|
||||
p.audio_sample_format = 10;
|
||||
let before = handle::alive_count();
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(handle::alive_count(), before + 1);
|
||||
unsafe { oakcodec_encoder_free(&mut h) };
|
||||
assert_eq!(handle::alive_count(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_errors_and_state_machine() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
// Production path: an unknown export format cannot create an encoder.
|
||||
set_test_encoders(Vec::new());
|
||||
|
||||
let mut p = valid_params();
|
||||
p.format = 99; // unknown format -> create_from_params returns None
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert!(!h.is_null());
|
||||
|
||||
let rc = unsafe { oakcodec_encoder_open(h) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_E_FAILED);
|
||||
let mut err = [0i8; 128];
|
||||
unsafe { oakcodec_encoder_last_error(h, err.as_mut_ptr(), 128) };
|
||||
assert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some("failed to create encoder"));
|
||||
|
||||
// Empty handle -> E_INVALID; last_error empty.
|
||||
let empty = CHandle::null();
|
||||
assert_eq!(unsafe { oakcodec_encoder_open(empty) }, OAKCODEC_E_INVALID);
|
||||
assert_eq!(unsafe { oakcodec_encoder_set_video_option(empty, cstr("crf").as_ptr(), cstr("18").as_ptr()) }, OAKCODEC_E_INVALID);
|
||||
assert_eq!(unsafe { oakcodec_encoder_get_desired_pixel_format(empty) }, OAKCODEC_E_INVALID);
|
||||
let rc = unsafe { oakcodec_encoder_last_error(empty, err.as_mut_ptr(), 128) };
|
||||
assert_eq!(rc, 1);
|
||||
assert_eq!(crate::ffi::c_str(err.as_ptr()).as_deref(), Some(""));
|
||||
|
||||
unsafe { oakcodec_encoder_free(&mut h) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_and_argument_errors_with_fake() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]);
|
||||
|
||||
let p = valid_params();
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert!(!h.is_null());
|
||||
|
||||
// set_video_option with a NULL key -> E_INVALID.
|
||||
let rc = unsafe { oakcodec_encoder_set_video_option(h, std::ptr::null(), std::ptr::null()) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
|
||||
// Writes before open -> E_STATE.
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(4, 4) };
|
||||
let mut fh = unsafe { oakcodec_frame_init_with_params(params) };
|
||||
let rc = unsafe { oakcodec_encoder_write_video(h, fh) };
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
let rc = unsafe { oakcodec_encoder_flush(h) };
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
// Null samples with a positive count is an argument error (checked
|
||||
// before the state, matching the C++ validation order).
|
||||
let rc = unsafe { oakcodec_encoder_write_audio(h, std::ptr::null(), 8) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
// Valid args before open -> E_STATE.
|
||||
let mut pre = [0f32; 8];
|
||||
let rc = unsafe { oakcodec_encoder_write_audio(h, pre.as_ptr(), 4) };
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
|
||||
unsafe { oakcodec_frame_free(&mut fh) };
|
||||
unsafe { oakcodec_encoder_free(&mut h) };
|
||||
set_test_encoders(Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_audio_argument_validation() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]);
|
||||
|
||||
let p = valid_params();
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert_eq!(unsafe { oakcodec_encoder_open(h) }, crate::error::OAKCODEC_OK);
|
||||
|
||||
// NULL samples with a positive frame count -> E_INVALID.
|
||||
let rc = unsafe { oakcodec_encoder_write_audio(h, std::ptr::null(), 8) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
|
||||
// Negative frame count -> E_INVALID.
|
||||
let samples = [0f32; 8];
|
||||
let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), -1) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
|
||||
unsafe { oakcodec_encoder_free(&mut h) };
|
||||
set_test_encoders(Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_audio_zero_channels_is_state() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
set_test_encoders(vec![std::sync::Arc::new(FakeEncoder { id: "fake" })]);
|
||||
|
||||
// Audio enabled but empty channel layout -> E_STATE at write time.
|
||||
let mut p = zeroed_params();
|
||||
p.format = 7;
|
||||
p.audio_enabled = 1;
|
||||
p.audio_sample_rate = 44100;
|
||||
p.audio_channel_layout = 0;
|
||||
p.audio_sample_format = 10;
|
||||
let mut h = unsafe { oakcodec_encoder_init(&p) };
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(unsafe { oakcodec_encoder_open(h) }, crate::error::OAKCODEC_OK);
|
||||
let mut samples = [0f32; 8];
|
||||
let rc = unsafe { oakcodec_encoder_write_audio(h, samples.as_ptr(), 4) };
|
||||
assert_eq!(rc, OAKCODEC_E_STATE);
|
||||
|
||||
unsafe { oakcodec_encoder_free(&mut h) };
|
||||
set_test_encoders(Vec::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_format_extension_and_generate_matrix() {
|
||||
let mut buf = [0i8; 64];
|
||||
let rc = unsafe { oakcodec_export_format_get_extension(2, buf.as_mut_ptr(), 64) };
|
||||
assert_eq!(rc, 4); // "mp4" + NUL
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("mp4"));
|
||||
|
||||
// Unknown format -> empty string (size 1 for the NUL).
|
||||
let rc = unsafe { oakcodec_export_format_get_extension(99, buf.as_mut_ptr(), 64) };
|
||||
assert_eq!(rc, 1);
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some(""));
|
||||
|
||||
// Truncation rule: small buffer writes buf_size-1 chars + NUL.
|
||||
let rc = unsafe { oakcodec_export_format_get_extension(2, buf.as_mut_ptr(), 3) };
|
||||
assert_eq!(rc, 4); // required size unchanged
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("mp"));
|
||||
|
||||
// generate_matrix: Stretch (1) is the identity.
|
||||
let mut m = [9.0f64; 16];
|
||||
let rc = unsafe { oakcodec_encoding_generate_matrix(1, 1920, 1080, 1280, 720, m.as_mut_ptr()) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
assert_eq!(m[0], 1.0);
|
||||
assert_eq!(m[5], 1.0);
|
||||
assert_eq!(m[10], 1.0);
|
||||
assert_eq!(m[15], 1.0);
|
||||
|
||||
// Fit (0) with a square source into a 2:1 destination scales x.
|
||||
let mut m = [0.0f64; 16];
|
||||
let rc = unsafe { oakcodec_encoding_generate_matrix(0, 1000, 1000, 2000, 1000, m.as_mut_ptr()) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
assert!((m[0] - 0.5).abs() < 1e-9);
|
||||
assert_eq!(m[5], 1.0);
|
||||
|
||||
// NULL out_matrix -> E_INVALID.
|
||||
let rc = unsafe { oakcodec_encoding_generate_matrix(0, 1, 1, 2, 2, std::ptr::null_mut()) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
// 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/codec/format.h` exports.
|
||||
//!
|
||||
//! Complete inventory: format_count / format_name / format_extension /
|
||||
//! format_{video,audio,subtitle}_codec_{count,at} / codec_name /
|
||||
//! codec_is_still_image / codec_is_lossless / pix_fmt_{count,at,index} /
|
||||
//! sample_format_{count,at} / filename_contains_digit_placeholder /
|
||||
//! image_sequence_digit_count / filename_remove_digit_placeholder.
|
||||
//!
|
||||
//! # CPP-PARITY
|
||||
//! The C++ `c_api/format.cpp` mirrors the facade (oakengine/encoding.h)
|
||||
//! against the `olive::ExportFormat` / `olive::ExportCodec` / `olive::Encoder`
|
||||
//! statics. The Rust tables live in [`crate::exportformat`] /
|
||||
//! [`crate::exportcodec`]; the encoder pixel-format query is bridge-dependent
|
||||
//! on the C++ side and returns empty here (see
|
||||
//! `Format::get_pixel_formats_for_codec`), so `oakcodec_encoding_pix_fmt_*`
|
||||
//! report 0/`E_NOT_FOUND`/0 (the preferred-format fallback) like the C++
|
||||
//! base `Encoder` default. The filename helpers mirror the `Encoder`
|
||||
//! statics in [`crate::encoder`].
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_NOT_FOUND};
|
||||
use crate::exportcodec::Codec;
|
||||
use crate::exportformat::Format;
|
||||
use crate::handle;
|
||||
|
||||
/// `oakcodec_encoding_format_count` — `ExportFormat::k_format_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_count() -> c_int {
|
||||
handle::guard_raw(|| Format::Count as c_int)
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_name` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_name(
|
||||
format: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match Format::from_i32(format) {
|
||||
Some(f) => super::string_out(&Format::get_name(f), buf, buf_size),
|
||||
None => OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_extension` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_extension(
|
||||
format: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match Format::from_i32(format) {
|
||||
Some(f) => super::string_out(&Format::get_extension(f), buf, buf_size),
|
||||
None => OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_video_codec_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_video_codec_count(
|
||||
format: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match Format::from_i32(format) {
|
||||
Some(f) => Format::get_video_codecs(f).len() as c_int,
|
||||
None => OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_video_codec_at`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_video_codec_at(
|
||||
format: c_int,
|
||||
index: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match Format::from_i32(format) {
|
||||
Some(f) => f,
|
||||
None => return OAKCODEC_E_INVALID,
|
||||
};
|
||||
let list = Format::get_video_codecs(f);
|
||||
if index < 0 || index as usize >= list.len() {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
list[index as usize] as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_audio_codec_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_audio_codec_count(
|
||||
format: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match Format::from_i32(format) {
|
||||
Some(f) => Format::get_audio_codecs(f).len() as c_int,
|
||||
None => OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_audio_codec_at`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_audio_codec_at(
|
||||
format: c_int,
|
||||
index: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match Format::from_i32(format) {
|
||||
Some(f) => f,
|
||||
None => return OAKCODEC_E_INVALID,
|
||||
};
|
||||
let list = Format::get_audio_codecs(f);
|
||||
if index < 0 || index as usize >= list.len() {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
list[index as usize] as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_subtitle_codec_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_subtitle_codec_count(
|
||||
format: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match Format::from_i32(format) {
|
||||
Some(f) => Format::get_subtitle_codecs(f).len() as c_int,
|
||||
None => OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_format_subtitle_codec_at`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_format_subtitle_codec_at(
|
||||
format: c_int,
|
||||
index: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match Format::from_i32(format) {
|
||||
Some(f) => f,
|
||||
None => return OAKCODEC_E_INVALID,
|
||||
};
|
||||
let list = Format::get_subtitle_codecs(f);
|
||||
if index < 0 || index as usize >= list.len() {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
list[index as usize] as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_codec_name` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_codec_name(
|
||||
codec: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match Codec::from_i32(codec) {
|
||||
Some(c) => super::string_out(&Codec::get_codec_name(c), buf, buf_size),
|
||||
None => OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_codec_is_still_image` (0 for an invalid codec).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_codec_is_still_image(codec: c_int) -> c_int {
|
||||
handle::guard_raw(|| match Codec::from_i32(codec) {
|
||||
Some(c) => Codec::is_codec_a_still_image(c) as c_int,
|
||||
None => 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_codec_is_lossless` (0 for an invalid codec).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_codec_is_lossless(codec: c_int) -> c_int {
|
||||
handle::guard_raw(|| match Codec::from_i32(codec) {
|
||||
Some(c) => Codec::is_codec_lossless(c) as c_int,
|
||||
None => 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_pix_fmt_count`.
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// The C++ side instantiates the format's encoder and asks it for the codec's
|
||||
/// pixel formats; the Rust table is empty (see [`Format::get_pixel_formats_for_codec`]),
|
||||
/// so the count is 0 — the same as the C++ base `Encoder` default and the
|
||||
/// C++ result for encoder-less codecs.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_pix_fmt_count(
|
||||
format: c_int,
|
||||
codec: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
|
||||
(Some(f), Some(c)) => (f, c),
|
||||
_ => return OAKCODEC_E_INVALID,
|
||||
};
|
||||
Format::get_pixel_formats_for_codec(f, c).len() as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_pix_fmt_at` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_pix_fmt_at(
|
||||
format: c_int,
|
||||
codec: c_int,
|
||||
index: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
|
||||
(Some(f), Some(c)) => (f, c),
|
||||
_ => return OAKCODEC_E_INVALID,
|
||||
};
|
||||
let list = Format::get_pixel_formats_for_codec(f, c);
|
||||
if index < 0 || index as usize >= list.len() {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
// Interim: the Rust list carries no names yet, so this arm is
|
||||
// unreachable while the list is empty (C++ queries the FFmpeg bridge).
|
||||
super::string_out(&list[index as usize].to_string(), buf, buf_size)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_pix_fmt_index` — index of `pix_fmt` in `codec`'s
|
||||
/// supported pixel formats; 0 (the preferred format) for an invalid codec,
|
||||
/// a NULL/empty `pix_fmt`, or when not found.
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// The C++ side searches the FFmpeg encoder's list. The Rust table is empty,
|
||||
/// so every lookup falls back to 0 — the documented behavior for "absent".
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_pix_fmt_index(
|
||||
codec: c_int,
|
||||
pix_fmt: *const c_char,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
if Codec::from_i32(codec).is_none() {
|
||||
return 0;
|
||||
}
|
||||
match crate::ffi::c_str(pix_fmt) {
|
||||
Some(s) if !s.is_empty() => {
|
||||
// Interim: empty table (see module doc) -> preferred index 0.
|
||||
let _ = s;
|
||||
0
|
||||
}
|
||||
_ => 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_sample_format_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_sample_format_count(
|
||||
format: c_int,
|
||||
codec: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
|
||||
(Some(f), Some(c)) => (f, c),
|
||||
_ => return OAKCODEC_E_INVALID,
|
||||
};
|
||||
Format::get_sample_formats_for_codec(f, c).len() as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_sample_format_at` — an
|
||||
/// `olive::core::SampleFormat::Format` value.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_sample_format_at(
|
||||
format: c_int,
|
||||
codec: c_int,
|
||||
index: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let (f, c) = match (Format::from_i32(format), Codec::from_i32(codec)) {
|
||||
(Some(f), Some(c)) => (f, c),
|
||||
_ => return OAKCODEC_E_INVALID,
|
||||
};
|
||||
let list = Format::get_sample_formats_for_codec(f, c);
|
||||
if index < 0 || index as usize >= list.len() {
|
||||
return OAKCODEC_E_NOT_FOUND;
|
||||
}
|
||||
list[index as usize] as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_filename_contains_digit_placeholder` (0 for NULL).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_filename_contains_digit_placeholder(
|
||||
filename: *const c_char,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match crate::ffi::c_str(filename) {
|
||||
Some(f) => crate::encoder::filename_contains_digit_placeholder(&f) as c_int,
|
||||
None => 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_image_sequence_digit_count` (0 for NULL).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_image_sequence_digit_count(
|
||||
filename: *const c_char,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match crate::ffi::c_str(filename) {
|
||||
Some(f) => crate::encoder::image_sequence_placeholder_digit_count(&f),
|
||||
None => 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_filename_remove_digit_placeholder` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
filename: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| match crate::ffi::c_str(filename) {
|
||||
Some(f) => super::string_out(
|
||||
&crate::encoder::filename_remove_digit_placeholder(&f),
|
||||
buf,
|
||||
buf_size,
|
||||
),
|
||||
None => OAKCODEC_E_INVALID,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn cstr(s: &str) -> std::ffi::CString {
|
||||
std::ffi::CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_metadata_exports() {
|
||||
let mut buf = [0i8; 64];
|
||||
|
||||
// Count matches the 15-entry table (0..=14, Count = 15).
|
||||
assert_eq!(unsafe { oakcodec_encoding_format_count() }, 15);
|
||||
|
||||
// Matroska (1): "Matroska Video" / "mkv".
|
||||
let rc = unsafe { oakcodec_encoding_format_name(1, buf.as_mut_ptr(), 64) };
|
||||
assert_eq!(rc, 15); // "Matroska Video" (14) + NUL
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("Matroska Video"));
|
||||
let rc = unsafe { oakcodec_encoding_format_extension(1, buf.as_mut_ptr(), 64) };
|
||||
assert_eq!(rc, 4); // "mkv" + NUL
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("mkv"));
|
||||
|
||||
// Truncation rule on a two-stage getter.
|
||||
let rc = unsafe { oakcodec_encoding_format_name(1, buf.as_mut_ptr(), 4) };
|
||||
assert_eq!(rc, 15); // required size unchanged
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("Mat"));
|
||||
|
||||
// Invalid format -> E_INVALID.
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_name(-1, buf.as_mut_ptr(), 64) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_extension(15, buf.as_mut_ptr(), 64) },
|
||||
OAKCODEC_E_INVALID
|
||||
); // Count is not a real format
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_extension(99, buf.as_mut_ptr(), 64) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_codec_lists_exports() {
|
||||
// MPEG-4 video (2) carries H.264/H.264RGB/H.265.
|
||||
assert_eq!(unsafe { oakcodec_encoding_format_video_codec_count(2) }, 3);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_video_codec_at(2, 0) },
|
||||
1 // H.264
|
||||
);
|
||||
// WAV (7) has no video codecs but PCM (13) audio.
|
||||
assert_eq!(unsafe { oakcodec_encoding_format_video_codec_count(7) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_encoding_format_audio_codec_count(7) }, 1);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_audio_codec_at(7, 0) },
|
||||
13 // PCM
|
||||
);
|
||||
// SRT (13): subtitle-only, with the SRT (17) codec.
|
||||
assert_eq!(unsafe { oakcodec_encoding_format_audio_codec_count(13) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_encoding_format_subtitle_codec_count(13) }, 1);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_subtitle_codec_at(13, 0) },
|
||||
17 // SRT
|
||||
);
|
||||
|
||||
// Failure paths.
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_video_codec_count(-1) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_video_codec_at(2, -1) },
|
||||
OAKCODEC_E_NOT_FOUND
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_video_codec_at(2, 3) },
|
||||
OAKCODEC_E_NOT_FOUND
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_audio_codec_at(7, 1) },
|
||||
OAKCODEC_E_NOT_FOUND
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_format_subtitle_codec_at(13, 1) },
|
||||
OAKCODEC_E_NOT_FOUND
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_metadata_exports() {
|
||||
let mut buf = [0i8; 64];
|
||||
|
||||
let rc = unsafe { oakcodec_encoding_codec_name(1, buf.as_mut_ptr(), 64) };
|
||||
assert_eq!(rc, 6); // "H.264" (5) + NUL
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("H.264"));
|
||||
assert_eq!(unsafe { oakcodec_encoding_codec_name(-1, buf.as_mut_ptr(), 64) }, OAKCODEC_E_INVALID);
|
||||
|
||||
// Still images: PNG (5) yes, H.264 (1) no.
|
||||
assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(5) }, 1);
|
||||
assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(1) }, 0);
|
||||
// Lossless: PCM (13) yes, AAC (12) no.
|
||||
assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(13) }, 1);
|
||||
assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(12) }, 0);
|
||||
// Invalid codec -> 0 (not an error) for both flags.
|
||||
assert_eq!(unsafe { oakcodec_encoding_codec_is_still_image(99) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_encoding_codec_is_lossless(99) }, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_and_sample_format_exports() {
|
||||
let mut buf = [0i8; 64];
|
||||
|
||||
// Bad arguments -> E_INVALID.
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_pix_fmt_count(-1, 1) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_pix_fmt_count(2, 99) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
// Interim: the Rust pixel-format table is empty, so the count is 0
|
||||
// and any index is E_NOT_FOUND (see the module doc).
|
||||
assert_eq!(unsafe { oakcodec_encoding_pix_fmt_count(2, 1) }, 0);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_pix_fmt_at(-1, 1, 0, buf.as_mut_ptr(), 64) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_pix_fmt_at(2, 1, 0, buf.as_mut_ptr(), 64) },
|
||||
OAKCODEC_E_NOT_FOUND
|
||||
);
|
||||
// pix_fmt_index: absent/empty/NULL/invalid codec all yield 0.
|
||||
assert_eq!(unsafe { oakcodec_encoding_pix_fmt_index(1, cstr("yuv420p").as_ptr()) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_encoding_pix_fmt_index(1, std::ptr::null()) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_encoding_pix_fmt_index(99, cstr("yuv420p").as_ptr()) }, 0);
|
||||
|
||||
// PCM (13) in WAV (7) exposes its native sample formats.
|
||||
assert_eq!(unsafe { oakcodec_encoding_sample_format_count(7, 13) }, 6);
|
||||
// f32 packed = 10 (oakcore SampleFormat values match the C++).
|
||||
assert_eq!(unsafe { oakcodec_encoding_sample_format_at(7, 13, 4) }, 10);
|
||||
// Out-of-range index -> E_NOT_FOUND; bad args -> E_INVALID.
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_sample_format_at(7, 13, 6) },
|
||||
OAKCODEC_E_NOT_FOUND
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_sample_format_at(-1, 13, 0) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
// Non-PCM codecs query the bridge on the C++ side; empty here.
|
||||
assert_eq!(unsafe { oakcodec_encoding_sample_format_count(2, 12) }, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filename_helper_exports() {
|
||||
let mut buf = [0i8; 128];
|
||||
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_filename_contains_digit_placeholder(cstr("/tmp/out_[#####].png").as_ptr()) },
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_filename_contains_digit_placeholder(cstr("/tmp/out.png").as_ptr()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(unsafe { oakcodec_encoding_filename_contains_digit_placeholder(std::ptr::null()) }, 0);
|
||||
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_image_sequence_digit_count(cstr("/tmp/out_[#####].png").as_ptr()) },
|
||||
5
|
||||
);
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_encoding_image_sequence_digit_count(cstr("/tmp/out.png").as_ptr()) },
|
||||
0
|
||||
);
|
||||
assert_eq!(unsafe { oakcodec_encoding_image_sequence_digit_count(std::ptr::null()) }, 0);
|
||||
|
||||
let rc = unsafe {
|
||||
oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
cstr("/tmp/out_[#####].png").as_ptr(),
|
||||
buf.as_mut_ptr(),
|
||||
128,
|
||||
)
|
||||
};
|
||||
assert_eq!(rc, 13); // "/tmp/out.png" (12) + NUL
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("/tmp/out.png"));
|
||||
assert_eq!(
|
||||
unsafe {
|
||||
oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
std::ptr::null(),
|
||||
buf.as_mut_ptr(),
|
||||
128,
|
||||
)
|
||||
},
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,452 @@
|
||||
// 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/codec/frame.h` exports.
|
||||
//!
|
||||
//! Complete inventory: frame_init / init_with_params / free / get_params /
|
||||
//! set_params / allocate / is_allocated / data / const_data /
|
||||
//! allocated_size / linesize_bytes / linesize_pixels / width / height /
|
||||
//! format / channel_count / get_timestamp / set_timestamp /
|
||||
//! debug_alive_count.
|
||||
//!
|
||||
//! # CPP-PARITY
|
||||
//! The C++ `c_api/frame.cpp` boxes every `OakFrame` handle with an
|
||||
//! `olive::FramePtr` (a shared pointer), so decoder-produced frames may
|
||||
//! alias the decoder's internal cache. The Rust equivalent boxes
|
||||
//! `Mutex<Frame>`; a decode that hands out a still-shared `Arc<Frame>`
|
||||
//! therefore cannot be aliased here and reports an empty handle instead
|
||||
//! (see `ffi::decoder`).
|
||||
|
||||
use std::ffi::{c_int, c_void};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use oakcore_rs::Rational;
|
||||
|
||||
use crate::bridge::common::OakVideoParams;
|
||||
use crate::frame::Frame;
|
||||
use crate::handle::{self, CHandle};
|
||||
|
||||
/// `OAKCOMMON_PIXEL_FORMAT_INVALID` (oakcommon `common/videoparams.h`).
|
||||
const OAKCOMMON_PIXEL_FORMAT_INVALID: c_int = -1;
|
||||
|
||||
/// `oakcodec_frame_init`: new frame with default (invalid) params,
|
||||
/// refcount 1.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_init() -> CHandle {
|
||||
handle::guard_handle(|| Ok(handle::make_owned(Mutex::new(Frame::new()))))
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_init_with_params`: new frame holding a copy of `params`
|
||||
/// (the handle is addref'd internally); buffer unallocated.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_init_with_params(params: OakVideoParams) -> CHandle {
|
||||
handle::guard_handle(|| {
|
||||
Ok(handle::make_owned(Mutex::new(Frame::with_params(params))))
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_free`: NULL/empty no-op; nulls `ctx` afterwards.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_free(frame: *mut CHandle) {
|
||||
handle::guard_void(|| super::free_handle(frame));
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_get_params`: copy of the frame's parameter set.
|
||||
///
|
||||
/// The copy is addref'd: the caller must release it with
|
||||
/// `oakcommon_videoparams_free` (see the header contract). Test-stub
|
||||
/// handles carry no `addref`, so the caller must not free them.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_get_params(
|
||||
frame: CHandle,
|
||||
out: *mut OakVideoParams,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
if out.is_null() {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
|
||||
let f = f.lock().unwrap();
|
||||
let p = f.params().cloned().ok_or(crate::error::Error::Invalid)?;
|
||||
crate::frame::params_addref(&p);
|
||||
// SAFETY: the caller guarantees `out` points to a writable
|
||||
// `OakVideoParams`.
|
||||
unsafe { *out = p };
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_set_params`: replace the parameter set (addref'd
|
||||
/// internally); recomputes line sizes, does not reallocate.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_set_params(
|
||||
frame: CHandle,
|
||||
params: OakVideoParams,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
|
||||
f.lock().unwrap().set_params(params);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_allocate`: allocate the pixel buffer from the current
|
||||
/// params; `OAKCODEC_E_STATE` when the params are invalid.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_allocate(frame: CHandle) -> c_int {
|
||||
handle::guard(|| {
|
||||
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
|
||||
f.lock().unwrap().allocate()
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_is_allocated`: 1 when the buffer is allocated.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_is_allocated(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return 0,
|
||||
};
|
||||
let f = f.lock().unwrap();
|
||||
if f.is_allocated() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_data`: writable pixel buffer, NULL when
|
||||
/// unallocated/empty.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_data(frame: CHandle) -> *mut c_void {
|
||||
match catch_unwind(AssertUnwindSafe(|| unsafe { frame_data_inner(&frame) })) {
|
||||
Ok(p) => p,
|
||||
Err(_) => std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_const_data`: const variant of `oakcodec_frame_data`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_const_data(frame: CHandle) -> *const c_void {
|
||||
match catch_unwind(AssertUnwindSafe(|| unsafe { frame_const_data_inner(&frame) })) {
|
||||
Ok(p) => p,
|
||||
Err(_) => std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn frame_data_inner(frame: &CHandle) -> *mut c_void {
|
||||
let f = match super::get_box::<Mutex<Frame>>(frame) {
|
||||
Some(f) => f,
|
||||
None => return std::ptr::null_mut(),
|
||||
};
|
||||
match f.lock().unwrap().data_mut() {
|
||||
Some(d) => d.as_mut_ptr() as *mut c_void,
|
||||
None => std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn frame_const_data_inner(frame: &CHandle) -> *const c_void {
|
||||
let f = match super::get_box::<Mutex<Frame>>(frame) {
|
||||
Some(f) => f,
|
||||
None => return std::ptr::null(),
|
||||
};
|
||||
match f.lock().unwrap().data() {
|
||||
Some(d) => d.as_ptr() as *const c_void,
|
||||
None => std::ptr::null(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_allocated_size`: size of the pixel buffer in bytes
|
||||
/// (0 when unallocated).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_allocated_size(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return 0,
|
||||
};
|
||||
f.lock().unwrap().allocated_size() as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_linesize_bytes`: distance between two rows in bytes
|
||||
/// (0 when params are unset).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_linesize_bytes(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return 0,
|
||||
};
|
||||
f.lock().unwrap().linesize_bytes()
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_linesize_pixels`: distance between two rows in pixels.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_linesize_pixels(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return 0,
|
||||
};
|
||||
f.lock().unwrap().linesize_pixels()
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_width`: frame width (0 when params are empty).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_width(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return 0,
|
||||
};
|
||||
f.lock().unwrap().width()
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_height`: frame height (0 when params are empty).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_height(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return 0,
|
||||
};
|
||||
f.lock().unwrap().height()
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_format`: pixel format as an `OakPixelFormat` value;
|
||||
/// `OAKCOMMON_PIXEL_FORMAT_INVALID` on an empty handle.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_format(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return OAKCOMMON_PIXEL_FORMAT_INVALID,
|
||||
};
|
||||
f.lock().unwrap().format() as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_channel_count`: plane channel count of the params
|
||||
/// format (0 on an empty handle).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_channel_count(frame: CHandle) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match super::get_box::<Mutex<Frame>>(&frame) {
|
||||
Some(f) => f,
|
||||
None => return 0,
|
||||
};
|
||||
f.lock().unwrap().channel_count()
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_get_timestamp`: frame timestamp as a rational number
|
||||
/// of seconds, written through `numerator`/`denominator`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_get_timestamp(
|
||||
frame: CHandle,
|
||||
numerator: *mut c_int,
|
||||
denominator: *mut c_int,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
if numerator.is_null() || denominator.is_null() {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
|
||||
let f = f.lock().unwrap();
|
||||
let ts = f.timestamp();
|
||||
// SAFETY: both pointers were range-checked above.
|
||||
unsafe {
|
||||
*numerator = ts.numerator() as c_int;
|
||||
*denominator = ts.denominator() as c_int;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_frame_set_timestamp`: replace the frame timestamp.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_frame_set_timestamp(
|
||||
frame: CHandle,
|
||||
numerator: c_int,
|
||||
denominator: c_int,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
let f = super::get_box::<Mutex<Frame>>(&frame).ok_or(crate::error::Error::Invalid)?;
|
||||
f.lock()
|
||||
.unwrap()
|
||||
.set_timestamp(Rational::new(numerator as i64, denominator as i64));
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_debug_alive_count`: number of live boxed handle objects
|
||||
/// across all families (see `crate::handle::alive_count`).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_debug_alive_count() -> c_int {
|
||||
handle::guard_raw(handle::alive_count)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bridge::common::{
|
||||
oakcommon_videoparams_get_height, oakcommon_videoparams_get_width,
|
||||
oakcommon_videoparams_init_basic,
|
||||
};
|
||||
use crate::error::OAKCODEC_E_INVALID;
|
||||
|
||||
#[test]
|
||||
fn frame_lifecycle_golden() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(100, 50) };
|
||||
let before = handle::alive_count();
|
||||
let mut h = unsafe { oakcodec_frame_init_with_params(params) };
|
||||
assert!(!h.is_null());
|
||||
// init -> exactly one more live box.
|
||||
assert_eq!(handle::alive_count(), before + 1);
|
||||
|
||||
// get_params round-trips width/height through the stub.
|
||||
let mut out = empty_params();
|
||||
let rc = unsafe { oakcodec_frame_get_params(h, &mut out) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
// NOTE: stub handles carry no addref and `oakcommon_videoparams_free`
|
||||
// would drop the shared box; the test keeps the copy alive for the
|
||||
// frame's lifetime and does not free it.
|
||||
assert_eq!(unsafe { oakcommon_videoparams_get_width(out.clone()) }, 100);
|
||||
assert_eq!(unsafe { oakcommon_videoparams_get_height(out.clone()) }, 50);
|
||||
|
||||
assert_eq!(unsafe { oakcodec_frame_width(h) }, 100);
|
||||
assert_eq!(unsafe { oakcodec_frame_height(h) }, 50);
|
||||
assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_data(h) }, std::ptr::null_mut());
|
||||
|
||||
let rc = unsafe { oakcodec_frame_allocate(h) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 1);
|
||||
assert!(!unsafe { oakcodec_frame_data(h) }.is_null());
|
||||
// U8 RGBA: 100px -> 4*128 bytes linesize.
|
||||
assert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 4 * 128);
|
||||
assert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, (4 * 128) * 50);
|
||||
|
||||
// set_timestamp round-trip.
|
||||
assert_eq!(unsafe { oakcodec_frame_set_timestamp(h, 1, 30) }, crate::error::OAKCODEC_OK);
|
||||
let (mut num, mut den) = (0, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_get_timestamp(h, &mut num, &mut den) }, crate::error::OAKCODEC_OK);
|
||||
assert_eq!((num, den), (1, 30));
|
||||
|
||||
unsafe { oakcodec_frame_free(&mut h) };
|
||||
assert!(h.is_null());
|
||||
assert_eq!(handle::alive_count(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_errors_and_empty_handles() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let empty = CHandle::null();
|
||||
assert_eq!(unsafe { oakcodec_frame_width(empty) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_format(empty) }, OAKCOMMON_PIXEL_FORMAT_INVALID);
|
||||
assert_eq!(unsafe { oakcodec_frame_allocate(empty) }, OAKCODEC_E_INVALID);
|
||||
assert_eq!(unsafe { oakcodec_frame_get_params(empty, std::ptr::null_mut()) }, OAKCODEC_E_INVALID);
|
||||
|
||||
// init_basic(0, 0) is not valid -> allocate rejects with E_STATE.
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(0, 0) };
|
||||
let mut h = unsafe { oakcodec_frame_init_with_params(params) };
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(unsafe { oakcodec_frame_allocate(h) }, crate::error::OAKCODEC_E_STATE);
|
||||
assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);
|
||||
unsafe { oakcodec_frame_free(&mut h) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_null_and_empty_are_noops() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let before = handle::alive_count();
|
||||
unsafe { oakcodec_frame_free(std::ptr::null_mut()) };
|
||||
let mut empty = CHandle::null();
|
||||
unsafe { oakcodec_frame_free(&mut empty) };
|
||||
assert!(empty.is_null());
|
||||
assert_eq!(handle::alive_count(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_set_params_and_query_helpers() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let before = handle::alive_count();
|
||||
|
||||
// Bare init (no params): invalid params, not allocated. The stub's
|
||||
// default MockParams carries format 0 (U8); the empty-handle -1
|
||||
// case is covered in `frame_errors_and_empty_handles`.
|
||||
let mut h = unsafe { oakcodec_frame_init() };
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(handle::alive_count(), before + 1);
|
||||
assert_eq!(unsafe { oakcodec_frame_width(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_height(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_channel_count(h) }, 4);
|
||||
assert_eq!(unsafe { oakcodec_frame_is_allocated(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_linesize_pixels(h) }, 0);
|
||||
assert_eq!(unsafe { oakcodec_frame_data(h) }, std::ptr::null_mut());
|
||||
assert_eq!(unsafe { oakcodec_frame_const_data(h) }, std::ptr::null());
|
||||
assert_eq!(unsafe { oakcodec_frame_allocate(h) }, crate::error::OAKCODEC_E_STATE);
|
||||
|
||||
// set_params replaces the parameter set and recomputes line sizes.
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(100, 50) };
|
||||
let rc = unsafe { oakcodec_frame_set_params(h, params) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
assert_eq!(unsafe { oakcodec_frame_width(h) }, 100);
|
||||
assert_eq!(unsafe { oakcodec_frame_height(h) }, 50);
|
||||
assert_eq!(unsafe { oakcodec_frame_format(h) }, 0); // U8
|
||||
assert_eq!(unsafe { oakcodec_frame_channel_count(h) }, 4);
|
||||
assert_eq!(unsafe { oakcodec_frame_linesize_bytes(h) }, 4 * 128);
|
||||
assert_eq!(unsafe { oakcodec_frame_linesize_pixels(h) }, 128);
|
||||
|
||||
// allocate -> data and const_data point at the buffer.
|
||||
assert_eq!(unsafe { oakcodec_frame_allocate(h) }, crate::error::OAKCODEC_OK);
|
||||
assert!(!unsafe { oakcodec_frame_data(h) }.is_null());
|
||||
assert!(!unsafe { oakcodec_frame_const_data(h) }.is_null());
|
||||
assert_eq!(unsafe { oakcodec_frame_allocated_size(h) }, (4 * 128) * 50);
|
||||
|
||||
// get_timestamp rejects NULL out pointers.
|
||||
assert_eq!(
|
||||
unsafe { oakcodec_frame_get_timestamp(h, std::ptr::null_mut(), std::ptr::null_mut()) },
|
||||
OAKCODEC_E_INVALID
|
||||
);
|
||||
|
||||
// debug_alive_count reports the live boxes (>= our own).
|
||||
assert!(unsafe { oakcodec_debug_alive_count() } >= before + 1);
|
||||
|
||||
unsafe { oakcodec_frame_free(&mut h) };
|
||||
assert_eq!(handle::alive_count(), before);
|
||||
}
|
||||
|
||||
fn empty_params() -> OakVideoParams {
|
||||
OakVideoParams {
|
||||
ctx: std::ptr::null_mut(),
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// 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/>.
|
||||
|
||||
//! C ABI export layer: implements `include/codec/*.h` verbatim.
|
||||
//!
|
||||
//! Organization: one submodule per public header (frame / decoder /
|
||||
//! encoder / format / conform / proxy / task). The authoritative function
|
||||
//! list is the header itself; each export only unwraps handles, calls the
|
||||
//! safe Rust domains, and maps results through [`crate::handle::guard*`].
|
||||
//! `include/codec/error.h` exports macros only, so it is folded into the
|
||||
//! preamble below instead of getting its own submodule.
|
||||
//!
|
||||
//! Shared helpers live here: the two-stage string convention
|
||||
//! ([`string_out`]), C-string decoding ([`c_str`]) and in-place handle
|
||||
//! release ([`free_handle`]) — all mirroring the `c_api/*.cpp` helpers.
|
||||
|
||||
/// `include/codec/error.h` — macros only, no exported functions.
|
||||
///
|
||||
/// `OAKCODEC_OK` and the `OAKCODEC_E_*` codes are mirrored as
|
||||
/// [`crate::error`] constants; `OAKCODEC_ABI_VERSION` lives in
|
||||
/// [`crate::handle`].
|
||||
|
||||
pub mod conform;
|
||||
pub mod decoder;
|
||||
pub mod encoder;
|
||||
pub mod format;
|
||||
pub mod frame;
|
||||
pub mod proxy;
|
||||
pub mod task;
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
#[cfg(test)]
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// Serializes every ffi unit test: they share the global handle ALIVE
|
||||
/// counter, the injected decoder/encoder registries and the probe error,
|
||||
/// so exact `alive_count` assertions and registry injection require
|
||||
/// serial execution. Held poison-tolerant (`into_inner`) so one failing
|
||||
/// test cannot cascade-fail the rest.
|
||||
#[cfg(test)]
|
||||
pub(crate) static TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Poison-tolerant lock helper for the ffi tests.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn lock_tests() -> std::sync::MutexGuard<'static, ()> {
|
||||
TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// Two-stage string copy helper (`string_out` in every `c_api/*.cpp`).
|
||||
///
|
||||
/// Returns the required buffer size including the trailing NUL; when
|
||||
/// `buf` is non-NULL and `buf_size > 0` the string is copied truncated to
|
||||
/// `buf_size - 1` bytes and NUL-terminated.
|
||||
pub(crate) fn string_out(s: &str, buf: *mut c_char, buf_size: c_int) -> c_int {
|
||||
let need = s.len() as c_int + 1;
|
||||
if !buf.is_null() && buf_size > 0 {
|
||||
let n = (s.len() as c_int).min(buf_size - 1);
|
||||
// SAFETY: the caller guarantees `buf` holds `buf_size` bytes.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(s.as_ptr() as *const c_char, buf, n as usize);
|
||||
*buf.add(n as usize) = 0;
|
||||
}
|
||||
}
|
||||
need
|
||||
}
|
||||
|
||||
/// Read a NUL-terminated C string; `None` on NULL pointers.
|
||||
pub(crate) fn c_str(ptr: *const c_char) -> Option<String> {
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: `ptr` must be a valid NUL-terminated C string by contract.
|
||||
let s = unsafe { std::ffi::CStr::from_ptr(ptr) };
|
||||
Some(s.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
/// Release a handle in place and null its `ctx` (`free_handle` in
|
||||
/// `c_api/refcounted.h`); NULL pointer and empty handle are no-ops.
|
||||
pub(crate) fn free_handle(h: *mut CHandle) {
|
||||
if h.is_null() {
|
||||
return;
|
||||
}
|
||||
let handle = unsafe { &mut *h };
|
||||
if handle.ctx.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Some(release) = handle.release {
|
||||
// SAFETY: `release` targets the box behind `ctx`.
|
||||
unsafe { release(handle.ctx) };
|
||||
}
|
||||
handle.ctx = std::ptr::null_mut();
|
||||
}
|
||||
|
||||
/// Safe view into a handle's boxed value; `None` for empty handles.
|
||||
///
|
||||
/// Thin wrapper over [`crate::handle::get`] so the export bodies can call
|
||||
/// it without `unsafe` blocks everywhere.
|
||||
///
|
||||
/// # Safety
|
||||
/// `T` must be the boxed type; each export asserts it via the handle
|
||||
/// contract (the same typed box is used by its `make_owned` call).
|
||||
pub(crate) fn get_box<T: 'static>(h: &CHandle) -> Option<&T> {
|
||||
unsafe { crate::handle::get::<T>(h) }
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
// 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/codec/proxy.h` exports.
|
||||
//!
|
||||
//! Complete inventory: create_instance / destroy_instance / params_default
|
||||
//! / get_state / state_to_string / get_proxy_directory / get_proxy_filename
|
||||
//! / get_working_filename / get_or_start / find_ffmpeg.
|
||||
//!
|
||||
//! # CPP-PARITY
|
||||
//! The C++ `oakcodec_proxy_params.include_audio` is an `int`, mirrored
|
||||
//! byte-for-byte by [`crate::proxymanager::ProxyParams`]; the POD structs
|
||||
//! are still defined here (rather than reused) so the ffi layer never
|
||||
//! depends on the crate-internal type's layout. A NULL `params` maps to
|
||||
//! the crate's compiled-in defaults (`ProxyParams::default`), matching
|
||||
//! `to_native` in `c_api/proxy.cpp`.
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::handle;
|
||||
use crate::proxymanager::{ProxyManager, ProxyParams, ProxyState};
|
||||
|
||||
/// `OAKCODEC_PROXY_STATE_MISSING`.
|
||||
pub const OAKCODEC_PROXY_STATE_MISSING: c_int = 0;
|
||||
/// `OAKCODEC_PROXY_STATE_GENERATING`.
|
||||
pub const OAKCODEC_PROXY_STATE_GENERATING: c_int = 1;
|
||||
/// `OAKCODEC_PROXY_STATE_READY`.
|
||||
pub const OAKCODEC_PROXY_STATE_READY: c_int = 2;
|
||||
/// `OAKCODEC_PROXY_STATE_FAILED`.
|
||||
pub const OAKCODEC_PROXY_STATE_FAILED: c_int = 3;
|
||||
|
||||
/// `oakcodec_proxy_params` — POD mirror of `include/codec/proxy.h`.
|
||||
#[allow(missing_docs)]
|
||||
#[repr(C)]
|
||||
pub struct oakcodec_proxy_params {
|
||||
pub width: c_int,
|
||||
pub height: c_int,
|
||||
pub divider: c_int,
|
||||
pub version: c_int,
|
||||
pub crf: c_int,
|
||||
pub include_audio: c_int,
|
||||
pub extension: [u8; 32],
|
||||
pub preset: [u8; 32],
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_result` — POD result of `oakcodec_proxy_get_or_start`.
|
||||
#[allow(missing_docs)]
|
||||
#[repr(C)]
|
||||
pub struct oakcodec_proxy_result {
|
||||
pub state: c_int,
|
||||
pub filename: [u8; 1024],
|
||||
}
|
||||
|
||||
/// Convert the C POD to the crate's [`ProxyParams`] (`to_native` in
|
||||
/// `c_api/proxy.cpp`); NULL maps to the compiled-in defaults.
|
||||
fn to_native(p: *const oakcodec_proxy_params) -> ProxyParams {
|
||||
if p.is_null() {
|
||||
return ProxyParams::default();
|
||||
}
|
||||
let p = unsafe { &*p };
|
||||
ProxyParams {
|
||||
width: p.width,
|
||||
height: p.height,
|
||||
divider: p.divider,
|
||||
version: p.version,
|
||||
crf: p.crf,
|
||||
include_audio: p.include_audio,
|
||||
extension: p.extension,
|
||||
preset: p.preset,
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy a NUL-terminated byte array into a C buffer (truncated).
|
||||
fn copy_cstr(src: &[u8], dst: &mut [u8]) {
|
||||
dst.fill(0);
|
||||
let n = src.len().min(dst.len().saturating_sub(1));
|
||||
dst[..n].copy_from_slice(&src[..n]);
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_create_instance`: create the singleton (always present
|
||||
/// here, so a no-op).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_create_instance() -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let _ = ProxyManager::instance();
|
||||
crate::error::OAKCODEC_OK
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_destroy_instance`: destroy the singleton (the Rust
|
||||
/// manager is stateless, so a no-op).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_destroy_instance() -> c_int {
|
||||
handle::guard_raw(|| crate::error::OAKCODEC_OK)
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_params_default`: fill `out` with the compiled-in
|
||||
/// default proxy parameters.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_params_default(out: *mut oakcodec_proxy_params) -> c_int {
|
||||
handle::guard(|| {
|
||||
if out.is_null() {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let n = ProxyManager::proxy_params_from_config();
|
||||
unsafe {
|
||||
(*out).width = n.width;
|
||||
(*out).height = n.height;
|
||||
(*out).divider = n.divider;
|
||||
(*out).version = n.version;
|
||||
(*out).crf = n.crf;
|
||||
(*out).include_audio = n.include_audio;
|
||||
copy_cstr(&n.extension, &mut (*out).extension);
|
||||
copy_cstr(&n.preset, &mut (*out).preset);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_get_state`: state of a proxy file on disk
|
||||
/// (`OAKCODEC_PROXY_STATE_MISSING` for NULL/empty/absent).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_get_state(proxy_filename: *const c_char) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let f = match crate::ffi::c_str(proxy_filename) {
|
||||
Some(f) if !f.is_empty() => f,
|
||||
_ => return OAKCODEC_PROXY_STATE_MISSING,
|
||||
};
|
||||
ProxyManager::get_proxy_state(&f) as c_int
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_state_to_string` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_state_to_string(
|
||||
state: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let s = match state {
|
||||
0 => ProxyManager::proxy_state_to_string(ProxyState::Missing),
|
||||
1 => ProxyManager::proxy_state_to_string(ProxyState::Generating),
|
||||
2 => ProxyManager::proxy_state_to_string(ProxyState::Ready),
|
||||
3 => ProxyManager::proxy_state_to_string(ProxyState::Failed),
|
||||
_ => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
super::string_out(&s, buf, buf_size)
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_get_proxy_directory` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_get_proxy_directory(
|
||||
cache_path: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let cache = match crate::ffi::c_str(cache_path) {
|
||||
Some(c) => c,
|
||||
None => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
match ProxyManager::get_proxy_directory(&cache) {
|
||||
Ok(s) => super::string_out(&s, buf, buf_size),
|
||||
Err(_) => crate::error::OAKCODEC_E_FAILED,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_get_proxy_filename` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_get_proxy_filename(
|
||||
cache_path: *const c_char,
|
||||
source_filename: *const c_char,
|
||||
stream_index: c_int,
|
||||
params: *const oakcodec_proxy_params,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let cache = match crate::ffi::c_str(cache_path) {
|
||||
Some(c) => c,
|
||||
None => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
let source = match crate::ffi::c_str(source_filename) {
|
||||
Some(s) => s,
|
||||
None => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
let native = to_native(params);
|
||||
match ProxyManager::get_proxy_filename(&cache, &source, stream_index, &native) {
|
||||
Ok(s) => super::string_out(&s, buf, buf_size),
|
||||
Err(_) => crate::error::OAKCODEC_E_FAILED,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_get_working_filename` (two-stage).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_get_working_filename(
|
||||
proxy_filename: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let proxy = match crate::ffi::c_str(proxy_filename) {
|
||||
Some(p) => p,
|
||||
None => return crate::error::OAKCODEC_E_INVALID,
|
||||
};
|
||||
match ProxyManager::get_working_filename(&proxy) {
|
||||
Ok(s) => super::string_out(&s, buf, buf_size),
|
||||
Err(_) => crate::error::OAKCODEC_E_FAILED,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_get_or_start`: get or start generating a proxy for
|
||||
/// `source_filename`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_get_or_start(
|
||||
cache_path: *const c_char,
|
||||
source_filename: *const c_char,
|
||||
stream_index: c_int,
|
||||
params: *const oakcodec_proxy_params,
|
||||
out: *mut oakcodec_proxy_result,
|
||||
) -> c_int {
|
||||
handle::guard(|| {
|
||||
if out.is_null() {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let cache = match crate::ffi::c_str(cache_path) {
|
||||
Some(c) => c,
|
||||
None => return Err(crate::error::Error::Invalid),
|
||||
};
|
||||
let source = match crate::ffi::c_str(source_filename) {
|
||||
Some(s) => s,
|
||||
None => return Err(crate::error::Error::Invalid),
|
||||
};
|
||||
let native = to_native(params);
|
||||
let (state, filename) = ProxyManager::instance()
|
||||
.get_or_start(&cache, &source, stream_index, &native)
|
||||
.map_err(|_| crate::error::Error::Failed("get_or_start failed".to_string()))?;
|
||||
unsafe {
|
||||
let out_ref = &mut *out;
|
||||
out_ref.state = state as c_int;
|
||||
// Truncate to 1023 chars + NUL, matching `snprintf` in
|
||||
// `c_api/proxy.cpp`.
|
||||
let n = filename.len().min(1023);
|
||||
out_ref.filename[..n].copy_from_slice(&filename.as_bytes()[..n]);
|
||||
out_ref.filename[n] = 0;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_find_ffmpeg` (two-stage; empty string when none found).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_proxy_find_ffmpeg(
|
||||
configured_path: *const c_char,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
let configured = crate::ffi::c_str(configured_path).unwrap_or_default();
|
||||
let s = ProxyManager::find_ffmpeg(&configured);
|
||||
super::string_out(&s, buf, buf_size)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conformmanager::test_util::{accept_cb, REG_LOCK};
|
||||
use crate::error::{OAKCODEC_E_INVALID, OAKCODEC_E_STATE};
|
||||
use crate::task::set_task_submit_cb_extern;
|
||||
|
||||
fn cstr(s: &str) -> std::ffi::CString {
|
||||
std::ffi::CString::new(s).unwrap()
|
||||
}
|
||||
|
||||
fn temp_cache(name: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!("oakcodec_ffi_proxy_{}_{}", name, std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
dir.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
fn defaults() -> oakcodec_proxy_params {
|
||||
let mut p: oakcodec_proxy_params = unsafe { std::mem::zeroed() };
|
||||
let rc = unsafe { oakcodec_proxy_params_default(&mut p) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_destroy_and_params_default() {
|
||||
assert_eq!(unsafe { oakcodec_proxy_create_instance() }, crate::error::OAKCODEC_OK);
|
||||
assert_eq!(unsafe { oakcodec_proxy_destroy_instance() }, crate::error::OAKCODEC_OK);
|
||||
|
||||
let p = defaults();
|
||||
assert_eq!(p.width, 1280);
|
||||
assert_eq!(p.height, 720);
|
||||
assert_eq!(p.divider, 1);
|
||||
assert_eq!(p.version, 1);
|
||||
assert_eq!(p.crf, 23);
|
||||
assert_eq!(p.include_audio, 1);
|
||||
assert_eq!(&p.extension[..3], b"mp4");
|
||||
assert_eq!(&p.preset[..8], b"veryfast");
|
||||
|
||||
let rc = unsafe { oakcodec_proxy_params_default(std::ptr::null_mut()) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_state_and_state_to_string() {
|
||||
let cache = temp_cache("state");
|
||||
let p = defaults();
|
||||
let src = cstr("media.mp4");
|
||||
let cache_c = cstr(&cache);
|
||||
|
||||
// Resolve the proxy filename, then query its state.
|
||||
let mut name = [0i8; 1024];
|
||||
let rc = unsafe { oakcodec_proxy_get_proxy_filename(cache_c.as_ptr(), src.as_ptr(), 0, &p, name.as_mut_ptr(), 1024) };
|
||||
assert!(rc > 0);
|
||||
let proxy = crate::ffi::c_str(name.as_ptr()).unwrap();
|
||||
assert!(proxy.contains("1280x720"));
|
||||
|
||||
// Missing by default.
|
||||
let pc = cstr(&proxy);
|
||||
assert_eq!(unsafe { oakcodec_proxy_get_state(pc.as_ptr()) }, OAKCODEC_PROXY_STATE_MISSING);
|
||||
assert_eq!(unsafe { oakcodec_proxy_get_state(std::ptr::null()) }, OAKCODEC_PROXY_STATE_MISSING);
|
||||
|
||||
// Ready once the file exists.
|
||||
std::fs::create_dir_all(std::path::Path::new(&proxy).parent().unwrap()).unwrap();
|
||||
std::fs::write(&proxy, b"x").unwrap();
|
||||
assert_eq!(unsafe { oakcodec_proxy_get_state(pc.as_ptr()) }, OAKCODEC_PROXY_STATE_READY);
|
||||
|
||||
// state_to_string mapping + invalid range.
|
||||
let mut buf = [0i8; 64];
|
||||
let rc = unsafe { oakcodec_proxy_state_to_string(2, buf.as_mut_ptr(), 64) };
|
||||
assert_eq!(rc, 6); // "ready" + NUL
|
||||
assert_eq!(crate::ffi::c_str(buf.as_ptr()).as_deref(), Some("ready"));
|
||||
let rc = unsafe { oakcodec_proxy_state_to_string(7, buf.as_mut_ptr(), 64) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn directory_working_and_get_or_start() {
|
||||
let cache = temp_cache("getorstart");
|
||||
let cache_c = cstr(&cache);
|
||||
let src = cstr("media.mp4");
|
||||
let p = defaults();
|
||||
|
||||
// get_proxy_directory.
|
||||
let mut buf = [0i8; 512];
|
||||
let rc = unsafe { oakcodec_proxy_get_proxy_directory(cache_c.as_ptr(), buf.as_mut_ptr(), 512) };
|
||||
assert!(rc > 0);
|
||||
assert_eq!(
|
||||
crate::ffi::c_str(buf.as_ptr()).as_deref(),
|
||||
Some(format!("{}/proxy", cache).as_str())
|
||||
);
|
||||
|
||||
// get_working_filename appends ".working.mp4".
|
||||
let proxy = format!("{}/proxy/{}-0.1280x720.v1.a1.mp4", cache, 12345);
|
||||
let pc = cstr(&proxy);
|
||||
let rc = unsafe { oakcodec_proxy_get_working_filename(pc.as_ptr(), buf.as_mut_ptr(), 512) };
|
||||
assert!(rc > 0);
|
||||
assert_eq!(
|
||||
crate::ffi::c_str(buf.as_ptr()).as_deref(),
|
||||
Some(format!("{}.working.mp4", proxy).as_str())
|
||||
);
|
||||
|
||||
// get_or_start without a registrar -> Missing.
|
||||
let _g = REG_LOCK.lock().unwrap();
|
||||
set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
let mut out: oakcodec_proxy_result = unsafe { std::mem::zeroed() };
|
||||
let rc = unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, &p, &mut out) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
assert_eq!(out.state, OAKCODEC_PROXY_STATE_MISSING);
|
||||
|
||||
// With a registrar and no files -> Generating.
|
||||
set_task_submit_cb_extern(Some(accept_cb), std::ptr::null_mut());
|
||||
let rc = unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, &p, &mut out) };
|
||||
assert_eq!(rc, crate::error::OAKCODEC_OK);
|
||||
assert_eq!(out.state, OAKCODEC_PROXY_STATE_GENERATING);
|
||||
|
||||
// Invalid args.
|
||||
let rc = unsafe { oakcodec_proxy_get_or_start(std::ptr::null(), src.as_ptr(), 0, &p, &mut out) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
let rc = unsafe { oakcodec_proxy_get_or_start(cache_c.as_ptr(), src.as_ptr(), 0, &p, std::ptr::null_mut()) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
|
||||
// get_proxy_directory / get_proxy_filename / get_working_filename
|
||||
// argument validation.
|
||||
let rc = unsafe { oakcodec_proxy_get_proxy_directory(std::ptr::null(), buf.as_mut_ptr(), 512) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
let rc = unsafe { oakcodec_proxy_get_proxy_filename(std::ptr::null(), src.as_ptr(), 0, &p, buf.as_mut_ptr(), 512) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
let rc = unsafe { oakcodec_proxy_get_working_filename(std::ptr::null(), buf.as_mut_ptr(), 512) };
|
||||
assert_eq!(rc, OAKCODEC_E_INVALID);
|
||||
|
||||
set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ffmpeg_uses_configured_path() {
|
||||
// The current test binary is a real executable: the configured
|
||||
// path resolves to an absolute path.
|
||||
let me = std::env::current_exe().unwrap();
|
||||
let mc = cstr(me.to_str().unwrap());
|
||||
let mut buf = [0i8; 1024];
|
||||
let rc = unsafe { oakcodec_proxy_find_ffmpeg(mc.as_ptr(), buf.as_mut_ptr(), 1024) };
|
||||
assert!(rc > 0);
|
||||
let found = crate::ffi::c_str(buf.as_ptr()).unwrap();
|
||||
assert!(found.starts_with('/'));
|
||||
|
||||
// NULL configured path falls back to the search (empty or absolute).
|
||||
let rc = unsafe { oakcodec_proxy_find_ffmpeg(std::ptr::null(), buf.as_mut_ptr(), 1024) };
|
||||
assert!(rc > 0);
|
||||
let found = crate::ffi::c_str(buf.as_ptr()).unwrap();
|
||||
assert!(found.is_empty() || found.starts_with('/'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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/codec/task.h` exports.
|
||||
//!
|
||||
//! Complete inventory: set_task_submit_cb / task_submit_is_registered.
|
||||
//! The callback typedef and request struct are mirrored in
|
||||
//! [`crate::task`].
|
||||
|
||||
use std::ffi::c_int;
|
||||
|
||||
use crate::handle;
|
||||
|
||||
/// `oakcodec_set_task_submit_cb`: register (or replace, or clear with
|
||||
/// NULL) the global task submit callback.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_set_task_submit_cb(
|
||||
cb: Option<crate::task::OakCodecTaskSubmitFn>,
|
||||
userdata: *mut std::ffi::c_void,
|
||||
) {
|
||||
handle::guard_void(|| {
|
||||
crate::task::set_task_submit_cb_extern(cb, userdata);
|
||||
})
|
||||
}
|
||||
|
||||
/// `oakcodec_task_submit_is_registered`: 1 when a callback is set.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oakcodec_task_submit_is_registered() -> c_int {
|
||||
handle::guard_raw(|| {
|
||||
if crate::task::task_submit_is_registered() {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conformmanager::test_util::{accept_cb, REG_LOCK};
|
||||
use crate::task::set_task_submit_cb_extern;
|
||||
|
||||
#[test]
|
||||
fn register_query_clear() {
|
||||
let _g = REG_LOCK.lock().unwrap();
|
||||
|
||||
assert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 0);
|
||||
|
||||
unsafe { oakcodec_set_task_submit_cb(Some(accept_cb), std::ptr::null_mut()) };
|
||||
assert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 1);
|
||||
|
||||
unsafe { oakcodec_set_task_submit_cb(None, std::ptr::null_mut()) };
|
||||
assert_eq!(unsafe { oakcodec_task_submit_is_registered() }, 0);
|
||||
|
||||
// Restore a clean slate for the other modules.
|
||||
set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,280 @@
|
||||
// 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/>.
|
||||
|
||||
//! `olive::FootageDescription` — codec-internal stream inventory.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/footagedescription.h`. A value type describing
|
||||
//! the streams a `Decoder::probe()` found in a file. Video and subtitle
|
||||
//! streams are stored as oakcommon by-value handles; audio streams as
|
||||
//! `oakcore_rs::TimeRangeList`/raw audio params. The original's
|
||||
//! `Track::Type` mapping and XML load/save are intentionally not reproduced
|
||||
//! (NOTES.md §4) — use [`FootageDescription::stream_is_video`] etc.
|
||||
|
||||
use oakcore_rs::{Rational, TimeRange};
|
||||
|
||||
use crate::bridge::common::{OakAudioParams, OakSubtitleParams, OakVideoParams};
|
||||
|
||||
/// One stream entry in a footage description.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum StreamEntry {
|
||||
/// A video stream (params handle, addref'd).
|
||||
Video(OakVideoParams),
|
||||
/// An audio stream.
|
||||
Audio(OakAudioParams),
|
||||
/// A subtitle stream (params handle, addref'd).
|
||||
Subtitle(OakSubtitleParams),
|
||||
}
|
||||
|
||||
/// `olive::FootageDescription` — the decoder name plus stream inventory.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct FootageDescription {
|
||||
/// The decoder id that produced this description.
|
||||
decoder: String,
|
||||
/// Total number of streams (video + audio + subtitle).
|
||||
total_stream_count: usize,
|
||||
/// The streams, in probe order.
|
||||
streams: Vec<StreamEntry>,
|
||||
/// The media has a source start time.
|
||||
has_source_start_time: bool,
|
||||
/// Source start time (when present).
|
||||
source_start_time: Rational,
|
||||
/// Total duration across streams (may be empty).
|
||||
duration: Option<TimeRange>,
|
||||
}
|
||||
|
||||
impl FootageDescription {
|
||||
/// New, empty description with the given decoder id.
|
||||
pub fn new(decoder: &str) -> Self {
|
||||
Self {
|
||||
decoder: decoder.to_string(),
|
||||
total_stream_count: 0,
|
||||
streams: Vec::new(),
|
||||
has_source_start_time: false,
|
||||
// C++ default-constructs the Rational member, i.e. the 0/0
|
||||
// null sentinel. It is not meaningful until a source start time
|
||||
// is set.
|
||||
source_start_time: Rational::default(),
|
||||
duration: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The decoder id.
|
||||
pub fn decoder(&self) -> &str {
|
||||
&self.decoder
|
||||
}
|
||||
|
||||
/// Total stream count.
|
||||
pub fn total_stream_count(&self) -> usize {
|
||||
self.total_stream_count
|
||||
}
|
||||
|
||||
/// Number of video streams.
|
||||
pub fn video_stream_count(&self) -> usize {
|
||||
self.streams
|
||||
.iter()
|
||||
.filter(|s| matches!(s, StreamEntry::Video(_)))
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Number of audio streams.
|
||||
pub fn audio_stream_count(&self) -> usize {
|
||||
self.streams
|
||||
.iter()
|
||||
.filter(|s| matches!(s, StreamEntry::Audio(_)))
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Number of subtitle streams.
|
||||
pub fn subtitle_stream_count(&self) -> usize {
|
||||
self.streams
|
||||
.iter()
|
||||
.filter(|s| matches!(s, StreamEntry::Subtitle(_)))
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Whether the `index`-th stream (in probe order) is a video stream.
|
||||
pub fn stream_is_video(&self, index: usize) -> bool {
|
||||
self.streams
|
||||
.get(index)
|
||||
.is_some_and(|s| matches!(s, StreamEntry::Video(_)))
|
||||
}
|
||||
|
||||
/// Whether the `index`-th stream (in probe order) is an audio stream.
|
||||
pub fn stream_is_audio(&self, index: usize) -> bool {
|
||||
self.streams
|
||||
.get(index)
|
||||
.is_some_and(|s| matches!(s, StreamEntry::Audio(_)))
|
||||
}
|
||||
|
||||
/// Whether the `index`-th stream (in probe order) is a subtitle stream.
|
||||
pub fn stream_is_subtitle(&self, index: usize) -> bool {
|
||||
self.streams
|
||||
.get(index)
|
||||
.is_some_and(|s| matches!(s, StreamEntry::Subtitle(_)))
|
||||
}
|
||||
|
||||
/// The `index`-th video stream's params (by video-stream ordinal).
|
||||
pub fn get_video_stream(&self, index: usize) -> Option<&OakVideoParams> {
|
||||
self.streams
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
StreamEntry::Video(p) => Some(p),
|
||||
_ => None,
|
||||
})
|
||||
.nth(index)
|
||||
}
|
||||
|
||||
/// The `index`-th audio stream's params (by audio-stream ordinal).
|
||||
pub fn get_audio_stream(&self, index: usize) -> Option<&OakAudioParams> {
|
||||
self.streams
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
StreamEntry::Audio(p) => Some(p),
|
||||
_ => None,
|
||||
})
|
||||
.nth(index)
|
||||
}
|
||||
|
||||
/// The `index`-th subtitle stream's params (by subtitle-stream ordinal).
|
||||
pub fn get_subtitle_stream(&self, index: usize) -> Option<&OakSubtitleParams> {
|
||||
self.streams
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
StreamEntry::Subtitle(p) => Some(p),
|
||||
_ => None,
|
||||
})
|
||||
.nth(index)
|
||||
}
|
||||
|
||||
/// Whether the media has a source start time.
|
||||
pub fn has_source_start_time(&self) -> bool {
|
||||
self.has_source_start_time
|
||||
}
|
||||
|
||||
/// Source start time.
|
||||
pub fn source_start_time(&self) -> Rational {
|
||||
self.source_start_time
|
||||
}
|
||||
|
||||
/// Total duration across streams.
|
||||
pub fn duration(&self) -> Option<TimeRange> {
|
||||
self.duration
|
||||
}
|
||||
|
||||
/// Append one stream entry, mirroring the C++ `add_*_stream` family.
|
||||
///
|
||||
/// Test/extension support: the ffi probe tests build
|
||||
/// `FootageDescription`s through a fake decoder and need a way to
|
||||
/// populate them. Hidden from docs; never called by production code.
|
||||
#[doc(hidden)]
|
||||
pub fn push_stream(&mut self, entry: StreamEntry) {
|
||||
self.total_stream_count += 1;
|
||||
self.streams.push(entry);
|
||||
}
|
||||
|
||||
/// Set the total stream count, mirroring the C++ `set_stream_count`
|
||||
/// (which records every stream in the container, including ones the
|
||||
/// probe could not describe and therefore did not push).
|
||||
///
|
||||
/// Hidden from docs; probe/extension support only.
|
||||
#[doc(hidden)]
|
||||
pub fn set_stream_count(&mut self, count: usize) {
|
||||
self.total_stream_count = count;
|
||||
}
|
||||
|
||||
/// Set the source start time, mirroring the C++ `set_source_start_time`.
|
||||
/// `source` is the raw metadata source kind (unused by the Rust port).
|
||||
///
|
||||
/// Hidden from docs; probe/extension support only.
|
||||
#[doc(hidden)]
|
||||
pub fn set_source_start_time(&mut self, time: Rational, _source: i32) {
|
||||
self.has_source_start_time = true;
|
||||
self.source_start_time = time;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn video_params(index: i32) -> OakVideoParams {
|
||||
OakVideoParams {
|
||||
ctx: index as usize as *mut std::ffi::c_void,
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: crate::handle::OAKCODEC_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
fn audio_params() -> OakAudioParams {
|
||||
OakAudioParams {
|
||||
ctx: std::ptr::null_mut(),
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: crate::handle::OAKCODEC_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
fn subtitle_params() -> OakSubtitleParams {
|
||||
OakSubtitleParams {
|
||||
ctx: std::ptr::null_mut(),
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: crate::handle::OAKCODEC_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_description_is_empty() {
|
||||
let d = FootageDescription::new("ffmpeg");
|
||||
assert_eq!(d.decoder(), "ffmpeg");
|
||||
assert_eq!(d.total_stream_count(), 0);
|
||||
assert_eq!(d.video_stream_count(), 0);
|
||||
assert_eq!(d.audio_stream_count(), 0);
|
||||
assert_eq!(d.subtitle_stream_count(), 0);
|
||||
assert!(!d.has_source_start_time());
|
||||
assert!(d.duration().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_stream_updates_counts_and_queries() {
|
||||
let mut d = FootageDescription::new("mock");
|
||||
d.push_stream(StreamEntry::Video(video_params(0)));
|
||||
d.push_stream(StreamEntry::Audio(audio_params()));
|
||||
d.push_stream(StreamEntry::Subtitle(subtitle_params()));
|
||||
d.push_stream(StreamEntry::Video(video_params(1)));
|
||||
|
||||
assert_eq!(d.total_stream_count(), 4);
|
||||
assert_eq!(d.video_stream_count(), 2);
|
||||
assert_eq!(d.audio_stream_count(), 1);
|
||||
assert_eq!(d.subtitle_stream_count(), 1);
|
||||
|
||||
// stream_is_* by probe order.
|
||||
assert!(d.stream_is_video(0));
|
||||
assert!(d.stream_is_audio(1));
|
||||
assert!(d.stream_is_subtitle(2));
|
||||
assert!(d.stream_is_video(3));
|
||||
|
||||
// Ordinal getters.
|
||||
assert!(d.get_video_stream(0).is_some());
|
||||
assert!(d.get_video_stream(1).is_some());
|
||||
assert!(d.get_video_stream(2).is_none());
|
||||
assert!(d.get_audio_stream(0).is_some());
|
||||
assert!(d.get_audio_stream(1).is_none());
|
||||
assert!(d.get_subtitle_stream(0).is_some());
|
||||
assert!(d.get_subtitle_stream(1).is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
// 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/>.
|
||||
|
||||
//! `olive::Frame` — a CPU pixel buffer plus an `OakVideoParams` handle.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/frame.h`. The params are held as an oakcommon
|
||||
//! by-value handle (`bridge::common::OakVideoParams`, refcounted) so the
|
||||
//! byte-level ABI of `oakcodec_frame_get_params`/`_set_params` is
|
||||
//! unchanged; the pixel data itself is a plain `Vec<u8>`. Line-size and
|
||||
//! pixel-format math lives here.
|
||||
|
||||
use crate::bridge::common::{
|
||||
oakcommon_videoparams_free, oakcommon_videoparams_get_format,
|
||||
oakcommon_videoparams_get_height, oakcommon_videoparams_get_is_valid,
|
||||
oakcommon_videoparams_get_width, oakcommon_videoparams_init, OakVideoParams,
|
||||
};
|
||||
use oakcore_rs::{PixelFormat, Rational};
|
||||
|
||||
/// Number of channels in the internal RGBA pipeline layout
|
||||
/// (`VideoParams::k_internal_channel_count == k_rgba_channel_count == 4`).
|
||||
/// The frame math (linesize, per-pixel offsets) always assumes this layout,
|
||||
/// matching the C++ decoder path which produces/consumes RGBA frames.
|
||||
const VIDEO_CHANNELS: i32 = 4;
|
||||
|
||||
/// Interlacing of a frame's parameter set (VideoParams::Interlacing).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum Interlacing {
|
||||
/// Progressive.
|
||||
None = 0,
|
||||
/// Upper field first.
|
||||
TopFieldFirst = 1,
|
||||
/// Lower field first.
|
||||
BottomFieldFirst = 2,
|
||||
}
|
||||
|
||||
/// `olive::Frame`: reference-counted CPU pixel buffer + params handle.
|
||||
#[derive(Debug)]
|
||||
pub struct Frame {
|
||||
/// Video parameter set (oakcommon handle, refcounted).
|
||||
pub params: Option<OakVideoParams>,
|
||||
/// Pixel buffer (unallocated until `allocate`).
|
||||
data: Vec<u8>,
|
||||
/// Distance between rows in bytes (0 until params are set).
|
||||
linesize_bytes: i32,
|
||||
/// Timestamp, rational seconds.
|
||||
timestamp: Rational,
|
||||
/// Allocated pixel format (may differ from params while converting).
|
||||
allocated_format: PixelFormat,
|
||||
}
|
||||
|
||||
/// Map an `OakPixelFormat` int code back to a `PixelFormat` (unknown codes
|
||||
/// become [`PixelFormat::Invalid`]).
|
||||
fn pixel_format_from_i32(v: i32) -> PixelFormat {
|
||||
match v {
|
||||
0 => PixelFormat::U8,
|
||||
1 => PixelFormat::U10,
|
||||
2 => PixelFormat::U16,
|
||||
3 => PixelFormat::F16,
|
||||
4 => PixelFormat::F32,
|
||||
_ => PixelFormat::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytes per pixel for `format` at `channels`, matching
|
||||
/// `VideoParams::get_bytes_per_pixel`:
|
||||
/// - U10 is a packed RGBA10A2 pixel: 4 bytes for the RGBA layout,
|
||||
/// regardless of channel count; anything else is rejected (0).
|
||||
/// - All other formats are `bytes_per_channel * channels`.
|
||||
fn bytes_per_pixel(format: PixelFormat, channels: i32) -> i32 {
|
||||
if format == PixelFormat::U10 {
|
||||
return if channels == VIDEO_CHANNELS { 4 } else { 0 };
|
||||
}
|
||||
(format.bytes_per_channel() as i32) * channels
|
||||
}
|
||||
|
||||
/// Increment the refcount of a params handle (a no-op for test-stub handles
|
||||
/// whose `addref` is `None`). `pub(crate)` so the ffi layer can hand out
|
||||
/// addref'd copies (`oakcodec_frame_get_params`).
|
||||
pub(crate) fn params_addref(p: &OakVideoParams) {
|
||||
if let Some(addref) = p.addref {
|
||||
// SAFETY: `addref` is a valid C function pointer targeting `ctx`.
|
||||
unsafe { addref(p.ctx) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Release a params handle (prefers the `release` function pointer; the
|
||||
/// test stubs use `oakcommon_videoparams_free` instead). Nulls `ctx` so the
|
||||
/// handle cannot be released twice.
|
||||
pub(crate) fn params_release(p: &mut OakVideoParams) {
|
||||
if p.ctx.is_null() {
|
||||
return;
|
||||
}
|
||||
if let Some(release) = p.release {
|
||||
// SAFETY: `release` is a valid C function pointer targeting `ctx`.
|
||||
unsafe { release(p.ctx) };
|
||||
} else {
|
||||
// SAFETY: `p` points at a live handle; `oakcommon_videoparams_free`
|
||||
// is a no-op for the null ctx we leave behind.
|
||||
unsafe { oakcommon_videoparams_free(p) };
|
||||
}
|
||||
p.ctx = std::ptr::null_mut();
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// New frame with default (invalid) params; buffer unallocated.
|
||||
pub fn new() -> Self {
|
||||
let params = unsafe { oakcommon_videoparams_init() };
|
||||
Frame {
|
||||
params: Some(params),
|
||||
data: Vec::new(),
|
||||
linesize_bytes: 0,
|
||||
timestamp: Rational::new(0, 1),
|
||||
allocated_format: PixelFormat::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
/// New frame with a copy of `params` (handle addref'd internally).
|
||||
pub fn with_params(params: OakVideoParams) -> Self {
|
||||
params_addref(¶ms);
|
||||
let mut frame = Frame {
|
||||
params: Some(params),
|
||||
data: Vec::new(),
|
||||
linesize_bytes: 0,
|
||||
timestamp: Rational::new(0, 1),
|
||||
allocated_format: PixelFormat::Invalid,
|
||||
};
|
||||
frame.recompute_linesize();
|
||||
frame
|
||||
}
|
||||
|
||||
/// The video parameter set, or `None` when empty.
|
||||
pub fn params(&self) -> Option<&OakVideoParams> {
|
||||
self.params.as_ref()
|
||||
}
|
||||
|
||||
/// Replace the parameter set (handle addref'd), recompute line sizes,
|
||||
/// do NOT reallocate the buffer.
|
||||
pub fn set_params(&mut self, params: OakVideoParams) {
|
||||
if let Some(mut old) = self.params.take() {
|
||||
params_release(&mut old);
|
||||
}
|
||||
params_addref(¶ms);
|
||||
self.params = Some(params);
|
||||
self.recompute_linesize();
|
||||
// Deliberately do not touch `data`: an existing buffer keeps its
|
||||
// layout; `allocated_format` stays at the old format until the next
|
||||
// `allocate()`.
|
||||
}
|
||||
|
||||
/// Recompute `linesize_bytes` from the current params (0 when unset).
|
||||
fn recompute_linesize(&mut self) {
|
||||
self.linesize_bytes = match &self.params {
|
||||
Some(p) => {
|
||||
let w = unsafe { oakcommon_videoparams_get_width(p.clone()) };
|
||||
let fmt = pixel_format_from_i32(unsafe {
|
||||
oakcommon_videoparams_get_format(p.clone())
|
||||
});
|
||||
Self::generate_linesize_bytes(fmt, w)
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
}
|
||||
|
||||
/// Allocate the pixel buffer from the current params.
|
||||
pub fn allocate(&mut self) -> crate::error::Result<()> {
|
||||
let params = match &self.params {
|
||||
Some(p) => p.clone(),
|
||||
None => return Err(crate::error::Error::State),
|
||||
};
|
||||
|
||||
let is_valid = unsafe { oakcommon_videoparams_get_is_valid(params.clone()) };
|
||||
if is_valid == 0 {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
|
||||
if self.is_allocated() {
|
||||
// Already allocated; leave the buffer alone.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let width = unsafe { oakcommon_videoparams_get_width(params.clone()) };
|
||||
let height = unsafe { oakcommon_videoparams_get_height(params.clone()) };
|
||||
let format = pixel_format_from_i32(unsafe {
|
||||
oakcommon_videoparams_get_format(params)
|
||||
});
|
||||
|
||||
let linesize = Self::generate_linesize_bytes(format, width);
|
||||
let size = (linesize as usize).wrapping_mul(height as usize);
|
||||
|
||||
self.data.resize(size, 0);
|
||||
self.linesize_bytes = linesize;
|
||||
self.allocated_format = format;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 1 when the pixel buffer is allocated.
|
||||
pub fn is_allocated(&self) -> bool {
|
||||
!self.data.is_empty()
|
||||
}
|
||||
|
||||
/// Writable pixel buffer slice, or `None` when unallocated.
|
||||
pub fn data(&self) -> Option<&[u8]> {
|
||||
if self.is_allocated() {
|
||||
Some(&self.data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Mutable pixel buffer slice, or `None` when unallocated.
|
||||
pub fn data_mut(&mut self) -> Option<&mut [u8]> {
|
||||
if self.is_allocated() {
|
||||
Some(&mut self.data)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Size of the pixel buffer in bytes (0 when unallocated).
|
||||
pub fn allocated_size(&self) -> usize {
|
||||
self.data.len()
|
||||
}
|
||||
|
||||
/// Distance between two rows in bytes (0 when params are unset).
|
||||
pub fn linesize_bytes(&self) -> i32 {
|
||||
self.linesize_bytes
|
||||
}
|
||||
|
||||
/// Distance between two rows in pixels.
|
||||
pub fn linesize_pixels(&self) -> i32 {
|
||||
let bpp = self.bytes_per_pixel();
|
||||
if bpp > 0 {
|
||||
self.linesize_bytes / bpp
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Bytes per pixel for the current params format (RGBA layout).
|
||||
fn bytes_per_pixel(&self) -> i32 {
|
||||
bytes_per_pixel(self.format(), VIDEO_CHANNELS)
|
||||
}
|
||||
|
||||
/// Frame width in pixels (0 when params are empty).
|
||||
pub fn width(&self) -> i32 {
|
||||
match &self.params {
|
||||
Some(p) => unsafe { oakcommon_videoparams_get_width(p.clone()) },
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Frame height in pixels (0 when params are empty).
|
||||
pub fn height(&self) -> i32 {
|
||||
match &self.params {
|
||||
Some(p) => unsafe { oakcommon_videoparams_get_height(p.clone()) },
|
||||
None => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Pixel format (`OakPixelFormat` value).
|
||||
pub fn format(&self) -> PixelFormat {
|
||||
match &self.params {
|
||||
Some(p) => {
|
||||
pixel_format_from_i32(unsafe { oakcommon_videoparams_get_format(p.clone()) })
|
||||
}
|
||||
None => PixelFormat::Invalid,
|
||||
}
|
||||
}
|
||||
|
||||
/// Plane channel count of the params format.
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// `src/codec/src/frame.h` reads this from the params handle via
|
||||
/// `oakcommon_videoparams_get_channel_count`, which is not exposed in the
|
||||
/// Rust bridge. Decoder frames are always produced in the internal RGBA
|
||||
/// layout, so this returns [`VIDEO_CHANNELS`] (4).
|
||||
pub fn channel_count(&self) -> i32 {
|
||||
VIDEO_CHANNELS
|
||||
}
|
||||
|
||||
/// Timestamp as a rational number of seconds.
|
||||
pub fn timestamp(&self) -> Rational {
|
||||
self.timestamp
|
||||
}
|
||||
|
||||
/// Set the timestamp.
|
||||
pub fn set_timestamp(&mut self, ts: Rational) {
|
||||
self.timestamp = ts;
|
||||
}
|
||||
|
||||
/// Distance between rows for a (format, width) pair, in bytes.
|
||||
///
|
||||
/// Matches `Frame::generate_linesize_bytes(width, format, channel_count)`
|
||||
/// in `src/codec/src/frame.cpp` with `channel_count` fixed at
|
||||
/// [`VIDEO_CHANNELS`]: bytes per pixel times the width rounded up to a
|
||||
/// 32-byte boundary. Uses wrapping arithmetic so extreme (or negative)
|
||||
/// widths behave like the C++ `int` math rather than panicking.
|
||||
pub fn generate_linesize_bytes(format: PixelFormat, width: i32) -> i32 {
|
||||
let bpp = bytes_per_pixel(format, VIDEO_CHANNELS);
|
||||
let aligned = width.wrapping_add(31) & !31;
|
||||
bpp.wrapping_mul(aligned)
|
||||
}
|
||||
|
||||
/// Convert the buffer to another pixel format (`convert_to_olive_format`).
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// `src/codec/src/frame.cpp` — the destination params are carried by
|
||||
/// the C++ callers via `oakcommon_videoparams_*`; Rust keeps the
|
||||
/// equivalent state in `self.params`.
|
||||
///
|
||||
/// When the current params format already matches the format the buffer
|
||||
/// was allocated in, this is a no-op (`Ok`). A genuine pixel-format
|
||||
/// conversion requires the OIIO bridge (`convert_to_olive_format`), which
|
||||
/// is not yet ported to the pure-Rust crate; until then a mismatched
|
||||
/// conversion is rejected with [`crate::error::Error::State`].
|
||||
pub fn convert(&mut self) -> crate::error::Result<()> {
|
||||
if !self.is_allocated() {
|
||||
return Err(crate::error::Error::State);
|
||||
}
|
||||
let fmt = self.format();
|
||||
if self.allocated_format == fmt {
|
||||
self.recompute_linesize();
|
||||
return Ok(());
|
||||
}
|
||||
Err(crate::error::Error::State)
|
||||
}
|
||||
|
||||
/// True when `(x, y)` lies inside the allocated buffer.
|
||||
fn contains_pixel(&self, x: i32, y: i32) -> bool {
|
||||
self.is_allocated() && x >= 0 && x < self.width() && y >= 0 && y < self.height()
|
||||
}
|
||||
|
||||
/// Read a pixel sample at (x, y).
|
||||
///
|
||||
/// Returns the first byte of the pixel at `(x, y)` (the R channel for
|
||||
/// RGBA). Out-of-bounds reads return 0, matching the C++ default
|
||||
/// (transparent black) color.
|
||||
pub fn get_pixel(&self, x: i32, y: i32) -> u8 {
|
||||
if !self.contains_pixel(x, y) {
|
||||
return 0;
|
||||
}
|
||||
let offset = (y as usize).wrapping_mul(self.linesize_bytes as usize)
|
||||
+ (x as usize).wrapping_mul(self.bytes_per_pixel() as usize);
|
||||
*self.data.get(offset).unwrap_or(&0)
|
||||
}
|
||||
|
||||
/// Write a pixel sample at (x, y).
|
||||
///
|
||||
/// Writes `value` to the first byte of the pixel at `(x, y)`. Out-of-bounds
|
||||
/// writes are ignored, matching the C++ `set_pixel`.
|
||||
pub fn set_pixel(&mut self, x: i32, y: i32, value: u8) {
|
||||
if !self.contains_pixel(x, y) {
|
||||
return;
|
||||
}
|
||||
let offset = (y as usize).wrapping_mul(self.linesize_bytes as usize)
|
||||
+ (x as usize).wrapping_mul(self.bytes_per_pixel() as usize);
|
||||
if let Some(byte) = self.data.get_mut(offset) {
|
||||
*byte = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Frame {
|
||||
/// Release the owned params handle when the last reference dies,
|
||||
/// mirroring the C++ `Frame::~Frame`.
|
||||
fn drop(&mut self) {
|
||||
if let Some(mut p) = self.params.take() {
|
||||
params_release(&mut p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bridge::common::oakcommon_videoparams_init_basic;
|
||||
|
||||
fn frame(w: i32, h: i32) -> Frame {
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(w, h) };
|
||||
Frame::with_params(params)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linesize_is_32_byte_aligned_for_u8() {
|
||||
// U8 RGBA: 4 bytes/pixel, width rounded up to a 32-byte boundary.
|
||||
assert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 9), 4 * 32);
|
||||
assert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 100), 4 * 128);
|
||||
assert_eq!(Frame::generate_linesize_bytes(PixelFormat::U8, 0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linesize_respects_16bit_and_u10() {
|
||||
assert_eq!(Frame::generate_linesize_bytes(PixelFormat::U16, 16), 8 * 32);
|
||||
// U10 is a packed 4-byte RGBA pixel regardless of channel count.
|
||||
assert_eq!(Frame::generate_linesize_bytes(PixelFormat::U10, 16), 4 * 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_fills_buffer_and_reports_size() {
|
||||
let mut f = frame(100, 50);
|
||||
assert!(!f.is_allocated());
|
||||
assert_eq!(f.allocated_size(), 0);
|
||||
assert!(f.data().is_none());
|
||||
|
||||
f.allocate().unwrap();
|
||||
assert!(f.is_allocated());
|
||||
assert_eq!(f.allocated_size(), (4 * 128) * 50);
|
||||
assert_eq!(f.data().map(|d| d.len()), Some((4 * 128) * 50));
|
||||
assert_eq!(f.linesize_bytes(), 4 * 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocate_invalid_params_is_error() {
|
||||
// init_basic(0, 0) is not valid -> allocate must reject.
|
||||
let mut f = frame(0, 0);
|
||||
assert!(f.allocate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_set_pixel_round_trip() {
|
||||
let mut f = frame(100, 50);
|
||||
f.allocate().unwrap();
|
||||
|
||||
f.set_pixel(3, 4, 0xAB);
|
||||
assert_eq!(f.get_pixel(3, 4), 0xAB);
|
||||
|
||||
// pixel (0,0) is the first byte; pixel (1,0) is bpp bytes later.
|
||||
f.set_pixel(0, 0, 0x11);
|
||||
f.set_pixel(1, 0, 0x22);
|
||||
assert_eq!(f.get_pixel(0, 0), 0x11);
|
||||
assert_eq!(f.get_pixel(1, 0), 0x22);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_bounds_reads_zero_and_writes_ignored() {
|
||||
let mut f = frame(10, 10);
|
||||
f.allocate().unwrap();
|
||||
assert_eq!(f.get_pixel(50, 50), 0);
|
||||
assert_eq!(f.get_pixel(-1, 0), 0);
|
||||
f.set_pixel(50, 50, 0xFF);
|
||||
// untouched
|
||||
assert_eq!(f.data().unwrap()[0], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_params_recomputes_linesize_without_realloc() {
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(10, 10) };
|
||||
let mut f = Frame::with_params(params);
|
||||
f.allocate().unwrap();
|
||||
let before = f.allocated_size();
|
||||
|
||||
let wider = unsafe { oakcommon_videoparams_init_basic(100, 10) };
|
||||
f.set_params(wider);
|
||||
// linesize reflects the new width, but the buffer is untouched.
|
||||
assert_eq!(f.linesize_bytes(), 4 * 128);
|
||||
assert_eq!(f.allocated_size(), before);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_extra {
|
||||
use super::*;
|
||||
use crate::bridge::common::oakcommon_videoparams_init_basic;
|
||||
|
||||
fn frame(w: i32, h: i32) -> Frame {
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(w, h) };
|
||||
Frame::with_params(params)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linesize_pixels_derives_from_bytes() {
|
||||
// U8 RGBA: bpp 4 -> linesize_pixels = linesize_bytes / 4.
|
||||
let mut f = frame(32, 16);
|
||||
f.allocate().unwrap();
|
||||
assert_eq!(f.linesize_bytes(), 4 * 32);
|
||||
assert_eq!(f.linesize_pixels(), 32);
|
||||
// Unallocated / unset params -> 0.
|
||||
let g = Frame::new();
|
||||
assert_eq!(g.linesize_pixels(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_count_is_internal_rgba_layout() {
|
||||
let f = frame(4, 4);
|
||||
assert_eq!(f.channel_count(), VIDEO_CHANNELS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convert_is_noop_when_format_matches() {
|
||||
let mut f = frame(16, 16);
|
||||
f.allocate().unwrap();
|
||||
// allocated U8 == params U8 -> no-op Ok.
|
||||
assert!(f.convert().is_ok());
|
||||
// Unallocated -> Err(State).
|
||||
let mut g = Frame::new();
|
||||
assert!(g.convert().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pixel_format_from_unknown_code_is_invalid() {
|
||||
let p = unsafe { oakcommon_videoparams_init_basic(1, 1) };
|
||||
unsafe { crate::bridge::common::oakcommon_videoparams_set_format(p.clone(), 99) };
|
||||
let f = Frame::with_params(p);
|
||||
assert_eq!(f.format(), PixelFormat::Invalid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_timestamp_is_zero() {
|
||||
let f = Frame::new();
|
||||
let ts = f.timestamp();
|
||||
assert_eq!(ts.numerator(), 0);
|
||||
assert_eq!(ts.denominator(), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// 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/>.
|
||||
|
||||
//! `olive::FrameManager` — a pool of reusable [`crate::frame::Frame`]
|
||||
//! buffers plus a background garbage-collection thread.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/framemanager.h`. The C++ manager kept a pool of
|
||||
//! `std::list<FramePtr>` and a QThread that periodically dropped frames
|
||||
//! whose last reference died. Rust keeps the same contract behind a
|
||||
//! `Mutex`; the background thread is replaced by a dedicated GC thread
|
||||
//! that drains the pool of freeable frames.
|
||||
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::bridge::common::{
|
||||
oakcommon_videoparams_equals, OakVideoParams,
|
||||
};
|
||||
use crate::frame::Frame;
|
||||
|
||||
/// `olive::FrameManager`: singleton frame pool with background GC.
|
||||
pub struct FrameManager {
|
||||
/// Pooled frames waiting for reuse (most-recently-freed first).
|
||||
pool: Mutex<Vec<Frame>>,
|
||||
/// Peak number of live frames observed (diagnostics).
|
||||
peak_count: Mutex<usize>,
|
||||
/// Current number of frames outstanding (not yet returned).
|
||||
outstanding: Mutex<usize>,
|
||||
}
|
||||
|
||||
impl FrameManager {
|
||||
/// The process-wide FrameManager singleton.
|
||||
///
|
||||
/// Constructs the manager on first use and spawns the background
|
||||
/// garbage-collection thread exactly once.
|
||||
pub fn instance() -> &'static FrameManager {
|
||||
static INSTANCE: OnceLock<FrameManager> = OnceLock::new();
|
||||
let mgr = INSTANCE.get_or_init(FrameManager::new);
|
||||
// Spawn the GC thread on first construction only. We use a `static`
|
||||
// flag guarded by the same lock-free path: the first caller to build
|
||||
// the manager also starts the thread. Subsequent calls skip it.
|
||||
spawn_gc_thread_once(mgr);
|
||||
mgr
|
||||
}
|
||||
|
||||
/// Create the empty manager.
|
||||
fn new() -> Self {
|
||||
FrameManager {
|
||||
pool: Mutex::new(Vec::new()),
|
||||
peak_count: Mutex::new(0),
|
||||
outstanding: Mutex::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the pool (dropping all cached frames).
|
||||
pub fn clear(&self) {
|
||||
self.pool.lock().unwrap().clear();
|
||||
}
|
||||
|
||||
/// Create a frame with the given params (borrowed from the pool when a
|
||||
/// compatible free frame exists, else freshly allocated).
|
||||
pub fn create_frame(&self, params: OakVideoParams) -> Arc<Frame> {
|
||||
let frame = {
|
||||
let mut pool = self.pool.lock().unwrap();
|
||||
match pool
|
||||
.iter()
|
||||
.position(|f| frame_matches(f, ¶ms))
|
||||
{
|
||||
Some(idx) => pool.swap_remove(idx),
|
||||
None => Frame::with_params(params),
|
||||
}
|
||||
};
|
||||
|
||||
let mut outstanding = self.outstanding.lock().unwrap();
|
||||
*outstanding += 1;
|
||||
let mut peak = self.peak_count.lock().unwrap();
|
||||
if *outstanding > *peak {
|
||||
*peak = *outstanding;
|
||||
}
|
||||
|
||||
Arc::new(frame)
|
||||
}
|
||||
|
||||
/// Return a frame to the pool for reuse.
|
||||
pub fn return_frame(&self, frame: Frame) {
|
||||
let mut outstanding = self.outstanding.lock().unwrap();
|
||||
*outstanding = outstanding.saturating_sub(1);
|
||||
self.pool.lock().unwrap().push(frame);
|
||||
}
|
||||
|
||||
/// Number of frames currently outstanding (not in the pool).
|
||||
pub fn live_count(&self) -> usize {
|
||||
*self.outstanding.lock().unwrap()
|
||||
}
|
||||
|
||||
/// Peak number of live frames observed.
|
||||
pub fn peak_count(&self) -> usize {
|
||||
*self.peak_count.lock().unwrap()
|
||||
}
|
||||
|
||||
/// Background GC loop; runs on the manager's dedicated thread.
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// `src/codec/src/framemanager.cpp` `run()` collected frames whose last
|
||||
/// reference had died, based on per-frame timestamps. The Rust skeleton
|
||||
/// keeps a pool of reusable buffers but no per-frame age, so the GC
|
||||
/// simply drains the whole pool. This bounds memory: frames are reused
|
||||
/// between GC passes and released once every GC period, which matches
|
||||
/// the C++ manager's intent of keeping pool memory from growing
|
||||
/// unbounded.
|
||||
fn gc_loop(&self) {
|
||||
self.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn the GC thread once for the process.
|
||||
fn spawn_gc_thread_once(mgr: &'static FrameManager) {
|
||||
static STARTED: OnceLock<()> = OnceLock::new();
|
||||
STARTED.get_or_init(|| {
|
||||
thread::spawn(move || {
|
||||
// `mgr` is `'static`; the thread may outlive every other
|
||||
// reference. Keep polling until the process exits.
|
||||
loop {
|
||||
thread::sleep(Duration::from_millis(5000));
|
||||
mgr.gc_loop();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// True when `frame` carries params equal to `params`.
|
||||
fn frame_matches(frame: &Frame, params: &OakVideoParams) -> bool {
|
||||
let Some(frame_params) = frame.params() else {
|
||||
return false;
|
||||
};
|
||||
let eq = unsafe {
|
||||
oakcommon_videoparams_equals(frame_params.clone(), params.clone())
|
||||
};
|
||||
eq != 0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bridge::common::oakcommon_videoparams_init_basic;
|
||||
|
||||
#[test]
|
||||
fn create_and_return_tracks_counts() {
|
||||
let mgr = FrameManager::new();
|
||||
assert_eq!(mgr.live_count(), 0);
|
||||
assert_eq!(mgr.peak_count(), 0);
|
||||
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(64, 64) };
|
||||
let frame = mgr.create_frame(params);
|
||||
assert_eq!(mgr.live_count(), 1);
|
||||
assert_eq!(mgr.peak_count(), 1);
|
||||
|
||||
// Return by unwrapping the single strong reference.
|
||||
let frame = Arc::try_unwrap(frame).unwrap();
|
||||
mgr.return_frame(frame);
|
||||
assert_eq!(mgr.live_count(), 0);
|
||||
assert_eq!(mgr.peak_count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_reuses_compatible_frames() {
|
||||
let mgr = FrameManager::new();
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(64, 64) };
|
||||
let f1 = mgr.create_frame(params);
|
||||
mgr.return_frame(Arc::try_unwrap(f1).unwrap());
|
||||
assert_eq!(mgr.live_count(), 0);
|
||||
|
||||
// A compatible request reuses the pooled buffer rather than
|
||||
// allocating a new one.
|
||||
let f2 = mgr.create_frame(unsafe {
|
||||
oakcommon_videoparams_init_basic(64, 64)
|
||||
});
|
||||
assert_eq!(mgr.live_count(), 1);
|
||||
assert_eq!(mgr.peak_count(), 1);
|
||||
Arc::try_unwrap(f2).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn peak_count_tracks_maximum() {
|
||||
let mgr = FrameManager::new();
|
||||
let p1 = unsafe { oakcommon_videoparams_init_basic(64, 64) };
|
||||
let p2 = unsafe { oakcommon_videoparams_init_basic(128, 128) };
|
||||
let a = mgr.create_frame(p1);
|
||||
let b = mgr.create_frame(p2);
|
||||
assert_eq!(mgr.live_count(), 2);
|
||||
assert_eq!(mgr.peak_count(), 2);
|
||||
mgr.return_frame(Arc::try_unwrap(a).unwrap());
|
||||
assert_eq!(mgr.live_count(), 1);
|
||||
assert_eq!(mgr.peak_count(), 2);
|
||||
Arc::try_unwrap(b).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_drops_pooled_frames() {
|
||||
let mgr = FrameManager::new();
|
||||
let params = unsafe { oakcommon_videoparams_init_basic(64, 64) };
|
||||
let f = mgr.create_frame(params);
|
||||
mgr.return_frame(Arc::try_unwrap(f).unwrap());
|
||||
assert_eq!(mgr.pool.lock().unwrap().len(), 1);
|
||||
|
||||
mgr.clear();
|
||||
assert_eq!(mgr.pool.lock().unwrap().len(), 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
// 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/>.
|
||||
|
||||
//! Refcounted-handle scaffolding. Same pattern as the oaknode/oakplugin
|
||||
//! crates (`src/node/rust/src/handle.rs`); intentionally duplicated rather
|
||||
//! than shared — each module DLL must run its own addref/release code
|
||||
//! (the function pointers in a handle always point into the DLL that
|
||||
//! created the object).
|
||||
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::ptr;
|
||||
use std::sync::atomic::{AtomicI32, AtomicU32, Ordering};
|
||||
|
||||
use crate::error::{self, OAKCODEC_E_FAILED};
|
||||
|
||||
/// Number of boxed handle objects currently alive (leak/debug checking).
|
||||
///
|
||||
/// Mirrors `oakcodec::g_alive_count` in `src/codec/c_api/frame.cpp`: every
|
||||
/// `make_owned` box increments it and `box_release` decrements it when the
|
||||
/// last reference drops. `oakcodec_debug_alive_count` reports it.
|
||||
static ALIVE: AtomicI32 = AtomicI32::new(0);
|
||||
|
||||
/// ABI version stamped into every handle.
|
||||
pub const OAKCODEC_ABI_VERSION: u32 = 1;
|
||||
|
||||
/// Heap box behind a handle's `ctx`.
|
||||
pub struct RefBox<T: ?Sized> {
|
||||
/// Atomic reference count.
|
||||
pub refs: AtomicU32,
|
||||
/// Boxed value.
|
||||
pub value: T,
|
||||
}
|
||||
|
||||
/// `#[repr(C)]` mirror of the public handle structs
|
||||
/// (`{ctx, addref, release, abi_version}`).
|
||||
///
|
||||
/// Handles are `Copy`: passing one by value copies the struct, not the
|
||||
/// reference count — exactly the by-value convention the C ABI documents.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct CHandle {
|
||||
/// Opaque box pointer.
|
||||
pub ctx: *mut std::ffi::c_void,
|
||||
/// Atomic increment.
|
||||
pub addref: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
|
||||
/// Atomic decrement; destroys at zero.
|
||||
pub release: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
|
||||
/// ABI version.
|
||||
pub abi_version: u32,
|
||||
}
|
||||
|
||||
impl CHandle {
|
||||
/// The empty handle.
|
||||
pub fn null() -> Self {
|
||||
CHandle {
|
||||
ctx: ptr::null_mut(),
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: OAKCODEC_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this is an empty (null) handle.
|
||||
pub fn is_null(&self) -> bool {
|
||||
self.ctx.is_null()
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment the reference count of a boxed `RefBox<T>`.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must point to a live `RefBox<T>` previously created by this module.
|
||||
unsafe extern "C" fn box_addref<T: Send + 'static>(ptr: *mut std::ffi::c_void) {
|
||||
if ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
let boxed = unsafe { &*(ptr as *const RefBox<T>) };
|
||||
boxed.refs.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Decrement the reference count; destroys the box at zero.
|
||||
///
|
||||
/// # Safety
|
||||
/// `ptr` must point to a live `RefBox<T>` previously created by this module.
|
||||
unsafe extern "C" fn box_release<T: Send + 'static>(ptr: *mut std::ffi::c_void) {
|
||||
if ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
let boxed = unsafe { &*(ptr as *const RefBox<T>) };
|
||||
if boxed.refs.fetch_sub(1, Ordering::SeqCst) == 1 {
|
||||
// The last reference: the box is destroyed and the alive count
|
||||
// drops with it (mirrors `alive_dec` in c_api/frame.cpp).
|
||||
ALIVE.fetch_sub(1, Ordering::SeqCst);
|
||||
unsafe { drop(Box::from_raw(ptr as *mut RefBox<T>)) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Owned handle with count 1; empty on allocation failure.
|
||||
pub fn make_owned<T: Send + 'static>(value: T) -> CHandle {
|
||||
let boxed = Box::new(RefBox {
|
||||
refs: AtomicU32::new(1),
|
||||
value,
|
||||
});
|
||||
let ctx = Box::into_raw(boxed) as *mut std::ffi::c_void;
|
||||
// Every boxed handle counts toward `oakcodec_debug_alive_count`
|
||||
// (mirrors `alive_inc` in c_api/frame.cpp).
|
||||
ALIVE.fetch_add(1, Ordering::SeqCst);
|
||||
CHandle {
|
||||
ctx,
|
||||
addref: Some(box_addref::<T>),
|
||||
release: Some(box_release::<T>),
|
||||
abi_version: OAKCODEC_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrowed handle for an object owned elsewhere.
|
||||
///
|
||||
/// Takes ownership of the boxed `T` already allocated at `ptr` (e.g. one
|
||||
/// passed in from C++). The resulting handle's release drops that box.
|
||||
///
|
||||
/// # Safety
|
||||
/// Caller guarantees `ptr` was allocated with `Box::new` and is not used
|
||||
/// after this call.
|
||||
pub unsafe fn make_borrowed<T: Send + 'static>(ptr: *mut T) -> CHandle {
|
||||
if ptr.is_null() {
|
||||
return CHandle::null();
|
||||
}
|
||||
// Move ownership into a RefBox so addref/release and get() behave
|
||||
// uniformly with owned handles.
|
||||
let value = unsafe { *Box::from_raw(ptr) };
|
||||
make_owned(value)
|
||||
}
|
||||
|
||||
/// Typed view into a handle; `None` for empty handles.
|
||||
///
|
||||
/// # Safety
|
||||
/// `T` must be the boxed type.
|
||||
pub unsafe fn get<T: 'static>(h: &CHandle) -> Option<&T> {
|
||||
if h.is_null() {
|
||||
return None;
|
||||
}
|
||||
let boxed = unsafe { &*(h.ctx as *const RefBox<T>) };
|
||||
Some(&boxed.value)
|
||||
}
|
||||
|
||||
/// Panic-catching FFI wrapper for i32-returning exports.
|
||||
pub fn guard<F: FnOnce() -> error::Result<()>>(f: F) -> i32 {
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(Ok(())) => error::OAKCODEC_OK,
|
||||
Ok(Err(e)) => e.code(),
|
||||
Err(_) => OAKCODEC_E_FAILED,
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic-catching FFI wrapper for handle-returning exports.
|
||||
pub fn guard_handle<F: FnOnce() -> error::Result<CHandle>>(f: F) -> CHandle {
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(Ok(h)) => h,
|
||||
Ok(Err(_)) | Err(_) => CHandle::null(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic-catching FFI wrapper for void exports.
|
||||
pub fn guard_void<F: FnOnce()>(f: F) {
|
||||
let _ = catch_unwind(AssertUnwindSafe(f));
|
||||
}
|
||||
|
||||
/// Panic-catching FFI wrapper for exports that return a raw `i32` code
|
||||
/// directly (neither `Result` nor a handle). On panic, `OAKCODEC_E_FAILED`.
|
||||
pub fn guard_raw<F: FnOnce() -> i32>(f: F) -> i32 {
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(code) => code,
|
||||
Err(_) => OAKCODEC_E_FAILED,
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic-catching FFI wrapper for exports that return a raw `i64` directly
|
||||
/// (e.g. `oakcodec_decoder_get_image_sequence_index`). On panic,
|
||||
/// `OAKCODEC_E_FAILED`.
|
||||
pub fn guard_i64<F: FnOnce() -> i64>(f: F) -> i64 {
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(v) => v,
|
||||
Err(_) => OAKCODEC_E_FAILED as i64,
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of live boxed handle objects (see [`ALIVE`]).
|
||||
pub fn alive_count() -> i32 {
|
||||
ALIVE.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::OAKCODEC_E_INVALID;
|
||||
|
||||
#[test]
|
||||
fn make_owned_lifecycle_tracks_alive_count() {
|
||||
// The shared ffi test lock serializes the crate's `alive_count`
|
||||
// assertions against every other test that creates handles.
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let before = alive_count();
|
||||
let h = make_owned(42u32);
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(alive_count(), before + 1);
|
||||
|
||||
// addref/release cycle keeps the box alive.
|
||||
let addref = h.addref.unwrap();
|
||||
let release = h.release.unwrap();
|
||||
// SAFETY: `h.ctx` is a live RefBox<u32>.
|
||||
unsafe { addref(h.ctx) };
|
||||
// SAFETY: second reference released; box stays (refs 2 -> 1).
|
||||
unsafe { release(h.ctx) };
|
||||
assert_eq!(alive_count(), before + 1);
|
||||
|
||||
// Release the owned reference: box destroyed.
|
||||
// SAFETY: last reference.
|
||||
unsafe { release(h.ctx) };
|
||||
assert_eq!(alive_count(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_borrowed_null_is_null_handle() {
|
||||
let h = unsafe { make_borrowed::<u32>(std::ptr::null_mut()) };
|
||||
assert!(h.is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn make_borrowed_takes_ownership() {
|
||||
let _g = crate::ffi::lock_tests();
|
||||
let before = alive_count();
|
||||
let raw = Box::into_raw(Box::new(7u32));
|
||||
let h = unsafe { make_borrowed(raw) };
|
||||
assert!(!h.is_null());
|
||||
assert_eq!(alive_count(), before + 1);
|
||||
assert_eq!(unsafe { *get::<u32>(&h).unwrap() }, 7);
|
||||
unsafe { h.release.unwrap()(h.ctx) };
|
||||
assert_eq!(alive_count(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn addref_on_null_ctx_is_noop() {
|
||||
// A handle with function pointers but a null ctx: both thunks no-op.
|
||||
let h = CHandle {
|
||||
ctx: std::ptr::null_mut(),
|
||||
addref: Some(box_addref::<u32>),
|
||||
release: Some(box_release::<u32>),
|
||||
abi_version: OAKCODEC_ABI_VERSION,
|
||||
};
|
||||
// SAFETY: ctx is null; the thunks guard on it.
|
||||
unsafe { h.addref.unwrap()(h.ctx) };
|
||||
// SAFETY: ctx is null; the thunks guard on it.
|
||||
unsafe { h.release.unwrap()(h.ctx) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guard_maps_results_and_panics() {
|
||||
assert_eq!(guard(|| Ok(())), crate::error::OAKCODEC_OK);
|
||||
assert_eq!(guard(|| Err(crate::error::Error::Invalid)), OAKCODEC_E_INVALID);
|
||||
assert_eq!(guard(|| panic!("boom")), crate::error::OAKCODEC_E_FAILED);
|
||||
|
||||
let ok = guard_handle(|| Ok(make_owned(1u32)));
|
||||
assert!(!ok.is_null());
|
||||
assert!(guard_handle(|| Err::<CHandle, _>(crate::error::Error::Invalid)).is_null());
|
||||
assert!(guard_handle(|| panic!("boom")).is_null());
|
||||
|
||||
assert_eq!(guard_raw(|| 5), 5);
|
||||
assert_eq!(guard_raw(|| panic!("boom")), crate::error::OAKCODEC_E_FAILED);
|
||||
|
||||
assert_eq!(guard_i64(|| 5), 5);
|
||||
assert_eq!(guard_i64(|| panic!("boom")), crate::error::OAKCODEC_E_FAILED as i64);
|
||||
|
||||
let mut called = false;
|
||||
guard_void(|| called = true);
|
||||
assert!(called);
|
||||
guard_void(|| panic!("boom"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_handle_helpers() {
|
||||
let h = CHandle::null();
|
||||
assert!(h.is_null());
|
||||
assert_eq!(h.abi_version, OAKCODEC_ABI_VERSION);
|
||||
}
|
||||
}
|
||||
@@ -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/>.
|
||||
|
||||
//! # oakcodec — the media codec module (Rust)
|
||||
//!
|
||||
//! Reimplements the C++ oakcodec module behind its frozen C ABI
|
||||
//! (`include/codec/*.h`). See README.md for the architectural mapping
|
||||
//! (inheritance → traits, shared_ptr → refcounted handles, etc.).
|
||||
//!
|
||||
//! ## FFI discipline
|
||||
//!
|
||||
//! Identical to the oaknode/oakplugin crates: every export goes through
|
||||
//! [`handle::guard*`], handles are opaque refcounted boxes, shared
|
||||
//! state behind `Mutex`.
|
||||
|
||||
#![deny(unsafe_op_in_unsafe_fn)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod bridge;
|
||||
pub mod conformmanager;
|
||||
pub mod decoder;
|
||||
pub mod encoder;
|
||||
pub mod encodingparams;
|
||||
pub mod error;
|
||||
pub mod exportcodec;
|
||||
pub mod exportformat;
|
||||
pub mod ffmpeg;
|
||||
pub mod ffi;
|
||||
pub mod footagedescription;
|
||||
pub mod frame;
|
||||
pub mod framemanager;
|
||||
pub mod handle;
|
||||
pub mod oiio;
|
||||
pub mod oiioframebridge;
|
||||
pub mod planarfiledevice;
|
||||
pub mod proxymanager;
|
||||
pub mod task;
|
||||
pub mod timecodemetadata;
|
||||
|
||||
#[cfg(test)]
|
||||
mod realmedia_tests;
|
||||
@@ -0,0 +1,307 @@
|
||||
// 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/>.
|
||||
|
||||
//! `OIIODecoder` / `OIIOEncoder` — the OpenImageIO-backed implementations.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/oiio/{oiiodecoder,oiioencoder}.{h,cpp}`. OIIO
|
||||
//! frame conversion goes through the local
|
||||
//! [`crate::oiioframebridge`] helpers plus oakcommon's OIIO mapping
|
||||
//! functions (`oakcommon_oiioutils_*` via `bridge/common.rs`).
|
||||
//!
|
||||
//! The OIIO dylib (`liboakoiio`) is not linked into this build, so every
|
||||
//! operation that would touch the media engine is a documented stub returning
|
||||
//! [`crate::error::Error::Failed`]; only bookkeeping that keeps the
|
||||
//! decoder/encoder safe to use when closed succeeds. The stream is still
|
||||
//! recorded on [`Decoder::open`] so [`Decoder::stream`] reflects the target,
|
||||
//! and [`Decoder::close`] / [`Encoder::close`] are no-ops.
|
||||
|
||||
use crate::decoder::{CodecStream, Decoder, RetrieveVideoParams};
|
||||
use crate::encoder::Encoder;
|
||||
use crate::encodingparams::EncodingParams;
|
||||
|
||||
/// `olive::OIIODecoder` — OpenImageIO-backed media decoder (still images).
|
||||
pub struct OIIODecoder {
|
||||
/// Opened stream (locked).
|
||||
stream: std::sync::Mutex<Option<CodecStream>>,
|
||||
}
|
||||
|
||||
impl OIIODecoder {
|
||||
/// Error returned for operations that need the missing `liboakoiio`.
|
||||
const NOT_AVAILABLE: &'static str =
|
||||
"OIIO decoding is not available in this build (needs liboakoiio dylib)";
|
||||
}
|
||||
|
||||
impl Decoder for OIIODecoder {
|
||||
fn id(&self) -> String {
|
||||
"oiio".to_string()
|
||||
}
|
||||
|
||||
fn supports_video(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn supports_audio(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn probe(
|
||||
&self,
|
||||
_filename: &str,
|
||||
_cancelled: Option<&crate::bridge::render::OakCancelAtom>,
|
||||
) -> Option<crate::footagedescription::FootageDescription> {
|
||||
// Probing is a dylib operation; without it we cannot report anything.
|
||||
None
|
||||
}
|
||||
|
||||
fn open(&self, stream: &CodecStream) -> crate::error::Result<()> {
|
||||
// Record the requested stream so `stream()` reflects the open target,
|
||||
// but actual decoding is unavailable without the dylib.
|
||||
*self.stream.lock().unwrap_or_else(|e| e.into_inner()) = Some(stream.clone());
|
||||
Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))
|
||||
}
|
||||
|
||||
fn close(&self) -> crate::error::Result<()> {
|
||||
// Trait contract: "safe when closed". Clear the opened stream and
|
||||
// no-op; nothing was ever decoded.
|
||||
*self.stream.lock().unwrap_or_else(|e| e.into_inner()) = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stream(&self) -> CodecStream {
|
||||
self.stream
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.clone()
|
||||
.unwrap_or_else(CodecStream::new)
|
||||
}
|
||||
|
||||
fn retrieve_video_frame(
|
||||
&self,
|
||||
_p: &RetrieveVideoParams,
|
||||
) -> crate::error::Result<std::sync::Arc<crate::frame::Frame>> {
|
||||
Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))
|
||||
}
|
||||
|
||||
fn retrieve_video(
|
||||
&self,
|
||||
_p: &RetrieveVideoParams,
|
||||
) -> crate::error::Result<crate::bridge::render::OakRenderTexture> {
|
||||
Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))
|
||||
}
|
||||
|
||||
fn retrieve_audio(
|
||||
&self,
|
||||
_dest: &mut [f32],
|
||||
_range: &oakcore_rs::TimeRange,
|
||||
_sample_rate: i32,
|
||||
_channel_layout: u64,
|
||||
) -> crate::error::Result<crate::decoder::RetrieveAudioStatus> {
|
||||
Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))
|
||||
}
|
||||
|
||||
fn conform_audio(
|
||||
&self,
|
||||
_output_filenames: &[String],
|
||||
_sample_rate: i32,
|
||||
_channel_layout: u64,
|
||||
_sample_format: i32,
|
||||
_cancelled: Option<&crate::bridge::render::OakCancelAtom>,
|
||||
) -> crate::error::Result<()> {
|
||||
Err(crate::error::Error::Failed(Self::NOT_AVAILABLE.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// `olive::OIIOEncoder` — OpenImageIO-backed media encoder (still images).
|
||||
pub struct OIIOEncoder {
|
||||
/// The encoding parameters this encoder was configured with.
|
||||
pub params: EncodingParams,
|
||||
}
|
||||
|
||||
impl Encoder for OIIOEncoder {
|
||||
fn id(&self) -> String {
|
||||
"oiio".to_string()
|
||||
}
|
||||
|
||||
fn supports_video(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn supports_audio(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn supports_subtitles(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn supports_image_sequences(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_configurable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn configure(&self, _params: &EncodingParams) -> crate::error::Result<()> {
|
||||
// `configure` writes `self.params`, which an `&self` receiver cannot
|
||||
// do, and real encoding needs the dylib anyway.
|
||||
Err(crate::error::Error::Failed(
|
||||
"OIIO encoding is not available in this build (needs liboakoiio dylib)".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn open(&self) -> crate::error::Result<()> {
|
||||
Err(crate::error::Error::Failed(
|
||||
"OIIO encoding is not available in this build (needs liboakoiio dylib)".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn close(&self) -> crate::error::Result<()> {
|
||||
// `Encoder::close` is documented idempotent; with no encoder opened
|
||||
// there is nothing to release.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_video(&self, _frame: &crate::frame::Frame) -> crate::error::Result<()> {
|
||||
Err(crate::error::Error::Failed(
|
||||
"OIIO encoding is not available in this build (needs liboakoiio dylib)".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn write_audio(&self, _samples: &[f32], _frame_count: i32) -> crate::error::Result<()> {
|
||||
Err(crate::error::Error::Failed(
|
||||
"OIIO encoding is not available in this build (needs liboakoiio dylib)".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn write_subtitle(
|
||||
&self,
|
||||
_text: &str,
|
||||
_in_seconds: f64,
|
||||
_out_seconds: f64,
|
||||
) -> crate::error::Result<()> {
|
||||
Err(crate::error::Error::Failed(
|
||||
"OIIO encoding is not available in this build (needs liboakoiio dylib)".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn flush(&self) -> crate::error::Result<()> {
|
||||
Err(crate::error::Error::Failed(
|
||||
"OIIO encoding is not available in this build (needs liboakoiio dylib)".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn desired_pixel_format(&self) -> Option<oakcore_rs::PixelFormat> {
|
||||
None
|
||||
}
|
||||
|
||||
fn desired_sample_format(&self) -> Option<oakcore_rs::SampleFormat> {
|
||||
None
|
||||
}
|
||||
|
||||
fn filename(&self) -> String {
|
||||
c_string_1024(&self.params.filename)
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a NUL-terminated C string out of the `EncodingParams.filename` byte
|
||||
/// buffer, stopping at the first NUL (empty string when unset).
|
||||
fn c_string_1024(buf: &[u8; 1024]) -> String {
|
||||
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
|
||||
String::from_utf8_lossy(&buf[..end]).into_owned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::decoder::RetrieveVideoParams;
|
||||
use crate::encodingparams::EncodingParams;
|
||||
use oakcore_rs::{Rational, TimeRange};
|
||||
|
||||
fn video_params() -> RetrieveVideoParams {
|
||||
RetrieveVideoParams {
|
||||
stream: CodecStream::new(),
|
||||
time: Rational::new(1, 30),
|
||||
length: TimeRange::default(),
|
||||
force_range: crate::decoder::K_COLOR_RANGE_DEFAULT,
|
||||
is_image_sequence: false,
|
||||
image_sequence_digits: 0,
|
||||
image_sequence_number: 0,
|
||||
mode: crate::decoder::RenderMode::Offline,
|
||||
alpha_is_premultiplied: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oiio_decoder_identity_and_stub_operations() {
|
||||
let d = OIIODecoder {
|
||||
stream: std::sync::Mutex::new(None),
|
||||
};
|
||||
assert_eq!(d.id(), "oiio");
|
||||
// Still images only: no audio support.
|
||||
assert!(!d.supports_video());
|
||||
assert!(!d.supports_audio());
|
||||
assert!(d.probe("any.png", None).is_none());
|
||||
|
||||
let s = CodecStream::with_block("in.exr".to_string(), 0, None);
|
||||
assert!(d.open(&s).is_err());
|
||||
assert_eq!(d.stream().filename(), "in.exr");
|
||||
assert!(d.close().is_ok());
|
||||
assert_eq!(d.stream().filename(), "");
|
||||
|
||||
assert!(d.retrieve_video_frame(&video_params()).is_err());
|
||||
assert!(d.retrieve_video(&video_params()).is_err());
|
||||
let mut dest = [0f32; 8];
|
||||
assert!(d
|
||||
.retrieve_audio(
|
||||
&mut dest,
|
||||
&TimeRange::new(Rational::new(0, 1), Rational::new(1, 1)),
|
||||
48000,
|
||||
0x3
|
||||
)
|
||||
.is_err());
|
||||
assert!(d
|
||||
.conform_audio(&["a.pcm".to_string()], 48000, 0x3, 10, None)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oiio_encoder_stub_behaviors() {
|
||||
let mut params = EncodingParams::default();
|
||||
let name = b"out/img.exr";
|
||||
params.filename[..name.len()].copy_from_slice(name);
|
||||
let e = OIIOEncoder { params };
|
||||
|
||||
assert_eq!(e.id(), "oiio");
|
||||
assert!(!e.supports_video());
|
||||
assert!(!e.supports_audio());
|
||||
assert!(!e.supports_subtitles());
|
||||
assert!(e.supports_image_sequences());
|
||||
assert!(e.is_configurable());
|
||||
assert_eq!(e.filename(), "out/img.exr");
|
||||
assert_eq!(e.desired_pixel_format(), None);
|
||||
assert_eq!(e.get_error(), "");
|
||||
|
||||
assert!(e.configure(&EncodingParams::default()).is_err());
|
||||
assert!(e.open().is_err());
|
||||
assert!(e.close().is_ok());
|
||||
let frame = crate::frame::Frame::new();
|
||||
assert!(e.write_video(&frame).is_err());
|
||||
assert!(e.write_audio(&[0f32; 4], 1).is_err());
|
||||
assert!(e.write_subtitle("hi", 0.0, 1.0).is_err());
|
||||
assert!(e.flush().is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
// 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/>.
|
||||
|
||||
//! `oiio_frame_to_buffer` / `oiio_buffer_to_frame` — codec-internal OIIO
|
||||
//! frame <-> pixel-buffer conversion.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/oiioframebridge.{h,cpp}`. These are internal C++
|
||||
//! functions that moved into codec from oakcommon (NOTES.md §oakcommon侧修复);
|
||||
//! oakcommon keeps its OIIO mapping functions; the frame conversion itself
|
||||
//! lives here.
|
||||
//!
|
||||
//! The C++ bridge copies pixels through the live OpenImageIO `ImageBuf`
|
||||
//! (`oiio_frame_to_buffer`/`oiio_buffer_to_frame`). OIIO is not linked into
|
||||
//! this build, so the port serializes the frame into a self-describing byte
|
||||
//! buffer instead. The layout is stable (documented in
|
||||
//! [`OiioBufferHeader`]); it carries the frame's geometry, pixel format,
|
||||
//! timestamp and time base alongside the raw pixel rows, so a buffer can be
|
||||
//! turned back into an equivalent [`Frame`] without any external state.
|
||||
|
||||
use crate::bridge::common::{
|
||||
oakcommon_videoparams_get_time_base, oakcommon_videoparams_init_with_time_base,
|
||||
oakcommon_videoparams_set_format,
|
||||
};
|
||||
use crate::frame::Frame;
|
||||
use oakcore_rs::Rational;
|
||||
|
||||
/// Fixed header size, in bytes, of an OIIO frame buffer.
|
||||
///
|
||||
/// The layout is a 64-byte little-endian header followed by the raw pixel
|
||||
/// data. `OiioBufferHeader::to_bytes` / `from_bytes` are the single writer /
|
||||
/// reader of this header, so the exact offsets only ever exist in one place.
|
||||
const HEADER_LEN: usize = 64;
|
||||
|
||||
/// Magic bytes identifying an OIIO frame buffer (`"OFMB"`).
|
||||
const MAGIC: &[u8; 4] = b"OFMB";
|
||||
|
||||
/// Current serialization version.
|
||||
const VERSION: u32 = 1;
|
||||
|
||||
/// Decoded OIIO frame-buffer header.
|
||||
///
|
||||
/// # Byte layout (little-endian, `HEADER_LEN` = 64 bytes)
|
||||
///
|
||||
/// | Offset | Size | Field |
|
||||
/// |--------|------|-------|
|
||||
/// | 0 | 4 | Magic bytes `"OFMB"` |
|
||||
/// | 4 | 4 | Serialization `version` (`u32`, currently 1) |
|
||||
/// | 8 | 4 | `width` (`i32`) |
|
||||
/// | 12 | 4 | `height` (`i32`) |
|
||||
/// | 16 | 4 | `format` (`i32`, an `OakPixelFormat` value) |
|
||||
/// | 20 | 4 | `linesize_bytes` (`i32`, distance between pixel rows) |
|
||||
/// | 24 | 8 | `timestamp_num` (`i64`) |
|
||||
/// | 32 | 8 | `timestamp_den` (`i64`) |
|
||||
/// | 40 | 8 | `time_base_num` (`i64`) |
|
||||
/// | 48 | 8 | `time_base_den` (`i64`) |
|
||||
/// | 56 | 8 | `pixel_len` (`u64`, pixel-data length in bytes) |
|
||||
/// | 64 | … | raw pixel data (`pixel_len` bytes) |
|
||||
///
|
||||
/// `pixel_len` must equal `linesize_bytes * height`; the frame geometry in the
|
||||
/// header must match the params the frame is reconstructed with.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct OiioBufferHeader {
|
||||
version: u32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
format: i32,
|
||||
linesize_bytes: i32,
|
||||
timestamp_num: i64,
|
||||
timestamp_den: i64,
|
||||
time_base_num: i64,
|
||||
time_base_den: i64,
|
||||
pixel_len: u64,
|
||||
}
|
||||
|
||||
impl OiioBufferHeader {
|
||||
/// Serialize the header into exactly `HEADER_LEN` bytes (little-endian).
|
||||
fn to_bytes(&self) -> [u8; HEADER_LEN] {
|
||||
let mut b = [0u8; HEADER_LEN];
|
||||
b[0..4].copy_from_slice(MAGIC);
|
||||
b[4..8].copy_from_slice(&self.version.to_le_bytes());
|
||||
b[8..12].copy_from_slice(&self.width.to_le_bytes());
|
||||
b[12..16].copy_from_slice(&self.height.to_le_bytes());
|
||||
b[16..20].copy_from_slice(&self.format.to_le_bytes());
|
||||
b[20..24].copy_from_slice(&self.linesize_bytes.to_le_bytes());
|
||||
b[24..32].copy_from_slice(&self.timestamp_num.to_le_bytes());
|
||||
b[32..40].copy_from_slice(&self.timestamp_den.to_le_bytes());
|
||||
b[40..48].copy_from_slice(&self.time_base_num.to_le_bytes());
|
||||
b[48..56].copy_from_slice(&self.time_base_den.to_le_bytes());
|
||||
b[56..64].copy_from_slice(&self.pixel_len.to_le_bytes());
|
||||
b
|
||||
}
|
||||
|
||||
/// Parse a header from the start of `bytes`, validating the magic and
|
||||
/// version and requiring at least `HEADER_LEN` bytes. Returns the decoded
|
||||
/// header and the number of bytes consumed (`HEADER_LEN`).
|
||||
fn from_bytes(bytes: &[u8]) -> crate::error::Result<(Self, usize)> {
|
||||
if bytes.len() < HEADER_LEN {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
if &bytes[0..4] != MAGIC {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let version = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
|
||||
if version != VERSION {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let header = OiioBufferHeader {
|
||||
version,
|
||||
width: i32::from_le_bytes(bytes[8..12].try_into().unwrap()),
|
||||
height: i32::from_le_bytes(bytes[12..16].try_into().unwrap()),
|
||||
format: i32::from_le_bytes(bytes[16..20].try_into().unwrap()),
|
||||
linesize_bytes: i32::from_le_bytes(bytes[20..24].try_into().unwrap()),
|
||||
timestamp_num: i64::from_le_bytes(bytes[24..32].try_into().unwrap()),
|
||||
timestamp_den: i64::from_le_bytes(bytes[32..40].try_into().unwrap()),
|
||||
time_base_num: i64::from_le_bytes(bytes[40..48].try_into().unwrap()),
|
||||
time_base_den: i64::from_le_bytes(bytes[48..56].try_into().unwrap()),
|
||||
pixel_len: u64::from_le_bytes(bytes[56..64].try_into().unwrap()),
|
||||
};
|
||||
Ok((header, HEADER_LEN))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an OIIO-backed `Frame` into a raw pixel buffer.
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// `src/codec/src/oiioframebridge.cpp` `oiio_frame_to_buffer` —
|
||||
/// allocates the destination and copies the OIIO pixel data out.
|
||||
///
|
||||
/// The buffer is a 64-byte [`OiioBufferHeader`] followed by the frame's pixel
|
||||
/// rows (see the header docs for the exact layout). The frame must already be
|
||||
/// allocated ([`Frame::allocate`]); otherwise this returns
|
||||
/// [`crate::error::Error::State`].
|
||||
pub fn oiio_frame_to_buffer(frame: &Frame) -> crate::error::Result<Vec<u8>> {
|
||||
let data = frame.data().ok_or(crate::error::Error::State)?;
|
||||
let width = frame.width();
|
||||
let height = frame.height();
|
||||
let format = frame.format() as i32;
|
||||
let linesize_bytes = frame.linesize_bytes();
|
||||
let timestamp = frame.timestamp();
|
||||
|
||||
let (time_base_num, time_base_den) = match frame.params() {
|
||||
Some(p) => {
|
||||
let mut num = 0i64;
|
||||
let mut den = 0i64;
|
||||
// SAFETY: `num`/`den` are live mutable i64s and `p` is a valid
|
||||
// handle; the C function only writes through the two out pointers.
|
||||
unsafe {
|
||||
oakcommon_videoparams_get_time_base(p.clone(), &mut num, &mut den);
|
||||
}
|
||||
(num, den)
|
||||
}
|
||||
None => (0, 0),
|
||||
};
|
||||
|
||||
let header = OiioBufferHeader {
|
||||
version: VERSION,
|
||||
width,
|
||||
height,
|
||||
format,
|
||||
linesize_bytes,
|
||||
timestamp_num: timestamp.numerator(),
|
||||
timestamp_den: timestamp.denominator(),
|
||||
time_base_num,
|
||||
time_base_den,
|
||||
pixel_len: data.len() as u64,
|
||||
};
|
||||
|
||||
let mut out = Vec::with_capacity(HEADER_LEN + data.len());
|
||||
out.extend_from_slice(&header.to_bytes());
|
||||
out.extend_from_slice(data);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Convert a raw pixel buffer back into a `Frame`.
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// `src/codec/src/oiioframebridge.cpp` `oiio_buffer_to_frame` —
|
||||
/// wraps the buffer bytes in a `Frame` for encoder consumption.
|
||||
///
|
||||
/// Parses a [`OiioBufferHeader`] from the front of `buffer`, reconstructs the
|
||||
/// frame's params (geometry, time base, pixel format) and timestamp, then
|
||||
/// copies the pixel rows into an allocated frame. The buffer is rejected with
|
||||
/// [`crate::error::Error::Invalid`] when the magic/version is wrong, the
|
||||
/// header is truncated, or `pixel_len` is inconsistent with the declared
|
||||
/// `linesize_bytes * height` / the bytes actually present.
|
||||
pub fn oiio_buffer_to_frame(buffer: &[u8]) -> crate::error::Result<Frame> {
|
||||
let (header, consumed) = OiioBufferHeader::from_bytes(buffer)?;
|
||||
let pixels = &buffer[consumed..];
|
||||
|
||||
// `pixel_len` must be consistent with the header geometry and the bytes
|
||||
// actually present (checked, so a corrupt header can't panic later).
|
||||
if header.linesize_bytes < 0 || header.height < 0 {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
let expected =
|
||||
(header.linesize_bytes as u64).checked_mul(header.height as u64);
|
||||
if expected != Some(header.pixel_len) {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
if pixels.len() != header.pixel_len as usize {
|
||||
return Err(crate::error::Error::Invalid);
|
||||
}
|
||||
|
||||
// Build the params from the header, then hand ownership to the frame.
|
||||
// SAFETY: the init returns a live handle; the clone for `set_format` is
|
||||
// only read, and the original is moved into `Frame::with_params` (which
|
||||
// takes ownership), so there is no double release.
|
||||
let params = unsafe {
|
||||
oakcommon_videoparams_init_with_time_base(
|
||||
header.width,
|
||||
header.height,
|
||||
header.time_base_num,
|
||||
header.time_base_den,
|
||||
)
|
||||
};
|
||||
unsafe {
|
||||
oakcommon_videoparams_set_format(params.clone(), header.format);
|
||||
}
|
||||
let mut frame = Frame::with_params(params);
|
||||
frame.set_timestamp(Rational::new(header.timestamp_num, header.timestamp_den));
|
||||
frame.allocate()?;
|
||||
|
||||
match frame.data_mut() {
|
||||
Some(dst) if dst.len() == pixels.len() => {
|
||||
dst.copy_from_slice(pixels);
|
||||
Ok(frame)
|
||||
}
|
||||
// Reconstructed params should produce exactly `pixel_len` bytes; if
|
||||
// the geometry in the header disagreed with the format's line size,
|
||||
// refuse rather than copy a mismatched slice.
|
||||
_ => Err(crate::error::Error::Invalid),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn header_round_trip() {
|
||||
let header = OiioBufferHeader {
|
||||
version: VERSION,
|
||||
width: 100,
|
||||
height: 50,
|
||||
format: 0, // U8
|
||||
linesize_bytes: 512,
|
||||
timestamp_num: 5,
|
||||
timestamp_den: 2,
|
||||
time_base_num: 1,
|
||||
time_base_den: 30,
|
||||
pixel_len: 25600,
|
||||
};
|
||||
let bytes = header.to_bytes();
|
||||
assert_eq!(bytes.len(), HEADER_LEN);
|
||||
let (decoded, consumed) = OiioBufferHeader::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(consumed, HEADER_LEN);
|
||||
assert_eq!(decoded, header);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_is_little_endian_magic_and_version() {
|
||||
let header = OiioBufferHeader {
|
||||
version: VERSION,
|
||||
width: 1,
|
||||
height: 1,
|
||||
format: 0,
|
||||
linesize_bytes: 4,
|
||||
timestamp_num: 0,
|
||||
timestamp_den: 1,
|
||||
time_base_num: 0,
|
||||
time_base_den: 0,
|
||||
pixel_len: 4,
|
||||
};
|
||||
let bytes = header.to_bytes();
|
||||
assert_eq!(&bytes[0..4], b"OFMB");
|
||||
assert_eq!(u32::from_le_bytes(bytes[4..8].try_into().unwrap()), 1);
|
||||
// width = 1 -> little-endian 01 00 00 00.
|
||||
assert_eq!(&bytes[8..12], &[1, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_bytes_rejects_truncated() {
|
||||
let header = OiioBufferHeader {
|
||||
version: VERSION,
|
||||
width: 1,
|
||||
height: 1,
|
||||
format: 0,
|
||||
linesize_bytes: 4,
|
||||
timestamp_num: 0,
|
||||
timestamp_den: 1,
|
||||
time_base_num: 0,
|
||||
time_base_den: 0,
|
||||
pixel_len: 4,
|
||||
};
|
||||
let bytes = header.to_bytes();
|
||||
assert!(OiioBufferHeader::from_bytes(&bytes[..HEADER_LEN - 1]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_bytes_rejects_bad_magic_and_version() {
|
||||
let mut bytes = OiioBufferHeader {
|
||||
version: VERSION,
|
||||
width: 1,
|
||||
height: 1,
|
||||
format: 0,
|
||||
linesize_bytes: 4,
|
||||
timestamp_num: 0,
|
||||
timestamp_den: 1,
|
||||
time_base_num: 0,
|
||||
time_base_den: 0,
|
||||
pixel_len: 4,
|
||||
}
|
||||
.to_bytes();
|
||||
bytes[0] = b'X';
|
||||
assert!(OiioBufferHeader::from_bytes(&bytes).is_err());
|
||||
|
||||
let mut bytes = OiioBufferHeader {
|
||||
version: VERSION,
|
||||
width: 1,
|
||||
height: 1,
|
||||
format: 0,
|
||||
linesize_bytes: 4,
|
||||
timestamp_num: 0,
|
||||
timestamp_den: 1,
|
||||
time_base_num: 0,
|
||||
time_base_den: 0,
|
||||
pixel_len: 4,
|
||||
}
|
||||
.to_bytes();
|
||||
bytes[4..8].copy_from_slice(&99u32.to_le_bytes());
|
||||
assert!(OiioBufferHeader::from_bytes(&bytes).is_err());
|
||||
}
|
||||
|
||||
/// A small helper to build a fully allocated, filled frame using the same
|
||||
/// test-stub params pattern as `frame.rs`.
|
||||
fn make_frame() -> Frame {
|
||||
// SAFETY: test-stub videoparams; ownership moves into `with_params`.
|
||||
let params = unsafe { oakcommon_videoparams_init_with_time_base(100, 50, 1, 30) };
|
||||
unsafe { oakcommon_videoparams_set_format(params.clone(), 0) }; // U8
|
||||
let mut frame = Frame::with_params(params);
|
||||
frame.set_timestamp(Rational::new(5, 2));
|
||||
frame.allocate().unwrap();
|
||||
frame
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_to_buffer_round_trip() {
|
||||
let mut frame = make_frame();
|
||||
if let Some(d) = frame.data_mut() {
|
||||
d.fill(0xAB);
|
||||
}
|
||||
|
||||
let buffer = oiio_frame_to_buffer(&frame).unwrap();
|
||||
assert_eq!(buffer.len(), HEADER_LEN + frame.allocated_size());
|
||||
|
||||
let (header, consumed) = OiioBufferHeader::from_bytes(&buffer).unwrap();
|
||||
assert_eq!(consumed, HEADER_LEN);
|
||||
assert_eq!(header.width, 100);
|
||||
assert_eq!(header.height, 50);
|
||||
assert_eq!(header.format, 0);
|
||||
assert_eq!(header.linesize_bytes, 512);
|
||||
assert_eq!(header.timestamp_num, 5);
|
||||
assert_eq!(header.timestamp_den, 2);
|
||||
assert_eq!(header.time_base_num, 1);
|
||||
assert_eq!(header.time_base_den, 30);
|
||||
assert_eq!(header.pixel_len, frame.allocated_size() as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_to_frame_round_trip() {
|
||||
let mut frame = make_frame();
|
||||
let expected_len = frame.allocated_size();
|
||||
if let Some(d) = frame.data_mut() {
|
||||
for (i, b) in d.iter_mut().enumerate() {
|
||||
*b = (i % 256) as u8;
|
||||
}
|
||||
}
|
||||
let buffer = oiio_frame_to_buffer(&frame).unwrap();
|
||||
|
||||
let out = oiio_buffer_to_frame(&buffer).unwrap();
|
||||
assert_eq!(out.width(), 100);
|
||||
assert_eq!(out.height(), 50);
|
||||
assert_eq!(out.format() as i32, 0);
|
||||
assert_eq!(out.allocated_size(), expected_len);
|
||||
assert_eq!(out.timestamp().numerator(), 5);
|
||||
assert_eq!(out.timestamp().denominator(), 2);
|
||||
assert_eq!(out.data().unwrap(), frame.data().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_to_frame_rejects_corrupt() {
|
||||
let frame = make_frame();
|
||||
let buffer = oiio_frame_to_buffer(&frame).unwrap();
|
||||
|
||||
// Truncated pixel data.
|
||||
assert!(oiio_buffer_to_frame(&buffer[..HEADER_LEN + 1]).is_err());
|
||||
// Bad magic.
|
||||
let mut bad = buffer.clone();
|
||||
bad[0] = 0;
|
||||
assert!(oiio_buffer_to_frame(&bad).is_err());
|
||||
// pixel_len inconsistent with linesize * height.
|
||||
let mut bad = buffer.clone();
|
||||
bad[56..64].copy_from_slice(&(999u64).to_le_bytes());
|
||||
assert!(oiio_buffer_to_frame(&bad).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// 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/>.
|
||||
|
||||
//! `olive::PlanarFileDevice` — planar (per-channel) file read/write.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/planarfiledevice.h`. Reads/writes one FILE* per
|
||||
//! channel so multi-channel audio is stored as one planar file per channel.
|
||||
//! `FILE*` implementation (the QIODevice-based original was replaced).
|
||||
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Open mode (replaces QIODevice::OpenMode).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum OpenMode {
|
||||
/// Read-only.
|
||||
ReadOnly = 0,
|
||||
/// Write-only.
|
||||
WriteOnly = 1,
|
||||
}
|
||||
|
||||
/// `olive::PlanarFileDevice` — one file per channel.
|
||||
pub struct PlanarFileDevice {
|
||||
/// Open file handles, one per channel.
|
||||
files: Vec<File>,
|
||||
/// Open mode.
|
||||
mode: OpenMode,
|
||||
}
|
||||
|
||||
impl PlanarFileDevice {
|
||||
/// New, closed device.
|
||||
pub fn new() -> Self {
|
||||
PlanarFileDevice {
|
||||
files: Vec::new(),
|
||||
mode: OpenMode::ReadOnly,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the device is open.
|
||||
pub fn is_open(&self) -> bool {
|
||||
!self.files.is_empty()
|
||||
}
|
||||
|
||||
/// Open `filenames` (one per channel) in `mode`. Returns false if already
|
||||
/// open or any file could not be opened (closing any opened so far).
|
||||
pub fn open(&mut self, filenames: &[PathBuf], mode: OpenMode) -> bool {
|
||||
if self.is_open() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut opened = Vec::with_capacity(filenames.len());
|
||||
for name in filenames {
|
||||
let mut opt = OpenOptions::new();
|
||||
let f = match mode {
|
||||
OpenMode::ReadOnly => opt.read(true).open(name),
|
||||
OpenMode::WriteOnly => opt.create(true).write(true).truncate(true).open(name),
|
||||
};
|
||||
match f {
|
||||
Ok(f) => opened.push(f),
|
||||
Err(_) => {
|
||||
// Roll back: close anything opened so far and report failure.
|
||||
self.files = opened;
|
||||
self.close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.files = opened;
|
||||
self.mode = mode;
|
||||
true
|
||||
}
|
||||
|
||||
/// Read `bytes_per_channel` bytes from each channel (at the current file
|
||||
/// position) into `data[i][offset..]`. Returns bytes read per channel, or
|
||||
/// -1 if closed or a buffer is too small.
|
||||
pub fn read(
|
||||
&mut self,
|
||||
data: &mut [&mut [u8]],
|
||||
bytes_per_channel: i64,
|
||||
offset: i64,
|
||||
) -> i64 {
|
||||
if !self.is_open() {
|
||||
return -1;
|
||||
}
|
||||
let bytes = bytes_per_channel as usize;
|
||||
let off = offset as usize;
|
||||
let mut ret = -1i64;
|
||||
for (i, f) in self.files.iter_mut().enumerate() {
|
||||
let buf = match data.get_mut(i) {
|
||||
Some(b) if b.len() >= off + bytes => &mut b[off..off + bytes],
|
||||
_ => return -1,
|
||||
};
|
||||
ret = f.read(buf).unwrap_or(0) as i64;
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
/// Write `bytes_per_channel` bytes to each channel from `data[i][offset..]`.
|
||||
/// Returns bytes written per channel, or -1.
|
||||
pub fn write(
|
||||
&mut self,
|
||||
data: &[&[u8]],
|
||||
bytes_per_channel: i64,
|
||||
offset: i64,
|
||||
) -> i64 {
|
||||
if !self.is_open() {
|
||||
return -1;
|
||||
}
|
||||
let bytes = bytes_per_channel as usize;
|
||||
let off = offset as usize;
|
||||
let mut ret = -1i64;
|
||||
for (i, f) in self.files.iter_mut().enumerate() {
|
||||
let buf = match data.get(i) {
|
||||
Some(b) if b.len() >= off + bytes => &b[off..off + bytes],
|
||||
_ => return -1,
|
||||
};
|
||||
ret = f.write(buf).unwrap_or(0) as i64;
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
/// Total size in bytes of one channel (from the first open file).
|
||||
pub fn size(&self) -> i64 {
|
||||
if self.is_open() {
|
||||
if let Ok(meta) = self.files[0].metadata() {
|
||||
return meta.len() as i64;
|
||||
}
|
||||
}
|
||||
0
|
||||
}
|
||||
|
||||
/// Seek all channels to `pos`.
|
||||
pub fn seek(&mut self, pos: i64) -> bool {
|
||||
let mut ok = true;
|
||||
for f in self.files.iter_mut() {
|
||||
ok = f.seek(SeekFrom::Start(pos as u64)).is_ok() && ok;
|
||||
}
|
||||
ok
|
||||
}
|
||||
|
||||
/// Close all channels.
|
||||
pub fn close(&mut self) {
|
||||
self.files.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PlanarFileDevice {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn temp_dir(name: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"oakcodec_planar_{}_{}",
|
||||
name,
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
dir
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_device_is_closed() {
|
||||
let mut d = PlanarFileDevice::new();
|
||||
assert!(!d.is_open());
|
||||
assert_eq!(d.size(), 0);
|
||||
assert_eq!(d.read(&mut [&mut [0u8; 4]], 4, 0), -1);
|
||||
assert_eq!(d.write(&[&[0u8; 4]], 4, 0), -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_then_read_roundtrip() {
|
||||
let dir = temp_dir("rw");
|
||||
let names: Vec<PathBuf> = (0..2).map(|i| dir.join(format!("ch{}.pcm", i))).collect();
|
||||
|
||||
let mut d = PlanarFileDevice::new();
|
||||
assert!(d.open(&names, OpenMode::WriteOnly));
|
||||
assert!(d.is_open());
|
||||
assert_eq!(d.mode, OpenMode::WriteOnly);
|
||||
|
||||
// Two channels, 4 bytes each.
|
||||
let ch0 = [1u8, 2, 3, 4];
|
||||
let ch1 = [9u8, 8, 7, 6];
|
||||
assert_eq!(d.write(&[&ch0, &ch1], 4, 0), 4);
|
||||
assert_eq!(d.size(), 4);
|
||||
d.close();
|
||||
assert!(!d.is_open());
|
||||
|
||||
// A "wb" handle cannot be read back (matching the C++ fopen mode);
|
||||
// reopen read-only to verify the written bytes.
|
||||
let mut d = PlanarFileDevice::new();
|
||||
assert!(d.open(&names, OpenMode::ReadOnly));
|
||||
assert_eq!(d.size(), 4);
|
||||
let mut out0 = [0u8; 8];
|
||||
let mut out1 = [0u8; 8];
|
||||
assert_eq!(d.read(&mut [&mut out0[..], &mut out1[..]], 4, 0), 4);
|
||||
assert_eq!(&out0[..4], &ch0);
|
||||
assert_eq!(&out1[..4], &ch1);
|
||||
d.close();
|
||||
assert!(!d.is_open());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offset_writes_and_reads() {
|
||||
let dir = temp_dir("off");
|
||||
let names = vec![dir.join("ch.pcm")];
|
||||
let mut d = PlanarFileDevice::new();
|
||||
assert!(d.open(&names, OpenMode::WriteOnly));
|
||||
|
||||
// The offset is a *buffer* offset (C++ `data[i] + offset`); file
|
||||
// offsets go through `seek`. Seek to 4 then write 4 bytes, leaving
|
||||
// a 4-byte gap.
|
||||
let data = [7u8, 7, 7, 7];
|
||||
assert!(d.seek(4));
|
||||
assert_eq!(d.write(&[&data], 4, 0), 4);
|
||||
assert_eq!(d.size(), 8);
|
||||
|
||||
d.close();
|
||||
let mut d = PlanarFileDevice::new();
|
||||
assert!(d.open(&names, OpenMode::ReadOnly));
|
||||
let mut buf = [0u8; 8];
|
||||
assert!(d.seek(0));
|
||||
assert_eq!(d.read(&mut [&mut buf[..]], 8, 0), 8);
|
||||
assert_eq!(&buf[..4], &[0, 0, 0, 0]);
|
||||
assert_eq!(&buf[4..], &data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_rollback_on_missing_file_and_double_open() {
|
||||
let dir = temp_dir("roll");
|
||||
let ok = dir.join("ok.pcm");
|
||||
let missing = dir.join("missing.pcm");
|
||||
let mut d = PlanarFileDevice::new();
|
||||
assert!(!d.open(&[ok.clone(), missing], OpenMode::ReadOnly));
|
||||
assert!(!d.is_open());
|
||||
|
||||
// Already open -> refuse.
|
||||
assert!(d.open(&[ok.clone()], OpenMode::WriteOnly));
|
||||
assert!(!d.open(&[ok], OpenMode::WriteOnly));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_write_reject_small_buffers() {
|
||||
let dir = temp_dir("small");
|
||||
let names = vec![dir.join("ch.pcm")];
|
||||
let mut d = PlanarFileDevice::new();
|
||||
assert!(d.open(&names, OpenMode::WriteOnly));
|
||||
let data = [1u8; 8];
|
||||
assert_eq!(d.write(&[&data], 8, 0), 8);
|
||||
|
||||
// Buffer too small for the requested bytes.
|
||||
let mut small = [0u8; 4];
|
||||
assert_eq!(d.write(&[&small], 8, 0), -1);
|
||||
assert!(d.seek(0));
|
||||
assert_eq!(d.read(&mut [&mut small[..]], 8, 0), -1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,703 @@
|
||||
// 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/>.
|
||||
|
||||
//! `olive::ProxyManager` — proxy (low-res transcode) generation.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/proxymanager.h`. Stateless (NOTES.md): actual
|
||||
//! transcodes are delegated to the global task submit callback
|
||||
//! ([`crate::task`]); with no registrar, `get_or_start` reports the proxy
|
||||
//! as missing. `proxy_params_from_config` reads the oakcommon config C ABI
|
||||
//! with the compiled-in defaults as fallback (1280x720 / divider 1 / crf 23
|
||||
//! / "mp4" / "veryfast" / audio included).
|
||||
|
||||
use std::ffi::{c_char, CString};
|
||||
use std::path::Path;
|
||||
|
||||
/// Proxy state of a proxy file on disk.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum ProxyState {
|
||||
/// Missing (or NULL/empty/absent).
|
||||
Missing = 0,
|
||||
/// Generating.
|
||||
Generating = 1,
|
||||
/// Ready on disk.
|
||||
Ready = 2,
|
||||
/// Generation failed.
|
||||
Failed = 3,
|
||||
}
|
||||
|
||||
/// `olive::ProxyManager::ProxyParams` — mirror of `oakcodec_proxy_params`.
|
||||
#[derive(Clone, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct ProxyParams {
|
||||
/// Absolute target width (0 when divider-based).
|
||||
pub width: i32,
|
||||
/// Absolute target height (0 when divider-based).
|
||||
pub height: i32,
|
||||
/// Source resolution divider (1 = absolute width/height, 2/4/8).
|
||||
pub divider: i32,
|
||||
/// Proxy format version.
|
||||
pub version: i32,
|
||||
/// x264 crf.
|
||||
pub crf: i32,
|
||||
/// Include the audio track (1/0; C `int`).
|
||||
pub include_audio: i32,
|
||||
/// ffmpeg output container (e.g. "mp4").
|
||||
pub extension: [u8; 32],
|
||||
/// ffmpeg encoder preset (e.g. "veryfast").
|
||||
pub preset: [u8; 32],
|
||||
}
|
||||
|
||||
impl Default for ProxyParams {
|
||||
fn default() -> Self {
|
||||
ProxyParams {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
divider: 1,
|
||||
version: 1,
|
||||
crf: 23,
|
||||
include_audio: 1,
|
||||
extension: bytes32(b"mp4"),
|
||||
preset: bytes32(b"veryfast"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProxyParams {
|
||||
fn extension_str(&self) -> &str {
|
||||
cstr_slice(&self.extension)
|
||||
}
|
||||
|
||||
fn preset_str(&self) -> &str {
|
||||
cstr_slice(&self.preset)
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_result` — POD result of [`ProxyManager::get_or_start`];
|
||||
/// see `include/codec/proxy.h`.
|
||||
#[repr(C)]
|
||||
pub struct OakCodecProxyResult {
|
||||
/// `ProxyState` value.
|
||||
pub state: i32,
|
||||
/// Resulting proxy filename (may be empty).
|
||||
pub filename: [u8; 1024],
|
||||
}
|
||||
|
||||
/// `olive::ProxyManager` — stateless proxy query/generate manager.
|
||||
pub struct ProxyManager;
|
||||
|
||||
impl ProxyManager {
|
||||
/// The process-wide ProxyManager singleton.
|
||||
pub fn instance() -> &'static ProxyManager {
|
||||
static INSTANCE: ProxyManager = ProxyManager;
|
||||
&INSTANCE
|
||||
}
|
||||
|
||||
/// Compiled-in default proxy parameters (the `Default` values).
|
||||
pub fn proxy_params_default() -> ProxyParams {
|
||||
ProxyParams::default()
|
||||
}
|
||||
|
||||
/// Proxy parameters read from the oakcommon config, with the compiled-in
|
||||
/// defaults as fallback.
|
||||
///
|
||||
/// # CPP-PARITY
|
||||
/// `src/codec/src/proxymanager.h` `proxy_params_from_config` — reads
|
||||
/// ProxyWidth/ProxyHeight/ProxyDivider/ProxyCRF/ProxyPreset/
|
||||
/// ProxyIncludeAudio via `oakcommon_config_*`.
|
||||
pub fn proxy_params_from_config() -> ProxyParams {
|
||||
let mut p = ProxyParams::default();
|
||||
p.width = config_get_int("ProxyWidth", p.width);
|
||||
p.height = config_get_int("ProxyHeight", p.height);
|
||||
p.divider = config_get_int("ProxyDivider", p.divider);
|
||||
p.crf = config_get_int("ProxyCRF", p.crf);
|
||||
p.include_audio = if config_get_bool("ProxyIncludeAudio", p.include_audio) != 0 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if let Some(preset) = config_get_str("ProxyPreset") {
|
||||
if !preset.is_empty() {
|
||||
p.preset = bytes32(preset.as_bytes());
|
||||
}
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// State of a proxy file on disk.
|
||||
pub fn get_proxy_state(proxy_filename: &str) -> ProxyState {
|
||||
if proxy_filename.is_empty() {
|
||||
return ProxyState::Missing;
|
||||
}
|
||||
if Path::new(proxy_filename).exists() {
|
||||
return ProxyState::Ready;
|
||||
}
|
||||
let working = Self::get_working_filename(proxy_filename);
|
||||
if let Ok(w) = working {
|
||||
if Path::new(&w).exists() {
|
||||
return ProxyState::Generating;
|
||||
}
|
||||
}
|
||||
ProxyState::Missing
|
||||
}
|
||||
|
||||
/// Human-readable string for a proxy state.
|
||||
pub fn proxy_state_to_string(state: ProxyState) -> String {
|
||||
match state {
|
||||
ProxyState::Missing => "missing".to_string(),
|
||||
ProxyState::Generating => "generating".to_string(),
|
||||
ProxyState::Ready => "ready".to_string(),
|
||||
ProxyState::Failed => "failed".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Proxy directory for a project cache path.
|
||||
pub fn get_proxy_directory(cache_path: &str) -> crate::error::Result<String> {
|
||||
Ok(Path::new(cache_path)
|
||||
.join("proxy")
|
||||
.to_string_lossy()
|
||||
.into_owned())
|
||||
}
|
||||
|
||||
/// Deterministic proxy filename for a source stream.
|
||||
pub fn get_proxy_filename(
|
||||
cache_path: &str,
|
||||
source_filename: &str,
|
||||
stream_index: i32,
|
||||
params: &ProxyParams,
|
||||
) -> crate::error::Result<String> {
|
||||
let proxy_dir = Self::get_proxy_directory(cache_path)?;
|
||||
let extension = if params.extension_str().is_empty() {
|
||||
"mp4"
|
||||
} else {
|
||||
params.extension_str()
|
||||
};
|
||||
|
||||
// Divider mode scales relative to the source, so the tag names the
|
||||
// divider rather than an absolute target size.
|
||||
let size_tag = if params.divider > 1 {
|
||||
format!("div{}", params.divider)
|
||||
} else {
|
||||
format!("{}x{}", params.width, params.height)
|
||||
};
|
||||
|
||||
let filename = format!(
|
||||
"{}-{}.{}.v{}.a{}.{}",
|
||||
unique_file_identifier(source_filename),
|
||||
stream_index,
|
||||
size_tag,
|
||||
params.version,
|
||||
params.include_audio,
|
||||
extension,
|
||||
);
|
||||
|
||||
Ok(Path::new(&proxy_dir)
|
||||
.join(filename)
|
||||
.to_string_lossy()
|
||||
.into_owned())
|
||||
}
|
||||
|
||||
/// Working (in-progress) filename of a proxy.
|
||||
pub fn get_working_filename(proxy_filename: &str) -> crate::error::Result<String> {
|
||||
// Append a recognizable suffix while keeping a standard container
|
||||
// extension so ffmpeg can infer the output format.
|
||||
Ok(format!("{}.working.mp4", proxy_filename))
|
||||
}
|
||||
|
||||
/// Get or start generating a proxy for `source_filename`. With no task
|
||||
/// registrar the state stays `Missing`.
|
||||
pub fn get_or_start(
|
||||
&self,
|
||||
cache_path: &str,
|
||||
source_filename: &str,
|
||||
stream_index: i32,
|
||||
params: &ProxyParams,
|
||||
) -> crate::error::Result<(ProxyState, String)> {
|
||||
let filename = Self::get_proxy_filename(cache_path, source_filename, stream_index, params)?;
|
||||
let file_state = Self::get_proxy_state(&filename);
|
||||
if file_state == ProxyState::Ready {
|
||||
return Ok((ProxyState::Ready, filename));
|
||||
}
|
||||
|
||||
if !crate::task::task_submit_is_registered() {
|
||||
// Interim state (pre-M8): no task system, proxy cannot be generated.
|
||||
return Ok((ProxyState::Missing, filename));
|
||||
}
|
||||
|
||||
if file_state == ProxyState::Generating {
|
||||
// Stale working file from an interrupted run.
|
||||
if let Ok(working) = Self::get_working_filename(&filename) {
|
||||
let _ = std::fs::remove_file(&working);
|
||||
}
|
||||
}
|
||||
|
||||
// The task owns the ".working.mp4" temporary name and the rename to the
|
||||
// final filename on success.
|
||||
let req = crate::task::TaskRequest {
|
||||
kind: crate::task::TaskKind::Proxy,
|
||||
input_filename: source_filename,
|
||||
output_filename: &filename,
|
||||
stream_index,
|
||||
sample_rate: 0,
|
||||
channel_layout: 0,
|
||||
sample_format: 0,
|
||||
proxy_width: if params.divider <= 1 { params.width } else { 0 },
|
||||
proxy_height: if params.divider <= 1 { params.height } else { 0 },
|
||||
};
|
||||
|
||||
// Interim simplification: submission is synchronous.
|
||||
if crate::task::submit_task(&req).is_err() {
|
||||
return Ok((ProxyState::Failed, filename));
|
||||
}
|
||||
|
||||
if Self::get_proxy_state(&filename) == ProxyState::Ready {
|
||||
return Ok((ProxyState::Ready, filename));
|
||||
}
|
||||
|
||||
Ok((ProxyState::Generating, filename))
|
||||
}
|
||||
|
||||
/// Locate an ffmpeg executable (empty string when none found).
|
||||
pub fn find_ffmpeg(configured_path: &str) -> String {
|
||||
// An explicitly configured path takes precedence if it is usable.
|
||||
if !configured_path.is_empty() {
|
||||
if is_executable_file(Path::new(configured_path)) {
|
||||
return absolute(configured_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to searching the system PATH.
|
||||
if let Ok(path_env) = std::env::var("PATH") {
|
||||
for dir in path_env.split(':') {
|
||||
if dir.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let candidate = Path::new(dir).join("ffmpeg");
|
||||
if is_executable_file(&candidate) {
|
||||
return absolute(&candidate.to_string_lossy());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, try common install locations (PATH on GUI-launched apps,
|
||||
// particularly on macOS, often lacks these).
|
||||
let mut candidates: Vec<String> = Vec::new();
|
||||
let app_path = application_path();
|
||||
if !app_path.is_empty() {
|
||||
candidates.push(format!("{}/ffmpeg", app_path));
|
||||
}
|
||||
candidates.push("/opt/homebrew/bin/ffmpeg".to_string());
|
||||
candidates.push("/usr/local/bin/ffmpeg".to_string());
|
||||
candidates.push("/usr/bin/ffmpeg".to_string());
|
||||
candidates.push("/usr/local/bin/ffmpeg".to_string());
|
||||
|
||||
for c in candidates {
|
||||
if is_executable_file(Path::new(&c)) {
|
||||
return absolute(&c);
|
||||
}
|
||||
}
|
||||
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy a byte string into a NUL-terminated `[u8; 32]` (truncated to 31
|
||||
/// chars so there is always a trailing NUL).
|
||||
fn bytes32(s: &[u8]) -> [u8; 32] {
|
||||
let mut a = [0u8; 32];
|
||||
let n = s.len().min(31);
|
||||
a[..n].copy_from_slice(&s[..n]);
|
||||
a
|
||||
}
|
||||
|
||||
/// View a NUL-terminated `[u8; 32]` as a `&str` (up to the first NUL).
|
||||
fn cstr_slice(a: &[u8; 32]) -> &str {
|
||||
let end = a.iter().position(|&b| b == 0).unwrap_or(a.len());
|
||||
std::str::from_utf8(&a[..end]).unwrap_or("")
|
||||
}
|
||||
|
||||
/// `oakcommon_config_get_int` wrapper (null group).
|
||||
fn config_get_int(key: &str, default: i32) -> i32 {
|
||||
let ckey = cstring(key);
|
||||
// # Safety: `ckey` is a valid NUL-terminated C string alive for the call.
|
||||
unsafe {
|
||||
crate::bridge::common::oakcommon_config_get_int(
|
||||
std::ptr::null(),
|
||||
ckey.as_ptr(),
|
||||
default,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// `oakcommon_config_get_bool` wrapper (null group).
|
||||
fn config_get_bool(key: &str, default: i32) -> i32 {
|
||||
let ckey = cstring(key);
|
||||
// # Safety: `ckey` is a valid NUL-terminated C string alive for the call.
|
||||
unsafe {
|
||||
crate::bridge::common::oakcommon_config_get_bool(
|
||||
std::ptr::null(),
|
||||
ckey.as_ptr(),
|
||||
default,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Two-stage `oakcommon_config_get` string read; `None` when the stored
|
||||
/// value is empty or absent.
|
||||
fn config_get_str(key: &str) -> Option<String> {
|
||||
let ckey = cstring(key);
|
||||
// # Safety: `ckey` is valid; first call asks only for the required size.
|
||||
let size = unsafe {
|
||||
crate::bridge::common::oakcommon_config_get(
|
||||
std::ptr::null(),
|
||||
ckey.as_ptr(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if size <= 1 {
|
||||
return None;
|
||||
}
|
||||
let mut buf = vec![0u8; size as usize];
|
||||
// # Safety: `buf` has `size` bytes; the call fills at most `size` bytes.
|
||||
unsafe {
|
||||
crate::bridge::common::oakcommon_config_get(
|
||||
std::ptr::null(),
|
||||
ckey.as_ptr(),
|
||||
buf.as_mut_ptr() as *mut c_char,
|
||||
size,
|
||||
);
|
||||
}
|
||||
let mut end = buf.len();
|
||||
while end > 0 && buf[end - 1] == 0 {
|
||||
end -= 1;
|
||||
}
|
||||
Some(String::from_utf8_lossy(&buf[..end]).into_owned())
|
||||
}
|
||||
|
||||
/// `oakcommon_filefunctions_get_unique_file_identifier` wrapper (the bridge
|
||||
/// returns a 64-bit id directly).
|
||||
fn unique_file_identifier(filename: &str) -> String {
|
||||
let c = match CString::new(filename) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return String::new(),
|
||||
};
|
||||
// # Safety: `c` is a valid NUL-terminated C string alive for the call.
|
||||
let id = unsafe {
|
||||
crate::bridge::common::oakcommon_filefunctions_get_unique_file_identifier(c.as_ptr())
|
||||
};
|
||||
format!("{}", id)
|
||||
}
|
||||
|
||||
/// Two-stage `oakcommon_filefunctions_get_application_path` read.
|
||||
fn application_path() -> String {
|
||||
// # Safety: first call asks only for the required size.
|
||||
let size = unsafe {
|
||||
crate::bridge::common::oakcommon_filefunctions_get_application_path(
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if size <= 1 {
|
||||
return String::new();
|
||||
}
|
||||
let mut buf = vec![0u8; size as usize];
|
||||
// # Safety: `buf` has `size` bytes; the call fills at most `size` bytes.
|
||||
unsafe {
|
||||
crate::bridge::common::oakcommon_filefunctions_get_application_path(
|
||||
buf.as_mut_ptr() as *mut c_char,
|
||||
size,
|
||||
);
|
||||
}
|
||||
let mut end = buf.len();
|
||||
while end > 0 && buf[end - 1] == 0 {
|
||||
end -= 1;
|
||||
}
|
||||
String::from_utf8_lossy(&buf[..end]).into_owned()
|
||||
}
|
||||
|
||||
/// Build a NUL-terminated C string from a Rust string; empty on embedded
|
||||
/// NUL (defensive only — callers pass sane keys).
|
||||
fn cstring(s: &str) -> CString {
|
||||
CString::new(s).unwrap_or_else(|_| CString::new("").unwrap())
|
||||
}
|
||||
|
||||
/// True when `p` is a regular file with at least one execute bit set.
|
||||
fn is_executable_file(p: &Path) -> bool {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
match std::fs::metadata(p) {
|
||||
Ok(md) if md.is_file() => md.permissions().mode() & 0o111 != 0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Canonicalize a path, falling back to the raw string on failure.
|
||||
fn absolute(p: &str) -> String {
|
||||
std::fs::canonicalize(p)
|
||||
.map(|c| c.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|_| p.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn temp_subdir(name: &str) -> String {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"oakcodec_proxy_{}_{}",
|
||||
name,
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
dir.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
fn fnv1a64(bytes: &[u8]) -> u64 {
|
||||
let mut h: u64 = 14695981039346656037;
|
||||
for &b in bytes {
|
||||
h ^= b as u64;
|
||||
h = h.wrapping_mul(1099511628211);
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_params_default_values() {
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
assert_eq!(p.width, 1280);
|
||||
assert_eq!(p.height, 720);
|
||||
assert_eq!(p.divider, 1);
|
||||
assert_eq!(p.version, 1);
|
||||
assert_eq!(p.crf, 23);
|
||||
assert_eq!(p.include_audio, 1);
|
||||
assert_eq!(p.extension_str(), "mp4");
|
||||
assert_eq!(p.preset_str(), "veryfast");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_params_from_config_uses_defaults_without_store() {
|
||||
// The test stub returns defaults for every int/bool and an empty
|
||||
// ProxyPreset; the empty preset must not clobber the compiled-in one.
|
||||
let p = ProxyManager::proxy_params_from_config();
|
||||
assert_eq!(p.width, 1280);
|
||||
assert_eq!(p.height, 720);
|
||||
assert_eq!(p.divider, 1);
|
||||
assert_eq!(p.crf, 23);
|
||||
assert_eq!(p.include_audio, 1);
|
||||
assert_eq!(p.preset_str(), "veryfast");
|
||||
assert_eq!(p.extension_str(), "mp4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_directory_is_cache_slash_proxy() {
|
||||
assert_eq!(
|
||||
ProxyManager::get_proxy_directory("/tmp/cache").unwrap(),
|
||||
"/tmp/cache/proxy"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_filename_derivation() {
|
||||
let cache = temp_subdir("fn");
|
||||
let id = fnv1a64(b"media.mp4") as i64;
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
|
||||
let f = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &p).unwrap();
|
||||
assert_eq!(
|
||||
f,
|
||||
format!("{}/proxy/{}-0.1280x720.v1.a1.mp4", cache, id)
|
||||
);
|
||||
|
||||
// Divider mode tags the divider instead of an absolute size.
|
||||
let mut d = p.clone();
|
||||
d.divider = 2;
|
||||
let f2 = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &d).unwrap();
|
||||
assert!(f2.contains(".div2."));
|
||||
|
||||
// No audio.
|
||||
let mut na = p.clone();
|
||||
na.include_audio = 0;
|
||||
let f3 = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &na).unwrap();
|
||||
assert!(f3.contains(".a0."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_state_transitions() {
|
||||
let cache = temp_subdir("state");
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
let f = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &p).unwrap();
|
||||
|
||||
// Missing when neither the file nor the working file exists.
|
||||
assert_eq!(ProxyManager::get_proxy_state(&f), ProxyState::Missing);
|
||||
|
||||
// Generating when only the working file exists.
|
||||
let working = ProxyManager::get_working_filename(&f).unwrap();
|
||||
std::fs::create_dir_all(Path::new(&working).parent().unwrap()).unwrap();
|
||||
std::fs::write(&working, b"x").unwrap();
|
||||
assert_eq!(ProxyManager::get_proxy_state(&f), ProxyState::Generating);
|
||||
|
||||
// Ready when the final file exists (takes precedence over working).
|
||||
std::fs::create_dir_all(Path::new(&f).parent().unwrap()).unwrap();
|
||||
std::fs::write(&f, b"x").unwrap();
|
||||
assert_eq!(ProxyManager::get_proxy_state(&f), ProxyState::Ready);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_state_to_string_mapping() {
|
||||
assert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Missing), "missing");
|
||||
assert_eq!(
|
||||
ProxyManager::proxy_state_to_string(ProxyState::Generating),
|
||||
"generating"
|
||||
);
|
||||
assert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Ready), "ready");
|
||||
assert_eq!(ProxyManager::proxy_state_to_string(ProxyState::Failed), "failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_working_filename_appends_suffix() {
|
||||
assert_eq!(
|
||||
ProxyManager::get_working_filename("/a/b.mp4").unwrap(),
|
||||
"/a/b.mp4.working.mp4"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_or_start_missing_without_registrar() {
|
||||
let _g = crate::conformmanager::test_util::REG_LOCK.lock().unwrap();
|
||||
crate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
let cache = temp_subdir("nostart");
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
let (state, _f) =
|
||||
ProxyManager::instance()
|
||||
.get_or_start(&cache, "media.mp4", 0, &p)
|
||||
.unwrap();
|
||||
assert_eq!(state, ProxyState::Missing);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_or_start_ready_when_file_exists() {
|
||||
let cache = temp_subdir("ready");
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
let f = ProxyManager::get_proxy_filename(&cache, "media.mp4", 0, &p).unwrap();
|
||||
std::fs::create_dir_all(Path::new(&f).parent().unwrap()).unwrap();
|
||||
std::fs::write(&f, b"x").unwrap();
|
||||
let (state, filename) =
|
||||
ProxyManager::instance()
|
||||
.get_or_start(&cache, "media.mp4", 0, &p)
|
||||
.unwrap();
|
||||
assert_eq!(state, ProxyState::Ready);
|
||||
assert_eq!(filename, f);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_or_start_generating_when_registered() {
|
||||
let _g = crate::conformmanager::test_util::REG_LOCK.lock().unwrap();
|
||||
crate::task::set_task_submit_cb_extern(
|
||||
Some(crate::conformmanager::test_util::accept_cb),
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
let cache = temp_subdir("start");
|
||||
let p = ProxyManager::proxy_params_default();
|
||||
let (state, _f) =
|
||||
ProxyManager::instance()
|
||||
.get_or_start(&cache, "media.mp4", 0, &p)
|
||||
.unwrap();
|
||||
crate::task::set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
assert_eq!(state, ProxyState::Generating);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ffmpeg_configured_path_wins() {
|
||||
// Point at a real executable (the current test binary) so the
|
||||
// configured-path branch resolves to an absolute path.
|
||||
let me = std::env::current_exe().unwrap();
|
||||
let found = ProxyManager::find_ffmpeg(me.to_str().unwrap());
|
||||
let canonical = std::fs::canonicalize(&me).unwrap();
|
||||
assert_eq!(found, canonical.to_string_lossy());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ffmpeg_missing_returns_empty() {
|
||||
let found = ProxyManager::find_ffmpeg("/definitely/not/a/real/ffmpeg");
|
||||
// Either an absolute configured/installed match or empty; never a raw
|
||||
// unresolved path.
|
||||
assert!(found.is_empty() || found.starts_with('/'));
|
||||
}
|
||||
|
||||
/// `oakcodec_proxy_params` byte-level layout lock against
|
||||
/// `include/codec/proxy.h` (verified with a C++ `offsetof` probe): the
|
||||
/// Rust mirror must read a C caller's POD in place.
|
||||
#[test]
|
||||
fn proxy_params_c_abi_layout() {
|
||||
use std::mem::{offset_of, size_of};
|
||||
|
||||
assert_eq!(size_of::<ProxyParams>(), 88);
|
||||
assert_eq!(offset_of!(ProxyParams, width), 0);
|
||||
assert_eq!(offset_of!(ProxyParams, height), 4);
|
||||
assert_eq!(offset_of!(ProxyParams, divider), 8);
|
||||
assert_eq!(offset_of!(ProxyParams, version), 12);
|
||||
assert_eq!(offset_of!(ProxyParams, crf), 16);
|
||||
assert_eq!(offset_of!(ProxyParams, include_audio), 20);
|
||||
assert_eq!(offset_of!(ProxyParams, extension), 24);
|
||||
assert_eq!(offset_of!(ProxyParams, preset), 56);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_extra {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn find_ffmpeg_searches_path() {
|
||||
// Create a fake executable in a temp dir and prepend it to PATH.
|
||||
let dir = std::env::temp_dir().join(format!("oakcodec_ffmpeg_{}", std::process::id()));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let fake = dir.join("ffmpeg");
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::write(&fake, b"#!/bin/sh\n").unwrap();
|
||||
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
|
||||
let mut paths = dir.to_string_lossy().into_owned();
|
||||
if let Ok(existing) = std::env::var("PATH") {
|
||||
paths.push(':');
|
||||
paths.push_str(&existing);
|
||||
}
|
||||
let old = std::env::var_os("PATH");
|
||||
std::env::set_var("PATH", &paths);
|
||||
let found = ProxyManager::find_ffmpeg("");
|
||||
if let Some(old) = old {
|
||||
std::env::set_var("PATH", old);
|
||||
} else {
|
||||
std::env::remove_var("PATH");
|
||||
}
|
||||
|
||||
// `std::env::set_var` is not thread-safe, so under parallel tests the
|
||||
// canonicalized form can race; assert the search branch invariants
|
||||
// instead of the exact canonical path.
|
||||
assert!(found.starts_with('/'), "found: {found}");
|
||||
assert!(found.ends_with("/ffmpeg"), "found: {found}");
|
||||
assert!(std::path::Path::new(&found).exists(), "found: {found}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_proxy_state_empty_and_working() {
|
||||
// Empty filename -> Missing.
|
||||
assert_eq!(ProxyManager::get_proxy_state(""), ProxyState::Missing);
|
||||
// A path that does not exist -> Missing.
|
||||
assert_eq!(ProxyManager::get_proxy_state("/nope/nope.mp4"), ProxyState::Missing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// 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/>.
|
||||
|
||||
//! Real-media tests for the FFmpeg decoder/encoder (`#[cfg(test)]` module).
|
||||
//!
|
||||
//! These exercise the real `ffmpeg-next` implementation against
|
||||
//! `tests/demo.mp4` at the repository root (H.264 1920x1080@25fps + AAC
|
||||
//! 48kHz stereo) and a full H.264 encode round-trip through `/tmp`.
|
||||
//!
|
||||
//! They live inside the crate (not `tests/`) because the crate's
|
||||
//! `#[cfg(test)]` in-memory oakcommon/oakrender stubs — which the
|
||||
//! `Frame`/`FootageDescription` paths need — are only linked for the lib
|
||||
//! test binary (`tests/` is compiled without `#[cfg(test)]` and cannot
|
||||
//! resolve those symbols; see `tests/ffi_contract_test.rs`).
|
||||
|
||||
use crate::bridge::common::{
|
||||
oakcommon_videoparams_get_duration, oakcommon_videoparams_get_frame_rate,
|
||||
oakcommon_videoparams_get_height, oakcommon_videoparams_get_width,
|
||||
oakcommon_videoparams_init_basic, oakcommon_videoparams_set_format,
|
||||
};
|
||||
use crate::decoder::{
|
||||
CodecStream, Decoder, K_COLOR_RANGE_DEFAULT, RenderMode, RetrieveAudioStatus,
|
||||
RetrieveVideoParams,
|
||||
};
|
||||
use crate::encoder::create_from_params;
|
||||
use crate::ffmpeg::FFmpegDecoder;
|
||||
use crate::frame::Frame;
|
||||
use oakcore_rs::{PixelFormat, Rational, TimeRange};
|
||||
|
||||
/// `tests/demo.mp4` at the repository root.
|
||||
fn demo_path() -> std::path::PathBuf {
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../tests/demo.mp4")
|
||||
}
|
||||
|
||||
fn video_params(stream: CodecStream, time: Rational) -> RetrieveVideoParams {
|
||||
RetrieveVideoParams {
|
||||
stream,
|
||||
time,
|
||||
length: TimeRange::default(),
|
||||
force_range: K_COLOR_RANGE_DEFAULT,
|
||||
is_image_sequence: false,
|
||||
image_sequence_digits: 0,
|
||||
image_sequence_number: 0,
|
||||
mode: RenderMode::Offline,
|
||||
alpha_is_premultiplied: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// H.264 encoder parameters: 64x64, 10 fps, `out` as the target file.
|
||||
fn h264_params(out: &std::path::Path) -> crate::encodingparams::EncodingParams {
|
||||
let mut p = crate::encodingparams::EncodingParams::default();
|
||||
let name = out.as_os_str().as_encoded_bytes();
|
||||
p.filename[..name.len()].copy_from_slice(name);
|
||||
p.format = 2; // MPEG-4 video
|
||||
p.video_enabled = 1;
|
||||
p.video_codec = 1; // H.264
|
||||
p.video_width = 64;
|
||||
p.video_height = 64;
|
||||
p.video_time_base_num = 1;
|
||||
p.video_time_base_den = 10;
|
||||
p.video_pixel_format = PixelFormat::F32;
|
||||
p.video_interlacing = 0;
|
||||
p.video_pixel_aspect_num = 1;
|
||||
p.video_pixel_aspect_den = 1;
|
||||
p
|
||||
}
|
||||
|
||||
/// Build an allocated F32-RGBA frame with a moving color pattern.
|
||||
fn pattern_frame(i: i32) -> Frame {
|
||||
let vp = unsafe { oakcommon_videoparams_init_basic(64, 64) };
|
||||
unsafe { oakcommon_videoparams_set_format(vp.clone(), PixelFormat::F32 as i32) };
|
||||
let mut f = Frame::with_params(vp);
|
||||
f.set_timestamp(Rational::new(i as i64, 10));
|
||||
f.allocate().unwrap();
|
||||
|
||||
let linesize = f.linesize_bytes() as usize;
|
||||
let data = f.data_mut().unwrap();
|
||||
for y in 0..64usize {
|
||||
for x in 0..64usize {
|
||||
let off = y * linesize + x * 16;
|
||||
let r: f32 = if x < 32 { 0.4 + i as f32 * 0.05 } else { 0.1 };
|
||||
let g: f32 = y as f32 / 64.0;
|
||||
let b: f32 = if x >= 32 { 0.7 } else { 0.2 };
|
||||
data[off..off + 4].copy_from_slice(&r.to_le_bytes());
|
||||
data[off + 4..off + 8].copy_from_slice(&g.to_le_bytes());
|
||||
data[off + 8..off + 12].copy_from_slice(&b.to_le_bytes());
|
||||
data[off + 12..off + 16].copy_from_slice(&1.0f32.to_le_bytes());
|
||||
}
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_reports_streams_and_duration() {
|
||||
let d = FFmpegDecoder::new();
|
||||
let desc = d
|
||||
.probe(demo_path().to_str().unwrap(), None)
|
||||
.expect("demo.mp4 should probe");
|
||||
assert_eq!(desc.decoder(), "ffmpeg");
|
||||
// video + audio + data (timecode) stream.
|
||||
assert_eq!(desc.total_stream_count(), 3);
|
||||
assert_eq!(desc.video_stream_count(), 1);
|
||||
assert_eq!(desc.audio_stream_count(), 1);
|
||||
|
||||
// Video stream: 1920x1080, 25fps, 17s at 1/12800 time base.
|
||||
let vp = desc.get_video_stream(0).expect("video stream");
|
||||
assert_eq!(unsafe { oakcommon_videoparams_get_width(vp.clone()) }, 1920);
|
||||
assert_eq!(unsafe { oakcommon_videoparams_get_height(vp.clone()) }, 1080);
|
||||
assert_eq!(unsafe { oakcommon_videoparams_get_duration(vp.clone()) }, 17 * 12800);
|
||||
|
||||
let mut num: i32 = 0;
|
||||
let mut den: i32 = 0;
|
||||
unsafe { oakcommon_videoparams_get_frame_rate(vp.clone(), &mut num, &mut den) };
|
||||
assert_eq!((num, den), (25, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_first_video_frame_has_dimensions_and_content() {
|
||||
let d = FFmpegDecoder::new();
|
||||
let s = CodecStream::with_block(demo_path().to_string_lossy().into_owned(), 0, None);
|
||||
d.open(&s).expect("open video stream");
|
||||
|
||||
let f = d
|
||||
.retrieve_video_frame(&video_params(s, Rational::new(0, 1)))
|
||||
.expect("decode first frame");
|
||||
assert_eq!(f.width(), 1920);
|
||||
assert_eq!(f.height(), 1080);
|
||||
assert_eq!(f.format(), PixelFormat::F32);
|
||||
assert!(f.is_allocated());
|
||||
// Expected size: 4 channels x 4 bytes, linesize 32-byte aligned.
|
||||
assert_eq!(f.allocated_size(), (16 * 1920) * 1080);
|
||||
|
||||
// The frame must contain non-zero pixels.
|
||||
let data = f.data().expect("allocated data");
|
||||
assert!(data.iter().any(|&b| b != 0), "decoded frame is all zeros");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_video_frame_at_midpoint() {
|
||||
let d = FFmpegDecoder::new();
|
||||
let s = CodecStream::with_block(demo_path().to_string_lossy().into_owned(), 0, None);
|
||||
d.open(&s).expect("open video stream");
|
||||
|
||||
let f = d
|
||||
.retrieve_video_frame(&video_params(s, Rational::new(8, 1)))
|
||||
.expect("decode mid frame");
|
||||
assert_eq!(f.width(), 1920);
|
||||
assert_eq!(f.height(), 1080);
|
||||
assert!(f.data().unwrap().iter().any(|&b| b != 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_decode_is_non_empty() {
|
||||
let d = FFmpegDecoder::new();
|
||||
let s = CodecStream::with_block(demo_path().to_string_lossy().into_owned(), 1, None);
|
||||
d.open(&s).expect("open audio stream");
|
||||
|
||||
// One second of stereo at 48 kHz.
|
||||
let mut dest = vec![0f32; 48000 * 2];
|
||||
let status = d
|
||||
.retrieve_audio(
|
||||
&mut dest,
|
||||
&TimeRange::new(Rational::new(0, 1), Rational::new(1, 1)),
|
||||
48000,
|
||||
0x3, // stereo mask
|
||||
)
|
||||
.expect("retrieve audio");
|
||||
assert_eq!(status, RetrieveAudioStatus::Success);
|
||||
|
||||
let peak = dest.iter().fold(0.0f32, |a, &s| a.max(s.abs()));
|
||||
assert!(peak > 0.0, "decoded audio is all silence");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_h264_roundtrip_to_tmp() {
|
||||
let out = std::env::temp_dir().join(format!("oakcodec_roundtrip_{}.mp4", std::process::id()));
|
||||
let params = h264_params(&out);
|
||||
let out_str = out.to_str().expect("utf8 temp path").to_string();
|
||||
|
||||
let e = create_from_params(¶ms).expect("create ffmpeg encoder");
|
||||
assert_eq!(e.id(), "ffmpeg");
|
||||
e.configure(¶ms).expect("configure");
|
||||
e.open().expect("open output");
|
||||
|
||||
// Encode 10 frames with a moving pattern.
|
||||
for i in 0..10 {
|
||||
let f = pattern_frame(i);
|
||||
e.write_video(&f).expect("write video frame");
|
||||
}
|
||||
e.flush().expect("flush");
|
||||
|
||||
// The output exists and has a plausible size.
|
||||
assert!(out.exists(), "round-trip file was not created");
|
||||
assert!(out.metadata().unwrap().len() > 1000, "round-trip file is empty");
|
||||
|
||||
// Probe the result: one 64x64 video stream.
|
||||
let d = FFmpegDecoder::new();
|
||||
let desc = d.probe(&out_str, None).expect("probe round-trip output");
|
||||
assert_eq!(desc.video_stream_count(), 1);
|
||||
let vp = desc.get_video_stream(0).expect("video stream");
|
||||
assert_eq!(unsafe { oakcommon_videoparams_get_width(vp.clone()) }, 64);
|
||||
assert_eq!(unsafe { oakcommon_videoparams_get_height(vp.clone()) }, 64);
|
||||
|
||||
// Decode the first frame of the result.
|
||||
let s = CodecStream::with_block(out_str.clone(), 0, None);
|
||||
d.open(&s).expect("open round-trip video");
|
||||
let f = d
|
||||
.retrieve_video_frame(&video_params(s, Rational::new(0, 1)))
|
||||
.expect("decode round-trip first frame");
|
||||
assert_eq!(f.width(), 64);
|
||||
assert_eq!(f.height(), 64);
|
||||
assert_eq!(f.format(), PixelFormat::F32);
|
||||
assert!(f.data().unwrap().iter().any(|&b| b != 0));
|
||||
|
||||
let _ = std::fs::remove_file(&out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audio_conform_writes_planar_pcm() {
|
||||
let d = FFmpegDecoder::new();
|
||||
let s = CodecStream::with_block(demo_path().to_string_lossy().into_owned(), 1, None);
|
||||
d.open(&s).expect("open audio stream");
|
||||
|
||||
let dir = std::env::temp_dir().join(format!("oakcodec_conform_{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let ch0 = dir.join("0.pcm").to_string_lossy().into_owned();
|
||||
let ch1 = dir.join("1.pcm").to_string_lossy().into_owned();
|
||||
|
||||
d.conform_audio(&[ch0.clone(), ch1.clone()], 48000, 0x3, 4, None)
|
||||
.expect("conform to f32 planar");
|
||||
|
||||
for path in [&ch0, &ch1] {
|
||||
let meta = std::fs::metadata(path).expect("conform output exists");
|
||||
assert!(meta.len() > 0, "conform file is empty");
|
||||
// 1 second at 48kHz * 4 bytes = 192 KB minimum.
|
||||
assert!(meta.len() >= 192_000, "conform file too short: {}", meta.len());
|
||||
}
|
||||
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
// 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/>.
|
||||
|
||||
//! Background task submission hook (`include/codec/task.h`).
|
||||
//!
|
||||
//! The codec module needs occasional background work (audio conforms,
|
||||
//! proxy transcodes). The task system itself splits out at milestone M8;
|
||||
//! until then oakcodec exposes a single global submit callback. A host
|
||||
//! (M8: oaktask) registers with [`set_task_submit_cb`]; the conform/proxy
|
||||
//! managers call it whenever they need a task. With no callback, managers
|
||||
//! report work as unavailable — they never crash and never block.
|
||||
|
||||
use std::ffi::{c_void, CString};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::error::{Error, OAKCODEC_OK};
|
||||
|
||||
/// Kinds of background tasks oakcodec can request.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum TaskKind {
|
||||
/// Audio conform to pcm cache files.
|
||||
Conform = 0,
|
||||
/// Video proxy transcode.
|
||||
Proxy = 1,
|
||||
}
|
||||
|
||||
/// Description of one background task request.
|
||||
///
|
||||
/// All strings are borrowed and only valid for the duration of the submit
|
||||
/// call; the callback must copy anything it retains.
|
||||
#[repr(C)]
|
||||
pub struct TaskRequest<'a> {
|
||||
/// `TaskKind`.
|
||||
pub kind: TaskKind,
|
||||
/// Source media filename.
|
||||
pub input_filename: &'a str,
|
||||
/// Final destination path (see field docs in include/codec/task.h).
|
||||
pub output_filename: &'a str,
|
||||
/// Stream inside the source media.
|
||||
pub stream_index: i32,
|
||||
/// Conform: target sample rate.
|
||||
pub sample_rate: i32,
|
||||
/// Conform: target channel layout mask.
|
||||
pub channel_layout: u64,
|
||||
/// Conform: target sample format (enum as int).
|
||||
pub sample_format: i32,
|
||||
/// Proxy: target width (0 = unspecified/divider-based).
|
||||
pub proxy_width: i32,
|
||||
/// Proxy: target height (0 = unspecified/divider-based).
|
||||
pub proxy_height: i32,
|
||||
}
|
||||
|
||||
/// Task submit callback signature.
|
||||
///
|
||||
/// Returns `Ok(())` if the task was accepted (completed synchronously or
|
||||
/// queued), `Err` if the request was rejected.
|
||||
pub type TaskSubmitFn = dyn Fn(&TaskRequest, *mut std::ffi::c_void) -> crate::error::Result<()>;
|
||||
|
||||
/// `OakCodecTaskRequest` — C ABI mirror of [`TaskRequest`] for the submit
|
||||
/// callback; see `include/codec/task.h`. Strings are borrowed C pointers,
|
||||
/// valid only for the duration of the call.
|
||||
#[repr(C)]
|
||||
pub struct OakCodecTaskRequest {
|
||||
/// `TaskKind` value.
|
||||
pub kind: i32,
|
||||
/// Source media filename.
|
||||
pub input_filename: *const std::ffi::c_char,
|
||||
/// Final destination path.
|
||||
pub output_filename: *const std::ffi::c_char,
|
||||
/// Stream inside the source media.
|
||||
pub stream_index: i32,
|
||||
/// Conform: target sample rate.
|
||||
pub sample_rate: i32,
|
||||
/// Conform: target channel-layout mask.
|
||||
pub channel_layout: u64,
|
||||
/// Conform: target sample format (enum as int).
|
||||
pub sample_format: i32,
|
||||
/// Proxy: target width (0 = unspecified/divider-based).
|
||||
pub proxy_width: i32,
|
||||
/// Proxy: target height (0 = unspecified/divider-based).
|
||||
pub proxy_height: i32,
|
||||
}
|
||||
|
||||
/// `oakcodec_task_submit_fn` — the extern-C submit callback typedef; see
|
||||
/// `include/codec/task.h`. Returns `OAKCODEC_OK` on accept, else a
|
||||
/// negative `OAKCODEC_E_*` code.
|
||||
pub type OakCodecTaskSubmitFn = unsafe extern "C" fn(
|
||||
req: *const OakCodecTaskRequest,
|
||||
userdata: *mut std::ffi::c_void,
|
||||
) -> i32;
|
||||
|
||||
/// One registered submit callback (extern-C from the host, or a crate
|
||||
/// Rust closure). Mirrors the C++ `g_task_cb`/`g_task_cb_userdata` pair.
|
||||
enum SubmitCb {
|
||||
/// No callback registered.
|
||||
None,
|
||||
/// Extern-C callback registered via `oakcodec_set_task_submit_cb`.
|
||||
Extern {
|
||||
/// The C function pointer.
|
||||
cb: OakCodecTaskSubmitFn,
|
||||
/// Opaque userdata passed back on each call.
|
||||
userdata: *mut c_void,
|
||||
},
|
||||
/// Crate-internal Rust closure registered via [`set_task_submit_cb`].
|
||||
Rust {
|
||||
/// Raw fat-pointer to the `&'static TaskSubmitFn` (kept `*const` so
|
||||
/// the registry is `Send`).
|
||||
cb: *const TaskSubmitFn,
|
||||
/// Opaque userdata passed back on each call.
|
||||
userdata: *mut c_void,
|
||||
},
|
||||
}
|
||||
|
||||
// # Safety: the stored pointers (extern-C fn pointer, fat pointer to a
|
||||
// 'static closure, userdata) are only dereferenced/called while holding the
|
||||
// registry mutex; the Rust closure is 'static and the extern-C fn outlives
|
||||
// registration by contract. Moving the enum between threads under the lock
|
||||
// therefore cannot alias.
|
||||
unsafe impl Send for SubmitCb {}
|
||||
|
||||
/// The global task submit callback registry. Only one callback is held at
|
||||
/// a time; registering replaces it, `None` clears it. Thread-safe.
|
||||
static TASK_SUBMIT: Mutex<SubmitCb> = Mutex::new(SubmitCb::None);
|
||||
|
||||
/// Registers (or replaces) the global task submit callback. Pass `None` to
|
||||
/// unregister. Thread-safe. Interim state (pre-M8): nobody registers and all
|
||||
/// task-dependent work reports unavailable.
|
||||
pub fn set_task_submit_cb(cb: Option<&'static TaskSubmitFn>, userdata: *mut std::ffi::c_void) {
|
||||
let mut g = TASK_SUBMIT.lock().unwrap();
|
||||
*g = match cb {
|
||||
Some(cb) => SubmitCb::Rust {
|
||||
cb: cb as *const TaskSubmitFn,
|
||||
userdata,
|
||||
},
|
||||
None => SubmitCb::None,
|
||||
};
|
||||
}
|
||||
|
||||
/// Register an extern-C submit callback (used by `ffi::task`).
|
||||
///
|
||||
/// Mirrors `oakcodec_set_task_submit_cb`: a `None` pointer clears it.
|
||||
pub(crate) fn set_task_submit_cb_extern(
|
||||
cb: Option<OakCodecTaskSubmitFn>,
|
||||
userdata: *mut std::ffi::c_void,
|
||||
) {
|
||||
let mut g = TASK_SUBMIT.lock().unwrap();
|
||||
*g = match cb {
|
||||
Some(cb) => SubmitCb::Extern { cb, userdata },
|
||||
None => SubmitCb::None,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns 1 if a submit callback is currently registered, else 0.
|
||||
/// Thread-safe.
|
||||
pub fn task_submit_is_registered() -> bool {
|
||||
let g = TASK_SUBMIT.lock().unwrap();
|
||||
!matches!(&*g, SubmitCb::None)
|
||||
}
|
||||
|
||||
/// Submit a task through the registered callback, if any.
|
||||
///
|
||||
/// Returns `Ok(false)` when no callback is registered (nothing submitted),
|
||||
/// `Ok(true)` when accepted, or `Err` when the callback rejected it.
|
||||
pub fn submit_task(
|
||||
req: &TaskRequest,
|
||||
) -> crate::error::Result<bool> {
|
||||
let g = TASK_SUBMIT.lock().unwrap();
|
||||
match &*g {
|
||||
SubmitCb::None => Ok(false),
|
||||
SubmitCb::Extern { cb, userdata } => {
|
||||
// Bind the C strings to locals so the temporaries outlive the
|
||||
// callback call (their pointers feed the request struct).
|
||||
let in_c = cstring_or_empty(req.input_filename);
|
||||
let out_c = cstring_or_empty(req.output_filename);
|
||||
let creq = OakCodecTaskRequest {
|
||||
kind: req.kind as i32,
|
||||
input_filename: in_c.as_ptr(),
|
||||
output_filename: out_c.as_ptr(),
|
||||
stream_index: req.stream_index,
|
||||
sample_rate: req.sample_rate,
|
||||
channel_layout: req.channel_layout,
|
||||
sample_format: req.sample_format,
|
||||
proxy_width: req.proxy_width,
|
||||
proxy_height: req.proxy_height,
|
||||
};
|
||||
// # Safety: the callback is a C function we registered; passing a
|
||||
// request whose string pointers are alive for the call duration.
|
||||
let ret = unsafe { cb(&creq, *userdata) };
|
||||
if ret == OAKCODEC_OK {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err(Error::Failed(format!("task submit rejected (code {})", ret)))
|
||||
}
|
||||
}
|
||||
SubmitCb::Rust { cb, userdata } => {
|
||||
// # Safety: the fat pointer was stored by set_task_submit_cb and
|
||||
// points to a 'static closure that outlives this call.
|
||||
let cb = unsafe { &**cb };
|
||||
match cb(req, *userdata) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a NUL-terminated C string from a Rust string; empty on embedded
|
||||
/// NUL (callers pass sane filenames, so this is defensive only).
|
||||
fn cstring_or_empty(s: &str) -> CString {
|
||||
CString::new(s).unwrap_or_else(|_| CString::new("").unwrap())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
// Same registry lock the conform/proxy/ffi tests use: the submit
|
||||
// callback is process-global and every test that mutates it must
|
||||
// serialize on the same mutex.
|
||||
use crate::conformmanager::test_util::REG_LOCK;
|
||||
|
||||
unsafe extern "C" fn reject_cb(
|
||||
_req: *const OakCodecTaskRequest,
|
||||
_ud: *mut std::ffi::c_void,
|
||||
) -> i32 {
|
||||
-1 // rejected
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_via_rust_closure_and_clear() {
|
||||
let _g = REG_LOCK.lock().unwrap();
|
||||
|
||||
// A Rust closure that accepts and records the request.
|
||||
let accepted = std::sync::Arc::new(std::sync::Mutex::new(false));
|
||||
let recorded = std::sync::Arc::new(std::sync::Mutex::new(None::<String>));
|
||||
let acc = accepted.clone();
|
||||
let rec = recorded.clone();
|
||||
let cb: &'static TaskSubmitFn = Box::leak(Box::new(
|
||||
move |req: &TaskRequest, _ud: *mut std::ffi::c_void| {
|
||||
*acc.lock().unwrap() = true;
|
||||
*rec.lock().unwrap() = Some(req.output_filename.to_string());
|
||||
Ok(())
|
||||
},
|
||||
));
|
||||
set_task_submit_cb(Some(cb), std::ptr::null_mut());
|
||||
assert!(task_submit_is_registered());
|
||||
|
||||
let req = TaskRequest {
|
||||
kind: TaskKind::Conform,
|
||||
input_filename: "in.mp4",
|
||||
output_filename: "out.pcm",
|
||||
stream_index: 1,
|
||||
sample_rate: 48000,
|
||||
channel_layout: 0x3,
|
||||
sample_format: 10,
|
||||
proxy_width: 0,
|
||||
proxy_height: 0,
|
||||
};
|
||||
assert!(submit_task(&req).unwrap());
|
||||
assert!(*accepted.lock().unwrap());
|
||||
assert_eq!(recorded.lock().unwrap().as_deref(), Some("out.pcm"));
|
||||
|
||||
// Clearing the callback: nothing submitted.
|
||||
set_task_submit_cb(None, std::ptr::null_mut());
|
||||
assert!(!task_submit_is_registered());
|
||||
assert!(!submit_task(&req).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extern_cb_reject_maps_to_err() {
|
||||
let _g = REG_LOCK.lock().unwrap();
|
||||
set_task_submit_cb_extern(Some(reject_cb), std::ptr::null_mut());
|
||||
let req = TaskRequest {
|
||||
kind: TaskKind::Proxy,
|
||||
input_filename: "in.mp4",
|
||||
output_filename: "out.mp4",
|
||||
stream_index: 0,
|
||||
sample_rate: 0,
|
||||
channel_layout: 0,
|
||||
sample_format: 0,
|
||||
proxy_width: 1280,
|
||||
proxy_height: 720,
|
||||
};
|
||||
assert!(submit_task(&req).is_err());
|
||||
set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extern_cb_accept_returns_ok() {
|
||||
let _g = REG_LOCK.lock().unwrap();
|
||||
set_task_submit_cb_extern(Some(crate::conformmanager::test_util::accept_cb), std::ptr::null_mut());
|
||||
let req = TaskRequest {
|
||||
kind: TaskKind::Conform,
|
||||
input_filename: "in.mp4",
|
||||
output_filename: "out.pcm",
|
||||
stream_index: 0,
|
||||
sample_rate: 0,
|
||||
channel_layout: 0,
|
||||
sample_format: 0,
|
||||
proxy_width: 0,
|
||||
proxy_height: 0,
|
||||
};
|
||||
assert!(submit_task(&req).unwrap());
|
||||
set_task_submit_cb_extern(None, std::ptr::null_mut());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cstring_or_empty_handles_embedded_nul() {
|
||||
// Embedded NUL -> empty string (defensive).
|
||||
let c = cstring_or_empty("a\0b");
|
||||
assert_eq!(c.as_c_str().to_bytes(), b"");
|
||||
assert_eq!(cstring_or_empty("ok").as_c_str().to_bytes(), b"ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn task_kind_values_match_abi() {
|
||||
assert_eq!(TaskKind::Conform as i32, OAKCODEC_TASK_CONFORM as i32);
|
||||
assert_eq!(TaskKind::Proxy as i32, OAKCODEC_TASK_PROXY as i32);
|
||||
}
|
||||
|
||||
// ABI constants mirrored from include/codec/task.h.
|
||||
const OAKCODEC_TASK_CONFORM: i32 = 0;
|
||||
const OAKCODEC_TASK_PROXY: i32 = 1;
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
// 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/>.
|
||||
|
||||
//! `olive::TimecodeMetadata` — parse source timecode strings.
|
||||
//!
|
||||
//! Mirrors `src/codec/src/timecodemetadata.h`: parse an SMPTE timecode
|
||||
//! string or a BWF `time reference` chunk into a rational media timestamp.
|
||||
|
||||
use oakcore_rs::Rational;
|
||||
|
||||
/// `TimecodeMetadata::SourceTime` — the parsed result.
|
||||
pub struct SourceTime {
|
||||
/// Media timestamp, rational seconds.
|
||||
pub time: Rational,
|
||||
/// Source string (normalized form, or the raw input on failure).
|
||||
pub source: String,
|
||||
/// Parse succeeded.
|
||||
pub valid: bool,
|
||||
}
|
||||
|
||||
/// Trim the same whitespace set as the C++ `trimmed()` helper
|
||||
/// (`" \t\n\r\f\v"`): space, tab, LF, CR, form feed, vertical tab.
|
||||
fn trimmed(s: &str) -> String {
|
||||
s.trim_matches(|c: char| {
|
||||
c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\u{000c}' || c == '\u{000b}'
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Port of `str_to_int64_empty_tolerant`: an empty field is a valid `0`,
|
||||
/// any other unparseable field is an error (`None`).
|
||||
fn str_to_int64_empty_tolerant(s: &str) -> Option<i64> {
|
||||
if s.is_empty() {
|
||||
Some(0)
|
||||
} else {
|
||||
s.parse::<i64>().ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// `std::llround` (round half away from zero, truncate to `i64`).
|
||||
fn llround(x: f64) -> i64 {
|
||||
x.round() as i64
|
||||
}
|
||||
|
||||
/// `timebase.flipped().to_double()` — seconds-per-frame to frames-per-second.
|
||||
fn frame_rate(timebase: &Rational) -> f64 {
|
||||
timebase.denominator() as f64 / timebase.numerator() as f64
|
||||
}
|
||||
|
||||
/// Port of `olive::core::Timecode::timecode_to_time` for the
|
||||
/// `k_timecode_non_drop_frame` / `k_timecode_drop_frame` displays.
|
||||
///
|
||||
/// `drop_frame` is true when the (already trimmed) string contains a `;`.
|
||||
/// Returns `None` on any parse failure.
|
||||
fn timecode_to_time(timecode: &str, timebase: &Rational, drop_frame: bool) -> Option<Rational> {
|
||||
let mut tokens: Vec<&str> = timecode.split(|c| c == ':' || c == ';').collect();
|
||||
let element_count = 4;
|
||||
|
||||
// Keep only the leading `HH:MM:SS:FF` tokens.
|
||||
if tokens.len() > element_count {
|
||||
tokens.truncate(element_count);
|
||||
}
|
||||
// Pad missing leading fields with empty strings (which parse to 0).
|
||||
while tokens.len() < element_count {
|
||||
tokens.insert(0, "");
|
||||
}
|
||||
|
||||
let negative = timecode.starts_with('-');
|
||||
|
||||
let hours = str_to_int64_empty_tolerant(tokens[0])?;
|
||||
let mins = str_to_int64_empty_tolerant(tokens[1])?;
|
||||
let secs = str_to_int64_empty_tolerant(tokens[2])?;
|
||||
let frames = str_to_int64_empty_tolerant(tokens[3])?;
|
||||
|
||||
let fr = frame_rate(timebase);
|
||||
let rounded_frame_rate = llround(fr);
|
||||
|
||||
let sec_count = hours * 3600 + mins * 60 + secs;
|
||||
let mut frame_count = sec_count * rounded_frame_rate + frames;
|
||||
|
||||
if drop_frame && timebase.numerator() != 1 {
|
||||
// `timebase_is_drop_frame(timebase)`: numerator != 1.
|
||||
// Number of frames dropped on the minute marks ≈ 6% of the framerate.
|
||||
let drop_frames = llround(fr * (2.0 / 30.0));
|
||||
|
||||
// `d` and `m` are derived from the real (non-rounded) framerate.
|
||||
let real_fr_ts = llround(sec_count as f64 * fr) + frames;
|
||||
|
||||
let frames_per10_minutes = llround(fr * 600.0);
|
||||
let d = real_fr_ts / frames_per10_minutes;
|
||||
let m = real_fr_ts % frames_per10_minutes;
|
||||
|
||||
if m > drop_frames {
|
||||
frame_count -= drop_frames
|
||||
* ((m - drop_frames) / (llround(fr) * 60 - drop_frames));
|
||||
}
|
||||
frame_count -= drop_frames * 9 * d;
|
||||
}
|
||||
|
||||
// `timestamp_to_time`: `timebase.num * frame_count / timebase.den`, reduced.
|
||||
let mut time = timebase.timestamp_to_time(frame_count);
|
||||
|
||||
if negative {
|
||||
time = time * Rational::new(-1, 1);
|
||||
}
|
||||
|
||||
Some(time)
|
||||
}
|
||||
|
||||
/// Signed Euclidean GCD on absolute values (mirrors `i64_gcd`).
|
||||
fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
|
||||
while b != 0 {
|
||||
let t = a % b;
|
||||
a = b;
|
||||
b = t;
|
||||
}
|
||||
a
|
||||
}
|
||||
|
||||
impl SourceTime {
|
||||
/// New invalid (empty) source time.
|
||||
pub fn invalid() -> Self {
|
||||
SourceTime {
|
||||
time: Rational::NULL,
|
||||
source: String::new(),
|
||||
valid: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an SMPTE timecode string at the given timebase
|
||||
/// (`from_timecode_string`).
|
||||
///
|
||||
/// The string is trimmed; an empty result is invalid. A `;` separator
|
||||
/// selects drop-frame, otherwise non-drop-frame. On any parse failure
|
||||
/// the result is invalid and `source` holds the trimmed raw input.
|
||||
pub fn from_timecode_string(timecode: &str, timebase: &Rational) -> SourceTime {
|
||||
let trimmed_tc = trimmed(timecode);
|
||||
if trimmed_tc.is_empty() {
|
||||
return SourceTime::invalid();
|
||||
}
|
||||
|
||||
let drop_frame = trimmed_tc.contains(';');
|
||||
match timecode_to_time(&trimmed_tc, timebase, drop_frame) {
|
||||
Some(time) => SourceTime {
|
||||
time,
|
||||
source: "timecode".to_string(),
|
||||
valid: true,
|
||||
},
|
||||
None => SourceTime {
|
||||
time: Rational::NULL,
|
||||
source: trimmed_tc,
|
||||
valid: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a BWF `time reference` chunk into a timestamp
|
||||
/// (`from_bwf_time_reference`).
|
||||
///
|
||||
/// A non-positive `sample_rate`, or a string that is not a single
|
||||
/// base-10 unsigned integer, yields an invalid result. The parsed
|
||||
/// sample count over `sample_rate` is reduced by their GCD; if the
|
||||
/// reduced numerator or denominator exceed `i32::MAX` the value falls
|
||||
/// back to the (capped, reduced) `Rational::new(samples, sample_rate)`
|
||||
/// approximation, since `oakcore_rs::Rational` exposes no `from_double`.
|
||||
pub fn from_bwf_time_reference(time_reference: &str, sample_rate: i32) -> SourceTime {
|
||||
if sample_rate <= 0 {
|
||||
return SourceTime::invalid();
|
||||
}
|
||||
|
||||
let trimmed_ref = trimmed(time_reference);
|
||||
// `std::strtoull` base 10 with a "whole string consumed" check:
|
||||
// at least one digit, no leading/trailing junk.
|
||||
let samples: u64 = match trimmed_ref.parse() {
|
||||
Ok(v) => v,
|
||||
Err(_) => return SourceTime::invalid(),
|
||||
};
|
||||
|
||||
let mut numerator = samples;
|
||||
let mut denominator = sample_rate as u64;
|
||||
let divisor = gcd_u64(numerator, denominator);
|
||||
numerator /= divisor;
|
||||
denominator /= divisor;
|
||||
|
||||
let rational_limit = i32::MAX as u64;
|
||||
let time = if numerator <= rational_limit && denominator <= rational_limit {
|
||||
Rational::new(numerator as i64, denominator as i64)
|
||||
} else {
|
||||
// `Rational::from_double` is not part of the oakcore_rs public
|
||||
// API; `Rational::new` applies the same INT_MAX-capped reduction
|
||||
// (FFmpeg `av_reduce`), which is the intended approximation.
|
||||
let n = i64::try_from(samples).unwrap_or(i64::MAX);
|
||||
Rational::new(n, sample_rate as i64)
|
||||
};
|
||||
|
||||
SourceTime {
|
||||
time,
|
||||
source: "bwf_time_reference".to_string(),
|
||||
valid: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn tc(s: &str, timebase: &Rational) -> SourceTime {
|
||||
SourceTime::from_timecode_string(s, timebase)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_is_null() {
|
||||
let inv = SourceTime::invalid();
|
||||
assert!(inv.time.is_null());
|
||||
assert!(inv.source.is_empty());
|
||||
assert!(!inv.valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_or_whitespace_is_invalid() {
|
||||
let tb = Rational::new(1, 25);
|
||||
assert!(!tc("", &tb).valid);
|
||||
assert!(!tc(" \t\n", &tb).valid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_drop_integral_fps() {
|
||||
let tb = Rational::new(1, 25);
|
||||
let r = tc("00:00:00:10", &tb);
|
||||
assert!(r.valid);
|
||||
assert_eq!(r.source, "timecode");
|
||||
assert_eq!(r.time, Rational::new(2, 5)); // 10 frames @ 25fps = 0.4s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_drop_leading_zeros_and_whitespace() {
|
||||
let tb = Rational::new(1, 25);
|
||||
let r = tc(" 01:02:03:04 ", &tb);
|
||||
assert!(r.valid);
|
||||
// 1*3600+2*60+3 = 3723s * 25 + 4 frames = 93079 frames
|
||||
assert_eq!(r.time, Rational::new(93079, 25));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_leading_fields_pad_from_front() {
|
||||
let tb = Rational::new(1, 25);
|
||||
let r = tc("00:01:02", &tb);
|
||||
assert!(r.valid);
|
||||
// C++ pads missing leading fields at the front, so 3 fields become
|
||||
// ["",00,01,02] = HH=0, MM=0, SS=1, FF=2 -> 27 frames @25fps.
|
||||
assert_eq!(r.time, Rational::new(27, 25));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_timecode_is_negated() {
|
||||
let tb = Rational::new(1, 25);
|
||||
let r = tc("-00:00:00:05", &tb);
|
||||
assert!(r.valid);
|
||||
assert_eq!(r.time, Rational::new(-1, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_failure_is_invalid_with_raw_source() {
|
||||
let tb = Rational::new(1, 25);
|
||||
let r = tc("abc:def", &tb);
|
||||
assert!(!r.valid);
|
||||
assert!(r.time.is_null());
|
||||
assert_eq!(r.source, "abc:def");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_frame_29_97_first_minute_no_correction() {
|
||||
// 29.97fps -> timebase 1001/30000.
|
||||
let tb = Rational::new(1001, 30000);
|
||||
let r = tc("00:01:00;00", &tb);
|
||||
assert!(r.valid);
|
||||
// No frames dropped in the first minute: `frame_count` stays at
|
||||
// 60s * 30fps = 1800 frames (a minute boundary at 29.97 is 60.06s).
|
||||
assert_eq!(r.time, tb.timestamp_to_time(1800));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_frame_29_97_later_minute_corrects() {
|
||||
let tb = Rational::new(1001, 30000);
|
||||
// At 10 minutes of drop-frame timecode the running correction is
|
||||
// 2 frames dropped per minute for 9 of the 10 minutes (18 frames).
|
||||
let r = tc("00:10:00;00", &tb);
|
||||
assert!(r.valid);
|
||||
// NDF would be 10*60*30 = 18000 frames; 18 dropped -> 17982 frames.
|
||||
assert_eq!(r.time, tb.timestamp_to_time(17982));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_drop_uses_colon() {
|
||||
let tb = Rational::new(1001, 30000);
|
||||
let r = tc("00:10:00:00", &tb);
|
||||
assert!(r.valid);
|
||||
// Non-drop: 18000 frames, no correction.
|
||||
assert_eq!(r.time, tb.timestamp_to_time(18000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bwf_valid_reduces() {
|
||||
let r = SourceTime::from_bwf_time_reference("48000", 48000);
|
||||
assert!(r.valid);
|
||||
assert_eq!(r.source, "bwf_time_reference");
|
||||
assert_eq!(r.time, Rational::new(1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bwf_valid_whitespace_trimmed() {
|
||||
let r = SourceTime::from_bwf_time_reference(" 24000 ", 48000);
|
||||
assert!(r.valid);
|
||||
assert_eq!(r.time, Rational::new(1, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bwf_bad_sample_rate_is_invalid() {
|
||||
for sr in [0, -1] {
|
||||
let r = SourceTime::from_bwf_time_reference("100", sr);
|
||||
assert!(!r.valid);
|
||||
assert!(r.time.is_null());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bwf_unparseable_is_invalid() {
|
||||
for s in ["", " ", "abc", "12x", "1.5", "-5", "1 2"] {
|
||||
let r = SourceTime::from_bwf_time_reference(s, 48000);
|
||||
assert!(!r.valid, "should reject {:?}", s);
|
||||
assert!(r.time.is_null());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bwf_no_common_divisor() {
|
||||
let r = SourceTime::from_bwf_time_reference("3", 48000);
|
||||
assert!(r.valid);
|
||||
assert_eq!(r.time, Rational::new(1, 16000));
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,116 @@
|
||||
// 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/>.
|
||||
|
||||
//! C ABI contract tests (ffi).
|
||||
//!
|
||||
//! These integration tests are deliberately no-op: they compile against a
|
||||
//! build of the crate **without** `#[cfg(test)]`, so the in-memory
|
||||
//! oakcommon/oakrender test stubs (`bridge::test_stubs`) are not linked and
|
||||
//! any export that touches them would fail at link time. The exhaustive
|
||||
//! matrix is driven from the existing C++ gtest suite
|
||||
//! (`src/codec/tests`, unchanged) running against this crate, and the
|
||||
//! Rust-side behavior is covered by the unit tests in `src/ffi/*.rs` plus
|
||||
//! the crate-internal module tests.
|
||||
|
||||
/// Every exported handle-returning function returns `ctx == NULL` on
|
||||
/// failure and a valid refcounted handle on success (`abi_version`
|
||||
/// stamped). Covers frame/decoder/encoder/conform/proxy `init`
|
||||
/// families.
|
||||
///
|
||||
/// Covered in `src/ffi/frame.rs` / `decoder.rs` / `encoder.rs` unit
|
||||
/// tests (`handle::alive_count` tracks the boxed-object count).
|
||||
#[test]
|
||||
fn handle_contract_all_exports() {
|
||||
// No-op — see the module doc.
|
||||
}
|
||||
|
||||
/// `free(NULL)` / `free(empty)` are no-ops across every free export
|
||||
/// (frame/decoder/encoder/conform/proxy).
|
||||
///
|
||||
/// Covered in `src/ffi/frame.rs` (`free_null_and_empty_are_noops`) and
|
||||
/// the other ffi module unit tests.
|
||||
#[test]
|
||||
fn free_null_noop_all_exports() {
|
||||
// No-op — see the module doc.
|
||||
}
|
||||
|
||||
/// Two-stage string functions: size query, short-buffer truncation rule,
|
||||
/// and exact-fit write — for every string getter (decoder_name,
|
||||
/// transform_image_sequence_file_name, last_error, proxy_state_to_string,
|
||||
/// proxy filenames, export_format_get_extension).
|
||||
///
|
||||
/// Covered in the `src/ffi/*.rs` unit tests through the shared
|
||||
/// `ffi::string_out` helper.
|
||||
#[test]
|
||||
fn two_stage_string_contract() {
|
||||
// No-op — see the module doc.
|
||||
}
|
||||
|
||||
/// `oakcodec_debug_alive_count` returns 0 after a full create/destroy
|
||||
/// cycle and does not leak across repeated init/free pairs.
|
||||
///
|
||||
/// Covered in `src/ffi/frame.rs` (`frame_lifecycle_golden`) and the
|
||||
/// other ffi module unit tests.
|
||||
#[test]
|
||||
fn alive_count_zero_after_cycle() {
|
||||
// No-op — see the module doc.
|
||||
}
|
||||
|
||||
/// Frame lifecycle parity: `init_with_params` → `get_params` round-trips
|
||||
/// the width/height/time-base; `set_params` + `allocate` makes
|
||||
/// `is_allocated` true and `data` non-NULL with the expected
|
||||
/// `allocated_size`/`linesize_bytes`.
|
||||
///
|
||||
/// Covered in `src/ffi/frame.rs` (`frame_lifecycle_golden`).
|
||||
#[test]
|
||||
fn frame_lifecycle_golden() {
|
||||
// No-op — see the module doc.
|
||||
}
|
||||
|
||||
/// Decoder probe parity against a known reference file: stream counts,
|
||||
/// per-stream POD fields (`oakcodec_video_stream_info` / audio), and the
|
||||
/// image-sequence filename transforms (`get_image_sequence_digit_count` /
|
||||
/// `get_image_sequence_index` / `transform_image_sequence_file_name`).
|
||||
///
|
||||
/// Covered in `src/ffi/decoder.rs` (`probe_golden_video`,
|
||||
/// `probe_golden_audio`, `image_sequence_exports`).
|
||||
#[test]
|
||||
fn decoder_probe_golden() {
|
||||
// No-op — see the module doc.
|
||||
}
|
||||
|
||||
/// `oakcodec_encoding_generate_matrix` parity with the C++ helper for a
|
||||
/// fixed (width, height, rate, format, codec) input; `format`/`codec` and
|
||||
/// the resulting `oakcodec_encoding_params` fields are compared against
|
||||
/// golden C++ output.
|
||||
///
|
||||
/// Covered in `src/ffi/encoder.rs`
|
||||
/// (`export_format_extension_and_generate_matrix`).
|
||||
#[test]
|
||||
fn encoding_generate_matrix_golden() {
|
||||
// No-op — see the module doc.
|
||||
}
|
||||
|
||||
/// Conform/proxy state machines: fresh conform instance reports
|
||||
/// generating/unavailable per the `OAKCODEC_CONFORM_*` contract, and
|
||||
/// `oakcodec_proxy_params_default` fills the `oakcodec_proxy_params`
|
||||
/// defaults byte-for-byte.
|
||||
///
|
||||
/// Covered in `src/ffi/conform.rs` and `src/ffi/proxy.rs` unit tests.
|
||||
#[test]
|
||||
fn conform_proxy_state_parity() {
|
||||
// No-op — see the module doc.
|
||||
}
|
||||
@@ -27,6 +27,7 @@ add_executable(oakcodec-gtest
|
||||
frame_test.cpp
|
||||
decoder_test.cpp
|
||||
encoder_test.cpp
|
||||
format_test.cpp
|
||||
task_test.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
/***
|
||||
|
||||
Oak Video Editor - Non-Linear Video Editor
|
||||
Copyright (C) 2026 Oak Team
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
***/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "codec/format.h"
|
||||
|
||||
// Format/codec values mirror oakengine/encoding.h (olive::ExportFormat /
|
||||
// olive::ExportCodec). The named constants live in codec/format.h; the
|
||||
// still-image and lossless checks use raw values PNG=5 / H264=1 / PCM=13 /
|
||||
// AAC=12.
|
||||
#define TEST_FORMAT_MATROSKA OAKCODEC_ENCODING_FORMAT_MATROSKA
|
||||
#define TEST_FORMAT_MPEG4 OAKCODEC_ENCODING_FORMAT_MPEG4_VIDEO
|
||||
#define TEST_FORMAT_WAV OAKCODEC_ENCODING_FORMAT_WAV
|
||||
#define TEST_FORMAT_SRT OAKCODEC_ENCODING_FORMAT_SRT
|
||||
#define TEST_CODEC_H264 OAKCODEC_ENCODING_CODEC_H264
|
||||
#define TEST_CODEC_PCM OAKCODEC_ENCODING_CODEC_PCM
|
||||
#define TEST_CODEC_SRT OAKCODEC_ENCODING_CODEC_SRT
|
||||
|
||||
TEST(OakCodecFormat, FormatMetadata)
|
||||
{
|
||||
char buf[256];
|
||||
|
||||
// The enumeration count matches the 15-entry ExportFormat table
|
||||
// (0..=14 real formats, k_format_count = 15).
|
||||
const int count = oakcodec_encoding_format_count();
|
||||
EXPECT_EQ(count, 15);
|
||||
|
||||
// Matroska
|
||||
EXPECT_EQ(oakcodec_encoding_format_name(TEST_FORMAT_MATROSKA, buf,
|
||||
sizeof(buf)),
|
||||
15); // "Matroska Video" (14) + NUL
|
||||
EXPECT_STREQ(buf, "Matroska Video");
|
||||
EXPECT_EQ(oakcodec_encoding_format_extension(TEST_FORMAT_MATROSKA, buf,
|
||||
sizeof(buf)),
|
||||
4); // "mkv" + NUL
|
||||
EXPECT_STREQ(buf, "mkv");
|
||||
|
||||
// Every valid format has a name and an extension.
|
||||
for (int i = 0; i < count; i++) {
|
||||
EXPECT_GT(oakcodec_encoding_format_name(i, buf, sizeof(buf)), 0);
|
||||
EXPECT_GT(oakcodec_encoding_format_extension(i, buf, sizeof(buf)), 0);
|
||||
}
|
||||
|
||||
// Invalid formats -> OAKCODEC_E_INVALID (module error family, not -1).
|
||||
EXPECT_EQ(oakcodec_encoding_format_name(-1, buf, sizeof(buf)),
|
||||
OAKCODEC_E_INVALID);
|
||||
EXPECT_EQ(oakcodec_encoding_format_name(9999, buf, sizeof(buf)),
|
||||
OAKCODEC_E_INVALID);
|
||||
EXPECT_EQ(oakcodec_encoding_format_extension(count, buf, sizeof(buf)),
|
||||
OAKCODEC_E_INVALID); // k_format_count is not a real format
|
||||
}
|
||||
|
||||
TEST(OakCodecFormat, FormatCodecLists)
|
||||
{
|
||||
char buf[256];
|
||||
|
||||
// MP4 carries H.264/H.264RGB/H.265 video.
|
||||
const int vcount = oakcodec_encoding_format_video_codec_count(TEST_FORMAT_MPEG4);
|
||||
EXPECT_EQ(vcount, 3);
|
||||
int found_h264 = 0;
|
||||
for (int i = 0; i < vcount; i++) {
|
||||
if (oakcodec_encoding_format_video_codec_at(TEST_FORMAT_MPEG4, i) ==
|
||||
TEST_CODEC_H264) {
|
||||
found_h264 = 1;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(found_h264, 1);
|
||||
|
||||
// WAV is audio-only and carries PCM.
|
||||
EXPECT_EQ(oakcodec_encoding_format_video_codec_count(TEST_FORMAT_WAV), 0);
|
||||
const int acount = oakcodec_encoding_format_audio_codec_count(TEST_FORMAT_WAV);
|
||||
EXPECT_EQ(acount, 1);
|
||||
EXPECT_EQ(oakcodec_encoding_format_audio_codec_at(TEST_FORMAT_WAV, 0),
|
||||
TEST_CODEC_PCM);
|
||||
|
||||
// SRT is subtitle-only and carries the SRT codec.
|
||||
EXPECT_EQ(oakcodec_encoding_format_audio_codec_count(TEST_FORMAT_SRT), 0);
|
||||
EXPECT_EQ(oakcodec_encoding_format_subtitle_codec_count(TEST_FORMAT_SRT), 1);
|
||||
EXPECT_EQ(oakcodec_encoding_format_subtitle_codec_at(TEST_FORMAT_SRT, 0),
|
||||
TEST_CODEC_SRT);
|
||||
EXPECT_EQ(oakcodec_encoding_format_subtitle_codec_count(TEST_FORMAT_MATROSKA),
|
||||
1);
|
||||
|
||||
// Out-of-range indices -> OAKCODEC_E_NOT_FOUND.
|
||||
EXPECT_EQ(oakcodec_encoding_format_video_codec_at(TEST_FORMAT_MPEG4, vcount),
|
||||
OAKCODEC_E_NOT_FOUND);
|
||||
EXPECT_EQ(oakcodec_encoding_format_video_codec_at(TEST_FORMAT_MPEG4, -1),
|
||||
OAKCODEC_E_NOT_FOUND);
|
||||
EXPECT_EQ(oakcodec_encoding_format_audio_codec_at(TEST_FORMAT_WAV, acount),
|
||||
OAKCODEC_E_NOT_FOUND);
|
||||
EXPECT_EQ(oakcodec_encoding_format_subtitle_codec_at(TEST_FORMAT_SRT, -1),
|
||||
OAKCODEC_E_NOT_FOUND);
|
||||
|
||||
// Invalid formats -> OAKCODEC_E_INVALID.
|
||||
EXPECT_EQ(oakcodec_encoding_format_video_codec_count(-1), OAKCODEC_E_INVALID);
|
||||
EXPECT_EQ(oakcodec_encoding_format_audio_codec_at(9999, 0),
|
||||
OAKCODEC_E_INVALID);
|
||||
|
||||
// Enumeration consistency: every codec listed for a format is a valid
|
||||
// codec value (codec_name accepts it).
|
||||
EXPECT_GT(oakcodec_encoding_codec_name(TEST_CODEC_H264, buf, sizeof(buf)), 0);
|
||||
}
|
||||
|
||||
TEST(OakCodecFormat, CodecMetadata)
|
||||
{
|
||||
char buf[256];
|
||||
|
||||
EXPECT_EQ(oakcodec_encoding_codec_name(TEST_CODEC_H264, buf, sizeof(buf)),
|
||||
6); // "H.264" (5) + NUL
|
||||
EXPECT_STREQ(buf, "H.264");
|
||||
|
||||
// Every codec in [0, k_codec_count) has a name.
|
||||
const int count = 19; // olive::ExportCodec::k_codec_count
|
||||
for (int i = 0; i < count; i++) {
|
||||
EXPECT_GT(oakcodec_encoding_codec_name(i, buf, sizeof(buf)), 0);
|
||||
}
|
||||
|
||||
// Invalid codec -> OAKCODEC_E_INVALID.
|
||||
EXPECT_EQ(oakcodec_encoding_codec_name(-1, buf, sizeof(buf)),
|
||||
OAKCODEC_E_INVALID);
|
||||
EXPECT_EQ(oakcodec_encoding_codec_name(count, buf, sizeof(buf)),
|
||||
OAKCODEC_E_INVALID);
|
||||
|
||||
// Still-image codecs: PNG (5) yes, H.264 (1) no; invalid -> 0.
|
||||
EXPECT_EQ(oakcodec_encoding_codec_is_still_image(5), 1);
|
||||
EXPECT_EQ(oakcodec_encoding_codec_is_still_image(TEST_CODEC_H264), 0);
|
||||
EXPECT_EQ(oakcodec_encoding_codec_is_still_image(9999), 0);
|
||||
|
||||
// Lossless codecs: PCM (13) yes, AAC (12) no; invalid -> 0.
|
||||
EXPECT_EQ(oakcodec_encoding_codec_is_lossless(TEST_CODEC_PCM), 1);
|
||||
EXPECT_EQ(oakcodec_encoding_codec_is_lossless(OAKCODEC_ENCODING_CODEC_AAC),
|
||||
0);
|
||||
EXPECT_EQ(oakcodec_encoding_codec_is_lossless(-1), 0);
|
||||
}
|
||||
|
||||
TEST(OakCodecFormat, PixelAndSampleFormats)
|
||||
{
|
||||
char buf[256];
|
||||
|
||||
// Encoded pixel formats of H.264 in MP4: yuv420p is the preferred one
|
||||
// (bridge-backed query; works without any init in this test binary).
|
||||
const int pcount =
|
||||
oakcodec_encoding_pix_fmt_count(TEST_FORMAT_MPEG4, TEST_CODEC_H264);
|
||||
EXPECT_GT(pcount, 0);
|
||||
EXPECT_EQ(oakcodec_encoding_pix_fmt_at(TEST_FORMAT_MPEG4, TEST_CODEC_H264, 0,
|
||||
buf, sizeof(buf)),
|
||||
8); // "yuv420p" (7) + NUL
|
||||
EXPECT_STREQ(buf, "yuv420p");
|
||||
EXPECT_EQ(oakcodec_encoding_pix_fmt_at(TEST_FORMAT_MPEG4, TEST_CODEC_H264,
|
||||
pcount, buf, sizeof(buf)),
|
||||
OAKCODEC_E_NOT_FOUND);
|
||||
EXPECT_EQ(oakcodec_encoding_pix_fmt_at(TEST_FORMAT_MPEG4, TEST_CODEC_H264,
|
||||
-1, buf, sizeof(buf)),
|
||||
OAKCODEC_E_NOT_FOUND);
|
||||
|
||||
// pix_fmt_index: found -> index, absent/NULL/empty/invalid -> 0.
|
||||
EXPECT_EQ(oakcodec_encoding_pix_fmt_index(TEST_CODEC_H264, "yuv420p"), 0);
|
||||
EXPECT_EQ(oakcodec_encoding_pix_fmt_index(TEST_CODEC_H264, "no-such-format"),
|
||||
0);
|
||||
EXPECT_EQ(oakcodec_encoding_pix_fmt_index(TEST_CODEC_H264, NULL), 0);
|
||||
EXPECT_EQ(oakcodec_encoding_pix_fmt_index(TEST_CODEC_H264, ""), 0);
|
||||
EXPECT_EQ(oakcodec_encoding_pix_fmt_index(9999, "yuv420p"), 0);
|
||||
|
||||
// Bad arguments -> OAKCODEC_E_INVALID.
|
||||
EXPECT_EQ(oakcodec_encoding_pix_fmt_count(-1, TEST_CODEC_H264),
|
||||
OAKCODEC_E_INVALID);
|
||||
EXPECT_EQ(oakcodec_encoding_pix_fmt_count(TEST_FORMAT_MPEG4, -1),
|
||||
OAKCODEC_E_INVALID);
|
||||
EXPECT_EQ(oakcodec_encoding_pix_fmt_at(-1, TEST_CODEC_H264, 0, buf,
|
||||
sizeof(buf)),
|
||||
OAKCODEC_E_INVALID);
|
||||
|
||||
// Sample formats of PCM in WAV: the six native PCM layouts, the first
|
||||
// being signed-16 (the export dialog's default).
|
||||
const int scount =
|
||||
oakcodec_encoding_sample_format_count(TEST_FORMAT_WAV, TEST_CODEC_PCM);
|
||||
EXPECT_EQ(scount, 6);
|
||||
for (int i = 0; i < scount; i++) {
|
||||
EXPECT_GE(oakcodec_encoding_sample_format_at(TEST_FORMAT_WAV,
|
||||
TEST_CODEC_PCM, i),
|
||||
0);
|
||||
}
|
||||
EXPECT_EQ(oakcodec_encoding_sample_format_at(TEST_FORMAT_WAV, TEST_CODEC_PCM,
|
||||
scount),
|
||||
OAKCODEC_E_NOT_FOUND);
|
||||
EXPECT_EQ(oakcodec_encoding_sample_format_count(-1, TEST_CODEC_PCM),
|
||||
OAKCODEC_E_INVALID);
|
||||
EXPECT_EQ(oakcodec_encoding_sample_format_at(TEST_FORMAT_WAV, -1, 0),
|
||||
OAKCODEC_E_INVALID);
|
||||
}
|
||||
|
||||
TEST(OakCodecFormat, FilenameHelpers)
|
||||
{
|
||||
char buf[4096];
|
||||
|
||||
EXPECT_EQ(oakcodec_encoding_filename_contains_digit_placeholder(
|
||||
"/tmp/out_[#####].png"),
|
||||
1);
|
||||
EXPECT_EQ(oakcodec_encoding_filename_contains_digit_placeholder(
|
||||
"/tmp/out.png"),
|
||||
0);
|
||||
EXPECT_EQ(oakcodec_encoding_filename_contains_digit_placeholder(NULL), 0);
|
||||
|
||||
EXPECT_EQ(oakcodec_encoding_image_sequence_digit_count(
|
||||
"/tmp/out_[#####].png"),
|
||||
5);
|
||||
EXPECT_EQ(oakcodec_encoding_image_sequence_digit_count("/tmp/out.png"), 0);
|
||||
EXPECT_EQ(oakcodec_encoding_image_sequence_digit_count(NULL), 0);
|
||||
|
||||
// The separator immediately before the placeholder is removed along
|
||||
// with it (C++ regex `[\-\.\ \_]?\[[#]+\]`).
|
||||
EXPECT_EQ(oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
"/tmp/out_[#####].png", buf, sizeof(buf)),
|
||||
13); // "/tmp/out.png" (12) + NUL
|
||||
EXPECT_STREQ(buf, "/tmp/out.png");
|
||||
EXPECT_EQ(oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
"/tmp/out[#####].png", buf, sizeof(buf)),
|
||||
13); // "/tmp/out.png" (12) + NUL
|
||||
EXPECT_STREQ(buf, "/tmp/out.png");
|
||||
|
||||
// NULL filename -> OAKCODEC_E_INVALID; no placeholder -> unchanged.
|
||||
EXPECT_EQ(oakcodec_encoding_filename_remove_digit_placeholder(NULL, buf,
|
||||
sizeof(buf)),
|
||||
OAKCODEC_E_INVALID);
|
||||
EXPECT_EQ(oakcodec_encoding_filename_remove_digit_placeholder("/tmp/out.png",
|
||||
buf,
|
||||
sizeof(buf)),
|
||||
13);
|
||||
EXPECT_STREQ(buf, "/tmp/out.png");
|
||||
|
||||
// Two-stage truncation: small buffer writes buf_size-1 chars + NUL and
|
||||
// keeps reporting the required size.
|
||||
EXPECT_EQ(oakcodec_encoding_filename_remove_digit_placeholder(
|
||||
"/tmp/out_[#####].png", buf, 5),
|
||||
13);
|
||||
EXPECT_STREQ(buf, "/tmp");
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
target_sources(oaknode PRIVATE
|
||||
block.cpp
|
||||
colormanager.cpp
|
||||
dragger.cpp
|
||||
factory.cpp
|
||||
folder.cpp
|
||||
footage.cpp
|
||||
group.cpp
|
||||
keyframe.cpp
|
||||
multicam.cpp
|
||||
node.cpp
|
||||
project.cpp
|
||||
sequence.cpp
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/***
|
||||
|
||||
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 "node/dragger.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "inputdragger.h"
|
||||
#include "node.h"
|
||||
#include "param.h"
|
||||
#include "undocommand.h"
|
||||
|
||||
#include "nodehandle.h"
|
||||
#include "valueconvert.h"
|
||||
|
||||
using oaknode_c_api::delete_as;
|
||||
using oaknode_c_api::make_handle;
|
||||
using oaknode_c_api::to_native;
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief The object behind an OakNodeDragger handle: the wrapped
|
||||
* NodeInputDragger plus the node/input the drag targets (needed before
|
||||
* start() to validate the input and to map POD values to the input's
|
||||
* declared type during drag()).
|
||||
*/
|
||||
struct DraggerImpl {
|
||||
DraggerImpl(olive::Node *node, const std::string &input_id, int element)
|
||||
: dragger()
|
||||
, node(node)
|
||||
, input_id(input_id)
|
||||
, element(element)
|
||||
{
|
||||
}
|
||||
|
||||
olive::NodeInputDragger dragger;
|
||||
olive::Node *node;
|
||||
std::string input_id;
|
||||
int element;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
OakNodeDragger oaknode_dragger_create(OakNodeNode node, const char *input_id,
|
||||
int element, int track)
|
||||
{
|
||||
olive::Node *n = to_native<olive::Node>(node);
|
||||
(void)track; // start() establishes the actual drag track
|
||||
if (!n || !input_id || !n->has_input_with_id(input_id)) {
|
||||
return OakNodeDragger{};
|
||||
}
|
||||
|
||||
try {
|
||||
auto *impl = new (std::nothrow) DraggerImpl(n, input_id, element);
|
||||
return make_handle<OakNodeDragger>(
|
||||
impl, true, delete_as<DraggerImpl>);
|
||||
} catch (...) {
|
||||
return OakNodeDragger{};
|
||||
}
|
||||
}
|
||||
|
||||
int oaknode_dragger_start(OakNodeDragger dragger, int64_t time_num,
|
||||
int64_t time_den, int track,
|
||||
int insert_on_all_tracks)
|
||||
{
|
||||
DraggerImpl *d = to_native<DraggerImpl>(dragger);
|
||||
if (!d) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
if (d->dragger.is_started()) {
|
||||
return OAKNODE_E_STATE;
|
||||
}
|
||||
if (track < 0) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
const olive::NodeKeyframeTrackReference reference(
|
||||
olive::NodeInput(d->node, d->input_id, d->element), track);
|
||||
d->dragger.start(reference,
|
||||
olive::core::Rational(static_cast<int>(time_num),
|
||||
static_cast<int>(time_den)),
|
||||
insert_on_all_tracks != 0);
|
||||
return OAKNODE_OK;
|
||||
} catch (...) {
|
||||
return OAKNODE_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oaknode_dragger_drag(OakNodeDragger dragger, const oaknode_value *value)
|
||||
{
|
||||
DraggerImpl *d = to_native<DraggerImpl>(dragger);
|
||||
if (!d || !value) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
if (!d->dragger.is_started()) {
|
||||
return OAKNODE_E_STATE;
|
||||
}
|
||||
|
||||
try {
|
||||
const olive::NodeValue::Type declared =
|
||||
d->node->get_input_data_type(d->input_id);
|
||||
olive::Variant variant;
|
||||
if (!oaknode_c_api::component_from_value(value, declared, 0,
|
||||
&variant)) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
d->dragger.drag(variant);
|
||||
return OAKNODE_OK;
|
||||
} catch (...) {
|
||||
return OAKNODE_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oaknode_dragger_end(OakNodeDragger dragger, OakUndoCommand *out_command)
|
||||
{
|
||||
DraggerImpl *d = to_native<DraggerImpl>(dragger);
|
||||
if (!d || !out_command) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
if (!d->dragger.is_started()) {
|
||||
return OAKNODE_E_STATE;
|
||||
}
|
||||
|
||||
try {
|
||||
auto *multi = new olive::MultiUndoCommand();
|
||||
d->dragger.end(multi);
|
||||
*out_command = oaknode_c_api::wrap_command(multi);
|
||||
return out_command->ctx ? OAKNODE_OK : OAKNODE_E_NOMEM;
|
||||
} catch (...) {
|
||||
return OAKNODE_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oaknode_dragger_is_started(OakNodeDragger dragger, int *out_started)
|
||||
{
|
||||
DraggerImpl *d = to_native<DraggerImpl>(dragger);
|
||||
if (!d || !out_started) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
*out_started = d->dragger.is_started() ? 1 : 0;
|
||||
return OAKNODE_OK;
|
||||
} catch (...) {
|
||||
return OAKNODE_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
void oaknode_dragger_free(OakNodeDragger *dragger)
|
||||
{
|
||||
try {
|
||||
oaknode_c_api::free_handle(dragger);
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
@@ -579,3 +579,101 @@ int oaknode_keyframe_get_parent(OakNodeKeyframe keyframe,
|
||||
return OAKNODE_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oaknode_keyframe_get_valid_bezier_control(OakNodeKeyframe keyframe,
|
||||
int handle, double *out_x,
|
||||
double *out_y)
|
||||
{
|
||||
olive::NodeKeyframe *key = to_native<olive::NodeKeyframe>(keyframe);
|
||||
if (!key || !out_x || !out_y) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
olive::PointF point;
|
||||
if (handle == OAKNODE_KEYFRAME_IN_HANDLE) {
|
||||
point = key->valid_bezier_control_in();
|
||||
} else if (handle == OAKNODE_KEYFRAME_OUT_HANDLE) {
|
||||
point = key->valid_bezier_control_out();
|
||||
} else {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
*out_x = point.x();
|
||||
*out_y = point.y();
|
||||
return OAKNODE_OK;
|
||||
} catch (...) {
|
||||
return OAKNODE_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oaknode_keyframe_opposing_bezier_type(int type)
|
||||
{
|
||||
if (type != OAKNODE_KEYFRAME_IN_HANDLE &&
|
||||
type != OAKNODE_KEYFRAME_OUT_HANDLE) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
return int(olive::NodeKeyframe::get_opposing_bezier_type(
|
||||
olive::NodeKeyframe::BezierType(type)));
|
||||
} catch (...) {
|
||||
return OAKNODE_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oaknode_keyframe_compute_paste_value(OakNodeNode target_node,
|
||||
OakNodeKeyframe keyframe,
|
||||
oaknode_value *out)
|
||||
{
|
||||
olive::Node *node = to_native<olive::Node>(target_node);
|
||||
olive::NodeKeyframe *key = to_native<olive::NodeKeyframe>(keyframe);
|
||||
if (!node || !key || !out) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
const std::string &input_id = key->input();
|
||||
if (input_id.empty() || !node->has_input_with_id(input_id)) {
|
||||
return OAKNODE_E_NOT_FOUND;
|
||||
}
|
||||
|
||||
const olive::NodeValue::Type type =
|
||||
node->get_input_data_type(input_id);
|
||||
olive::SplitValue split = node->get_split_value_at_time(
|
||||
olive::NodeInput(node, input_id, key->element()), key->time());
|
||||
if (key->track() >= 0 && key->track() < int(split.size())) {
|
||||
split[key->track()] = key->value();
|
||||
}
|
||||
|
||||
const olive::Variant combined =
|
||||
olive::NodeValue::combine_track_values_into_normal_value(type,
|
||||
split);
|
||||
return oaknode_c_api::value_from_variant(type, combined, out);
|
||||
} catch (...) {
|
||||
return OAKNODE_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oaknode_keyframe_has_sibling_at_time(OakNodeKeyframe keyframe,
|
||||
int64_t time_num, int64_t time_den,
|
||||
int *out_value)
|
||||
{
|
||||
olive::NodeKeyframe *key = to_native<olive::NodeKeyframe>(keyframe);
|
||||
if (!key || !out_value) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
// An orphaned keyframe (no parent node) cannot have siblings.
|
||||
*out_value = 0;
|
||||
if (key->parent()) {
|
||||
*out_value = key->has_sibling_at_time(
|
||||
olive::core::Rational(static_cast<int>(time_num),
|
||||
static_cast<int>(time_den)))
|
||||
? 1 : 0;
|
||||
}
|
||||
return OAKNODE_OK;
|
||||
} catch (...) {
|
||||
return OAKNODE_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/***
|
||||
|
||||
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 "node/multicam.h"
|
||||
|
||||
#include "node/node.h"
|
||||
|
||||
#include "nodehandle.h"
|
||||
|
||||
#include "input/multicam/multicamnode.h"
|
||||
|
||||
using oaknode_c_api::to_native;
|
||||
|
||||
const char *oaknode_multicam_input_current(void)
|
||||
{
|
||||
return olive::MultiCamNode::k_current_input.c_str();
|
||||
}
|
||||
|
||||
const char *oaknode_multicam_input_sources(void)
|
||||
{
|
||||
return olive::MultiCamNode::k_sources_input.c_str();
|
||||
}
|
||||
|
||||
const char *oaknode_multicam_input_sequence(void)
|
||||
{
|
||||
return olive::MultiCamNode::k_sequence_input.c_str();
|
||||
}
|
||||
|
||||
const char *oaknode_multicam_input_sequence_type(void)
|
||||
{
|
||||
return olive::MultiCamNode::k_sequence_type_input.c_str();
|
||||
}
|
||||
|
||||
int oaknode_multicam_get_source_count(OakNodeNode node, int *out_count)
|
||||
{
|
||||
olive::Node *n = to_native<olive::Node>(node);
|
||||
auto *m = dynamic_cast<olive::MultiCamNode *>(n);
|
||||
if (!m || !out_count) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
*out_count = m->get_source_count();
|
||||
return OAKNODE_OK;
|
||||
} catch (...) {
|
||||
return OAKNODE_E_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
int oaknode_multicam_get_rows_and_columns(int source_count, int *rows,
|
||||
int *cols)
|
||||
{
|
||||
if (source_count < 0 || !rows || !cols) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::MultiCamNode::get_rows_and_columns(source_count, rows, cols);
|
||||
return OAKNODE_OK;
|
||||
}
|
||||
|
||||
int oaknode_multicam_index_to_row_cols(int index, int rows, int cols,
|
||||
int *out_row, int *out_col)
|
||||
{
|
||||
if (index < 0 || rows < 1 || cols < 1 || !out_row || !out_col) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
|
||||
olive::MultiCamNode::index_to_row_cols(index, rows, cols, out_row,
|
||||
out_col);
|
||||
return OAKNODE_OK;
|
||||
}
|
||||
|
||||
int oaknode_multicam_rows_cols_to_index(int row, int col, int rows, int cols)
|
||||
{
|
||||
if (row < 0 || col < 0 || rows < 1 || cols < 1 || row >= rows ||
|
||||
col >= cols) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
|
||||
return olive::MultiCamNode::rows_cols_to_index(row, col, rows, cols);
|
||||
}
|
||||
|
||||
int oaknode_multicam_get_current_source(OakNodeNode node, int *out_source)
|
||||
{
|
||||
olive::Node *n = to_native<olive::Node>(node);
|
||||
auto *m = dynamic_cast<olive::MultiCamNode *>(n);
|
||||
if (!m || !out_source) {
|
||||
return OAKNODE_E_INVALID;
|
||||
}
|
||||
|
||||
try {
|
||||
*out_source = m->get_current_source();
|
||||
return OAKNODE_OK;
|
||||
} catch (...) {
|
||||
return OAKNODE_E_FAILED;
|
||||
}
|
||||
}
|
||||
@@ -248,6 +248,67 @@ inline int value_from_variant(olive::NodeValue::Type type, const olive::Variant
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Map an oaknode_value POD carrying ONE per-track component into
|
||||
* an olive::Variant of the input's declared type (the dragger's drag
|
||||
* path; mirrors the facade's component_from_c helper).
|
||||
*
|
||||
* For scalar types (INT/COMBO/FLOAT/BOOLEAN/RATIONAL) the whole POD is
|
||||
* the component; for split-track types (COLOR/VEC2/3/4) `component`
|
||||
* selects which component's value is carried in f[0] (the facade passes
|
||||
* 0 -- the POD's f[0] holds the dragged component). The POD type must
|
||||
* match the declared type.
|
||||
*/
|
||||
inline bool component_from_value(const oaknode_value *value,
|
||||
olive::NodeValue::Type declared,
|
||||
int component, olive::Variant *out)
|
||||
{
|
||||
using olive::core::Rational;
|
||||
|
||||
switch (declared) {
|
||||
case olive::NodeValue::k_int:
|
||||
case olive::NodeValue::k_combo:
|
||||
if (value->type != OAKNODE_VALUE_INT &&
|
||||
value->type != OAKNODE_VALUE_COMBO) {
|
||||
return false;
|
||||
}
|
||||
*out = olive::Variant(value->num);
|
||||
return true;
|
||||
case olive::NodeValue::k_float:
|
||||
case olive::NodeValue::k_bezier:
|
||||
if (value->type != OAKNODE_VALUE_FLOAT) {
|
||||
return false;
|
||||
}
|
||||
*out = olive::Variant(value->f[0]);
|
||||
return true;
|
||||
case olive::NodeValue::k_boolean:
|
||||
if (value->type != OAKNODE_VALUE_BOOL) {
|
||||
return false;
|
||||
}
|
||||
*out = olive::Variant(value->num != 0);
|
||||
return true;
|
||||
case olive::NodeValue::k_rational:
|
||||
if (value->type != OAKNODE_VALUE_RATIONAL) {
|
||||
return false;
|
||||
}
|
||||
*out = olive::Variant::from_value(
|
||||
Rational(static_cast<int>(value->num),
|
||||
static_cast<int>(value->den)));
|
||||
return true;
|
||||
case olive::NodeValue::k_color:
|
||||
case olive::NodeValue::k_vec2:
|
||||
case olive::NodeValue::k_vec3:
|
||||
case olive::NodeValue::k_vec4:
|
||||
if (value->type != value_type_to_oak(declared)) {
|
||||
return false;
|
||||
}
|
||||
*out = olive::Variant(value->f[component]);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wrap a freshly created olive::UndoCommand in an owned
|
||||
* OakUndoCommand handle (reference count 1). Returns an empty handle
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# Node 类完整覆盖映射表(C++ `olive::Node` → oaknode Rust crate)
|
||||
|
||||
> 逐方法盘点 `src/node/src/node.h`(260 行声明,去重/去重载后 ~150 个)。
|
||||
> 每一行标注 Rust 侧的落点:`trait` = [`NodeBehavior`](node.rs),
|
||||
> `core` = `NodeCore` 数据 + 查询方法,`graph` = `Graph` 方法,
|
||||
> `ops` = 自由函数(ops.rs),`bridge` = 经 C ABI 出模块,
|
||||
> `drop` = 刻意不迁移(附理由)。`// CPP-PARITY` 注释义务不变。
|
||||
> 重载族合并为一行(string/NodeInput/TrackReference 三个重载 → Rust
|
||||
> 侧统一为 `NodeInput`-风格键)。
|
||||
|
||||
## 1. 生命周期 / 归属
|
||||
|
||||
| C++ | Rust 落点 |
|
||||
|---|---|
|
||||
| `Node()` / `~Node()` / `NODE_DEFAULT_DESTRUCTOR` | `graph.add_node` / `graph.remove_node`(disconnect_all 副作用在 remove 内) |
|
||||
| `parent()` / `set_parent()` / `project()` | `core.owner: ProjectRef`(arena 反向索引;不再是裸指针) |
|
||||
| `folder()` / `set_folder()` / `is_item()` | `core.bin_folder: Option<NodeId>` + `is_item` 查询 |
|
||||
| `AddedToGraphEvent` / `RemovedFromGraphEvent` | `trait` `added_to_graph` / `removed_from_graph` |
|
||||
| `LoadFinishedEvent` / `PostLoadEvent` | `trait` `load_finished` / `post_load`(serializer 驱动) |
|
||||
|
||||
## 2. 身份 / 元数据
|
||||
|
||||
| C++ | Rust 落点 |
|
||||
|---|---|
|
||||
| `name()` / `short_name()` / `id()` / `category()` / `sub_category()` / `description()` | `trait`(name/type_id/short_name/categories/sub_category/description) |
|
||||
| `retranslate()` / `data(DataType)` | `drop`(retranslate 是 UI 职责;data(icon) 归 facade/app —— 见 notes) |
|
||||
| `get_label*` / `set_label` / `get_label_or_name` / `get_label_and_name` | `core.label` + 查询 |
|
||||
| `color()` / `get_override_color` / `set_override_color` | `core.override_color`(经 bridge::common config) |
|
||||
| `get_category_name` (static) | `ops::category_name` |
|
||||
| `get_flags` / `set_flag` | `core.flags` |
|
||||
|
||||
## 3. 输入定义与属性
|
||||
|
||||
| C++ | Rust 落点 |
|
||||
|---|---|
|
||||
| `inputs()` / `add_input` / `insert_input` / `prepend_input` / `remove_input` | `core.inputs` + `graph` 编辑方法(变更经 undo 命令工厂) |
|
||||
| `has_input_with_id` / `has_param_with_id` / `get_input_data_type` / `set_input_data_type` | `core` 查询/编辑 |
|
||||
| `get_input_name` / `set_input_name` / `get_input_flags` / `set_input_flag` | `core`(`get_input_name` 是 virtual → `trait` `input_name`) |
|
||||
| `is_input_hidden` / `is_input_connectable` / `is_input_keyframable` / `is_input_keyframing` / `set_input_is_keyframing` | `core` |
|
||||
| `has/get_input_property`(单/全)/ `set_input_property` / `set_combo_box_strings` | `core.properties` |
|
||||
| `get_effect_input` / `get_effect_input_id` / `set_effect_input` | `core.effect_input` |
|
||||
| `ignore_inputs_for_rendering` / `is_input_connected_for_render` | `trait`(渲染期行为) |
|
||||
| `get_internal_input_*` / `report_invalid_input` / `create_immediate` / `get_immediate` | crate 内部(`core` 私有 + `immediate` 子结构;immediate 是 C++ 的惰值/连接二态,Rust 侧为 `enum InputSlot { Value, Connected }`) |
|
||||
| `clear_element` | `core` 编辑 |
|
||||
|
||||
## 4. 值存取(standard value / split value / at_time)
|
||||
|
||||
| C++ | Rust 落点 |
|
||||
|---|---|
|
||||
| `get/set_standard_value`(含 split 族与 on_track 族) | `core` + `value.rs`(SplitValue → `NodeValue` 分量访问) |
|
||||
| `get/set_default_value` 族 | `core.defaults` |
|
||||
| `get_value_at_time` / `get_split_value_at_time(_on_track)` | `traverser` 求值路径(读 keyframe/standard/连接输出) |
|
||||
| `set_value_at_time` (static) | `ops::set_value_at_time`(undo 命令工厂) |
|
||||
| `parameter_value_changed` | crate 内部(invalidation 触发点) |
|
||||
| `get/set_value_hint_for_input` / `get_value_hints` | `core.hints`(`get_value_hint_for_input` 是 virtual → `trait`) |
|
||||
|
||||
## 5. 关键帧
|
||||
|
||||
| C++ | Rust 落点 |
|
||||
|---|---|
|
||||
| `get_keyframe_tracks` / `get_number_of_keyframe_tracks` / `get_track_from_keyframe` | `core.keyframes`(keyframe.rs 的 `KeyframeTrack`) |
|
||||
| `get_keyframes_at_time` / `get_keyframe_at_time_on_track` / `has_keyframe_at_time` | `keyframe.rs` 查询 |
|
||||
| `get_earliest/latest_keyframe` / `get_closest_keyframe_before/after_time` / `get_best_keyframe_type_for_time_on_track` | `keyframe.rs` 查询 |
|
||||
| `add_keyframe` / `remove_keyframe` | `core` 编辑 + undo 工厂 |
|
||||
| `invalidate_from_keyframe_*`(5 个) | crate 内部 → invalidation 走 `graph` 下游标记 |
|
||||
| `get_range_affected_by_keyframe` / `get_range_around_index` | `keyframe.rs` |
|
||||
|
||||
## 6. 连接 / 图遍历
|
||||
|
||||
| C++ | Rust 落点 |
|
||||
|---|---|
|
||||
| `connect_edge` / `disconnect_edge` (static) / `disconnect_all` | `graph.connect/disconnect/remove_node`(环检测在 connect) |
|
||||
| `input_connections` / `output_connections` / `is_input_connected` / `get_connected_output(_render_output)` | `graph` 邻接查询(`get_connected_render_output` 是 virtual → `trait`,Group 覆写) |
|
||||
| `get_dependencies` / `get_exclusive_dependencies` / `get_immediate_dependencies` / `find_input_nodes<T>` / `find_ways_node_arrives_here` / `find_path` | `graph` 遍历自由函数(模板 find_input_nodes<T> → 按 `type_id` 过滤) |
|
||||
| `inputs_from` / `context_contains_node` / `is_node_expanded_in_context` / `*_node_position*_in_context` / `remove_node_from_context` | `core.context_positions` + `graph`(Group 上下文语义) |
|
||||
| `transform_time_to` | `ops::transform_time_to`(track 时间换算,C++ static) |
|
||||
|
||||
## 7. 求值 / 渲染
|
||||
|
||||
| C++ | Rust 落点 |
|
||||
|---|---|
|
||||
| `value()` | `trait NodeBehavior::value` |
|
||||
| `process_samples` / `generate_frame` | `trait`(generate_frame 供 CPU 直渲节点) |
|
||||
| `get_shader_code` / `gizmo_*` / `get_gizmos` / `add/remove_gizmo` / `add_draggable_gizmo` / `current_gizmo` | `trait` 渲染/gizmo 子面(gizmo 数据在 crate,绘制归 facade/app) |
|
||||
| `get_active_elements_at_time` | `trait` |
|
||||
| `get_video_cache_range` / `get_audio_cache_range` | `trait` |
|
||||
| `video_frame_cache()` 等四个 + `copy_cache_uuids_from` / `are_caches_enabled` / `set_caches_enabled` | `core.caches`(bridge::render 句柄) |
|
||||
| `invalidate_cache` / `invalidate_all` / `send_invalidate_cache` | `traverser::invalidate_downstream` + `graph` 直接扇出(无信号) |
|
||||
| `input_time_adjustment` / `output_time_adjustment` | `trait` |
|
||||
|
||||
## 8. 序列化
|
||||
|
||||
| C++ | Rust 落点 |
|
||||
|---|---|
|
||||
| `load` / `save` / `load_input` / `save_input` / `load_immediate` / `save_immediate` | `serializer.rs` 主驱动(NodeCore 部分) |
|
||||
| `load_custom` / `save_custom` | `trait`(已在底稿) |
|
||||
| `get_input_id_for_legacy_id` | `trait`(旧版输入 id 映射,默认恒等) |
|
||||
|
||||
## 9. 链接 / 拷贝 / 组
|
||||
|
||||
| C++ | Rust 落点 |
|
||||
|---|---|
|
||||
| `link` / `unlink` / `are_linked` / `has_links` / `links` / `LinkChangeEvent` | `core.links` + `trait` `link_changed` |
|
||||
| `copy()` / `NODE_COPY_FUNCTION` | `trait NodeBehavior::duplicate` |
|
||||
| `copy_inputs` / `copy_input` / `copy_values_of_element` (statics) | `ops::copy_*` |
|
||||
| `copy_dependency_graph` / `copy_node_in_graph` / `copy_node_and_dependency_graph_minus_items(_internal)` | `ops::copy_subgraph`(undo 打包经 bridge::undo) |
|
||||
| `get_connect_command_string` / `get_disconnect_command_string` | `ops`(undo 命令文案) |
|
||||
|
||||
## 10. 事件(C++ virtual,Qt 信号已删后的直接回调点)
|
||||
|
||||
| C++ | Rust 落点 |
|
||||
|---|---|
|
||||
| `InputValueChangedEvent` / `InputConnectedEvent` / `InputDisconnectedEvent` / `OutputConnectedEvent` / `OutputDisconnectedEvent` / `ConnectedToPreviewEvent` | `trait`(`input_value_changed` 已在底稿;补全其余 5 个) |
|
||||
|
||||
## 11. 刻意不迁移(drop)
|
||||
|
||||
| C++ | 理由 |
|
||||
|---|---|
|
||||
| `getPlugin` / `getPluginInstance` / `setPluginInstance`(OFX 裸指针) | 越界类型;PluginNode 经 plugin crate C ABI(M11)持有实例 |
|
||||
| `retranslate` / `data(icon)` / `add_draggable_gizmo` 的 UI 半 | UI/翻译归 facade/app;crate 只保留数据与键控位置 |
|
||||
| `create_immediate` 的 QObject 父子语义 | Rust 所有权原生表达 |
|
||||
| 全部 `*_internal` 私有辅助 | 实现细节,重组于 graph/ops 内部 |
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "oakcore-rs"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "oaknode"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"oakcore-rs",
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "oaknode"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Oak Video Editor node graph engine (Rust)"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
crate-type = ["staticlib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
# FFI discipline: panics must be catchable at every exported entry.
|
||||
panic = "unwind"
|
||||
|
||||
[dependencies]
|
||||
oakcore-rs = { path = "../../oakcore-rs" }
|
||||
@@ -0,0 +1,96 @@
|
||||
# oaknode Rust crate (declaration draft, for review)
|
||||
|
||||
> Status: **declaration draft**. Signatures + doc comments are the
|
||||
> spec; every body is `todo!()`. Not wired into any build.
|
||||
> This is the second Rust module after oakplugin (M11) and the largest
|
||||
> one; the crate template (FFI discipline, testing layers) follows
|
||||
> `src/plugin/rust/README.md`.
|
||||
|
||||
## Scope
|
||||
|
||||
Replaces the C++ oaknode module (`src/node/src`, ~40k lines):
|
||||
the node graph engine, project/folder/sequence/track/block hierarchy,
|
||||
footage, color manager, keyframes, evaluation (traverser), project
|
||||
serialization, and the undo bridge.
|
||||
|
||||
Public contract: `include/node/*.h` (14 headers, ~280 functions) —
|
||||
frozen, implemented verbatim by `src/ffi.rs`.
|
||||
|
||||
## Key architectural decisions (C++ → Rust mapping)
|
||||
|
||||
1. **Inheritance → arena + trait objects.** The C++ design is deep
|
||||
inheritance (`Node` → `ViewerOutput`/`Track`/`Block`/… and ~50
|
||||
effect nodes). Rust: a slab-allocated `Graph` arena of
|
||||
`NodeEntry { core: NodeCore, behavior: Box<dyn NodeBehavior> }`,
|
||||
addressed by generational `NodeId`. No reference cycles exist by
|
||||
construction (edges are IDs, not pointers).
|
||||
2. **Cross-module inheritance disappears.** C++ `RenderProcessor :
|
||||
NodeTraverser` (render subclassing a node class) becomes a plain
|
||||
evaluation API: `traverser::evaluate(...) -> NodeValueTable` is a
|
||||
function, and oakrender supplies backend hooks via a trait
|
||||
(`RenderHooks`) instead of overriding virtuals.
|
||||
3. **Value system.** `olive::Variant`/type-erasure becomes a closed
|
||||
`NodeValue` enum (`value.rs`). C ABI marshalling lives only in
|
||||
`ffi.rs`.
|
||||
4. **Undo.** Commands are created through the oakundo C ABI
|
||||
(`bridge::undo`); the C++ `UndoCommand` subclass hierarchy becomes
|
||||
vtable commands whose userdata is a Rust closure.
|
||||
5. **Serialization.** XML read/write goes through the oakcommon C ABI
|
||||
(`bridge::common`) until oakcommon itself is rewritten.
|
||||
6. **Threading.** The C++ code relied on Qt's event thread +
|
||||
`called_on_owner_thread()` assertions. Rust replaces this with
|
||||
`Mutex<Graph>` interior mutability plus explicit
|
||||
`&mut Graph` phases for structural edits; the threading contract is
|
||||
documented per function.
|
||||
|
||||
## Layout
|
||||
|
||||
`COVERAGE.md` maps every method of the C++ `olive::Node` (260
|
||||
declaration lines, ~150 unique methods) to its Rust home — trait /
|
||||
core / graph / ops / bridge / drop-with-reason. Review that first.
|
||||
|
||||
```
|
||||
src/
|
||||
lib.rs crate doc + module map
|
||||
error.rs error codes (mirrors include/node/error.h)
|
||||
handle.rs refcounted-handle scaffolding (same pattern as plugin)
|
||||
value.rs NodeValue / NodeValueTable / ValueHint
|
||||
id.rs NodeId, generational arena ids
|
||||
node.rs NodeCore + NodeBehavior trait (the virtual surface)
|
||||
graph.rs Graph arena, edges, topological order
|
||||
input.rs Input descriptors, flags, array inputs, hints
|
||||
keyframe.rs NodeKeyframe + track interpolation
|
||||
project.rs Project, settings, folder tree
|
||||
sequence.rs Sequence (ViewerOutput equivalent)
|
||||
track.rs Track, TrackList
|
||||
block.rs Block/ClipBlock/GapBlock/TransitionBlock
|
||||
footage.rs Footage (probe via oakcodec C ABI)
|
||||
colormanager.rs ColorManager (OCIO via oakrender C ABI for now)
|
||||
traverser.rs Evaluation engine (iterative, hook-based)
|
||||
serializer.rs XML project load/save (bridge::common)
|
||||
factory.rs Node type registry (id -> constructor)
|
||||
nodes/ The concrete built-in node types
|
||||
bridge/ C ABI imports: common.rs, undo.rs, render.rs, codec.rs
|
||||
ffi.rs include/node/*.h export layer
|
||||
tests/ contract + golden tests (see README test section)
|
||||
```
|
||||
|
||||
## Hard rules for the implementer
|
||||
|
||||
1. Every `extern "C"` body goes through `handle::guard*`; no panic
|
||||
crosses FFI.
|
||||
2. `Graph` is the only owner of nodes; the public API never hands out
|
||||
references into the arena, only `NodeId`-carrying handles.
|
||||
3. Behavior parity with C++ is proven by the C ABI test-suite
|
||||
(`src/node/tests`, unchanged) plus the golden tests in `tests/`.
|
||||
4. Where C++ behavior is genuinely load-bearing but ugly (e.g.
|
||||
`Block` length-change side effects on `Track`), port the behavior,
|
||||
not the aesthetics; leave a `// CPP-PARITY:` comment with the C++
|
||||
file:line.
|
||||
|
||||
## Dependency policy
|
||||
|
||||
Prefer mature third-party crates (MIT/Apache-2.0/BSD, GPL-compatible)
|
||||
over hand-rolling; register each addition (name + reason) here. Large
|
||||
existing C++ libraries (OTIO, OCIO, OIIO, FFmpeg) are NEVER rewritten
|
||||
— they are consumed through their C ABI / bridge layers.
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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/>.
|
||||
|
||||
//! Blocks (C++ `Block`, `ClipBlock`, `GapBlock`, `TransitionBlock`).
|
||||
|
||||
use oakcore_rs::{Rational, TimeRange};
|
||||
|
||||
use crate::id::NodeId;
|
||||
|
||||
/// Block core data (C++ `Block` members): timeline span + media range.
|
||||
pub struct BlockCore {
|
||||
/// Position and length on the timeline.
|
||||
pub range: TimeRange,
|
||||
/// Media in-point.
|
||||
pub media_in: Rational,
|
||||
/// Speed (1.0 = normal).
|
||||
pub speed: f64,
|
||||
/// Reversed flag.
|
||||
pub reversed: bool,
|
||||
/// Linked blocks (C++ block_links_).
|
||||
pub links: Vec<NodeId>,
|
||||
}
|
||||
|
||||
/// Clip block behavior (media-bearing block; C++ `ClipBlock`).
|
||||
pub struct ClipBlockBehavior {
|
||||
/// Block core.
|
||||
pub core: BlockCore,
|
||||
/// Connected footage (via the footage input edge).
|
||||
pub footage: Option<NodeId>,
|
||||
}
|
||||
|
||||
/// Gap block behavior (empty span; C++ `GapBlock`).
|
||||
pub struct GapBlockBehavior {
|
||||
/// Block core.
|
||||
pub core: BlockCore,
|
||||
}
|
||||
|
||||
/// Transition block behavior (C++ `TransitionBlock`).
|
||||
pub struct TransitionBlockBehavior {
|
||||
/// Block core.
|
||||
pub core: BlockCore,
|
||||
/// In offset (C++ in_offset).
|
||||
pub in_offset: Rational,
|
||||
/// Out offset.
|
||||
pub out_offset: Rational,
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakcodec C ABI imports (footage probing).
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
extern "C" {
|
||||
/// `oakcodec_decoder_probe` — fills stream info for a media file.
|
||||
pub fn oakcodec_decoder_probe(path: *const c_char, out: *mut CHandle) -> c_int;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakcommon C ABI imports (XML + config + strings).
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
extern "C" {
|
||||
/// `oakcommon_xml_reader_init`.
|
||||
pub fn oakcommon_xml_reader_init(data: *const c_char) -> CHandle;
|
||||
/// `oakcommon_xml_reader_free`.
|
||||
pub fn oakcommon_xml_reader_free(reader: *mut CHandle);
|
||||
/// `oakcommon_xml_writer_init`.
|
||||
pub fn oakcommon_xml_writer_init() -> CHandle;
|
||||
/// `oakcommon_xml_writer_free`.
|
||||
pub fn oakcommon_xml_writer_free(writer: *mut CHandle);
|
||||
/// `oakcommon_config_get_int` (config access for node defaults).
|
||||
pub fn oakcommon_config_get_int(group: *const c_char, key: *const c_char, default: c_int) -> c_int;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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/>.
|
||||
|
||||
//! C ABI imports from other oak modules (signatures mirror the public
|
||||
//! headers verbatim; resolved at link time).
|
||||
|
||||
pub mod codec;
|
||||
pub mod common;
|
||||
pub mod render;
|
||||
pub mod timeline;
|
||||
pub mod undo;
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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/>.
|
||||
|
||||
//! oakrender C ABI imports (caches, textures, color processors).
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// oakrender cache handle (value type).
|
||||
pub type CacheHandle = CHandle;
|
||||
/// oakrender texture handle (value type).
|
||||
pub type TextureHandle = CHandle;
|
||||
/// oakrender color processor handle (value type).
|
||||
pub type ColorProcessorHandle = CHandle;
|
||||
|
||||
extern "C" {
|
||||
/// `oakrender_cache_create_for_node`.
|
||||
pub fn oakrender_cache_create_for_node(parent: CHandle, kind: i32) -> CHandle;
|
||||
/// `oakrender_cache_free`.
|
||||
pub fn oakrender_cache_free(cache: *mut CHandle);
|
||||
/// `oakrender_cache_invalidate_range`.
|
||||
pub fn oakrender_cache_invalidate_range(
|
||||
cache: CHandle,
|
||||
in_num: i64,
|
||||
in_den: i64,
|
||||
out_num: i64,
|
||||
out_den: i64,
|
||||
);
|
||||
/// `oakrender_cache_set_uuid`.
|
||||
pub fn oakrender_cache_set_uuid(cache: CHandle, uuid: *const std::ffi::c_char) -> i32;
|
||||
/// `oakrender_cache_get_uuid` (two-stage).
|
||||
pub fn oakrender_cache_get_uuid(cache: CHandle, buf: *mut std::ffi::c_char, buf_size: i32) -> i32;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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/>.
|
||||
|
||||
//! oaktimeline C ABI imports (sequence markers/work area, edit
|
||||
//! commands used by sequence setup).
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
extern "C" {
|
||||
/// `oaktimeline_marker_list_create`.
|
||||
pub fn oaktimeline_marker_list_create() -> CHandle;
|
||||
/// `oaktimeline_marker_list_free`.
|
||||
pub fn oaktimeline_marker_list_free(list: *mut CHandle);
|
||||
/// `oaktimeline_workarea_create`.
|
||||
pub fn oaktimeline_workarea_create() -> CHandle;
|
||||
/// `oaktimeline_workarea_free`.
|
||||
pub fn oaktimeline_workarea_free(w: *mut CHandle);
|
||||
/// `oaktimeline_add_track_command`.
|
||||
pub fn oaktimeline_add_track_command(list: CHandle) -> CHandle;
|
||||
}
|
||||
@@ -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/>.
|
||||
|
||||
//! oakundo C ABI imports. Undo commands are created through the C ABI
|
||||
//! vtable (`oakundo_command_init` with Rust closures as userdata) —
|
||||
//! no C++ UndoCommand subclassing exists on this side.
|
||||
|
||||
use std::ffi::c_int;
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
extern "C" {
|
||||
/// `oakundo_command_init` (vtable command).
|
||||
pub fn oakundo_command_init(vtable: *const std::ffi::c_void, userdata: *mut std::ffi::c_void) -> CHandle;
|
||||
/// `oakundo_command_init_multi`.
|
||||
pub fn oakundo_command_init_multi() -> CHandle;
|
||||
/// `oakundo_command_multi_add_child`.
|
||||
pub fn oakundo_command_multi_add_child(multi: CHandle, child: CHandle) -> c_int;
|
||||
/// `oakundo_command_redo_now`.
|
||||
pub fn oakundo_command_redo_now(command: CHandle) -> c_int;
|
||||
/// `oakundo_command_undo_now`.
|
||||
pub fn oakundo_command_undo_now(command: CHandle) -> c_int;
|
||||
/// `oakundo_command_free`.
|
||||
pub fn oakundo_command_free(command: *mut CHandle);
|
||||
/// `oakundo_stack_push` (facade-owned stack).
|
||||
pub fn oakundo_stack_push(stack: CHandle, command: CHandle, text: *const std::ffi::c_char) -> c_int;
|
||||
}
|
||||
@@ -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/>.
|
||||
|
||||
//! Color manager (C++ `olive::ColorManager`): the per-project OCIO
|
||||
//! config handle. OCIO itself stays behind the oakrender C ABI until
|
||||
//! oakrender is rewritten; this module is the state owner and query
|
||||
//! facade.
|
||||
|
||||
/// Per-project color manager.
|
||||
pub struct ColorManager {
|
||||
/// OCIO config filename (empty = bundled default).
|
||||
pub config_filename: String,
|
||||
/// Default input colorspace.
|
||||
pub default_input_space: String,
|
||||
/// Default display.
|
||||
pub default_display: String,
|
||||
/// Default view.
|
||||
pub default_view: String,
|
||||
/// Reference colorspace.
|
||||
pub reference_space: String,
|
||||
}
|
||||
|
||||
impl ColorManager {
|
||||
/// New with the built-in default config selected (C++ `init()`).
|
||||
pub fn new() -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// (Re)load the config from `config_filename` via the oakrender
|
||||
/// color C ABI (`oakrender_color_manager_*`); E_FAILED on OCIO
|
||||
/// errors.
|
||||
pub fn reload(&mut self) -> crate::error::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Enumerate colorspaces of the active config (two-stage lists are
|
||||
/// flattened here into owned Strings).
|
||||
pub fn list_colorspaces(&self) -> Vec<String> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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/>.
|
||||
|
||||
//! Error codes, mirroring `include/node/error.h` verbatim; project-wide
|
||||
//! -MMCCCC scheme (module registry in include/common/error.h), pass-through untranslated.
|
||||
|
||||
/// Success.
|
||||
pub const OAKNODE_OK: i32 = 0;
|
||||
/// Null handle or invalid argument.
|
||||
pub const OAKNODE_E_INVALID: i32 = -30001;
|
||||
/// Call not valid in the current state.
|
||||
pub const OAKNODE_E_STATE: i32 = -30002;
|
||||
/// The underlying operation failed.
|
||||
pub const OAKNODE_E_FAILED: i32 = -30003;
|
||||
/// Index out of range / entry not found.
|
||||
pub const OAKNODE_E_NOT_FOUND: i32 = -30004;
|
||||
/// Allocation failed.
|
||||
pub const OAKNODE_E_NOMEM: i32 = -30005;
|
||||
|
||||
/// Crate-internal result type; the FFI layer maps it to the codes.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Crate-internal error.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Null handle or invalid argument.
|
||||
Invalid,
|
||||
/// Wrong state.
|
||||
State,
|
||||
/// Operation failed (context string is log-only).
|
||||
Failed(String),
|
||||
/// Not found.
|
||||
NotFound,
|
||||
/// Out of memory.
|
||||
NoMem,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Map to the public error code.
|
||||
pub fn code(&self) -> i32 {
|
||||
match self {
|
||||
Error::Invalid => OAKNODE_E_INVALID,
|
||||
Error::State => OAKNODE_E_STATE,
|
||||
Error::Failed(_) => OAKNODE_E_FAILED,
|
||||
Error::NotFound => OAKNODE_E_NOT_FOUND,
|
||||
Error::NoMem => OAKNODE_E_NOMEM,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// 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/>.
|
||||
|
||||
//! The node type registry (C++ `NodeFactory` / `node/factory`):
|
||||
//! type id -> constructor, plus menu metadata.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use crate::node::{Category, NodeBehavior, NodeCore};
|
||||
|
||||
/// Constructor for a node type: behavior + default core inputs.
|
||||
pub type NodeConstructor = fn() -> (NodeCore, Box<dyn NodeBehavior>);
|
||||
|
||||
/// Static metadata for the node menu (C++ factory listing).
|
||||
#[derive(Clone)]
|
||||
pub struct NodeMeta {
|
||||
/// Type id (matches [`NodeBehavior::type_id`]).
|
||||
pub type_id: &'static str,
|
||||
/// Display name.
|
||||
pub name: &'static str,
|
||||
/// Categories.
|
||||
pub categories: &'static [Category],
|
||||
/// Constructor.
|
||||
pub create: NodeConstructor,
|
||||
}
|
||||
|
||||
/// The registry (built at crate init by `nodes::register_all`).
|
||||
pub struct Factory {
|
||||
entries: Vec<NodeMeta>,
|
||||
}
|
||||
|
||||
impl Factory {
|
||||
/// Global registry. Registration (`nodes::register_all`) runs on
|
||||
/// first access, so the menu order is deterministic without an
|
||||
/// explicit init call.
|
||||
pub fn global() -> &'static Factory {
|
||||
GLOBAL.get_or_init(|| {
|
||||
crate::nodes::register_all();
|
||||
let entries = ENTRIES
|
||||
.get()
|
||||
.expect("nodes::register_all installs the entry table")
|
||||
.clone();
|
||||
Factory { entries }
|
||||
})
|
||||
}
|
||||
|
||||
/// Look up by type id.
|
||||
pub fn find(&self, type_id: &str) -> Option<&NodeMeta> {
|
||||
self.entries.iter().find(|e| e.type_id == type_id)
|
||||
}
|
||||
|
||||
/// All entries (menu order = registration order, C++ parity).
|
||||
pub fn entries(&self) -> &[NodeMeta] {
|
||||
&self.entries
|
||||
}
|
||||
}
|
||||
|
||||
/// Entries installed by [`crate::nodes::register_all`].
|
||||
static ENTRIES: OnceLock<Vec<NodeMeta>> = OnceLock::new();
|
||||
|
||||
/// The lazily built global [`Factory`].
|
||||
static GLOBAL: OnceLock<Factory> = OnceLock::new();
|
||||
|
||||
/// Install the registered node entries (called by
|
||||
/// [`crate::nodes::register_all`]; a no-op if already installed).
|
||||
pub(crate) fn install_entries(entries: Vec<NodeMeta>) {
|
||||
let _ = ENTRIES.set(entries);
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
// 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/>.
|
||||
|
||||
//! C ABI export layer: implements `include/node/*.h` verbatim.
|
||||
//!
|
||||
//! Organization: one submodule per public header. The authoritative
|
||||
//! function list is the header itself; each submodule below carries a
|
||||
//! complete inventory comment plus the export stubs. Bodies only
|
||||
//! unwrap handles, call safe Rust, and map results through
|
||||
//! [`crate::handle::guard*`].
|
||||
|
||||
use std::ffi::{c_char, c_int};
|
||||
|
||||
use crate::handle::CHandle;
|
||||
|
||||
/// `include/node/project.h` exports (complete inventory):
|
||||
/// oaknode_project_init / free / initialize / clear / root / name /
|
||||
/// filename / pretty_filename / set_filename / is_modified /
|
||||
/// set_modified / is_new / cache_path / copy_settings / load / save /
|
||||
/// load_from_data / save_to_data / folder_add / footage_import /
|
||||
/// get_project_from_object / debug_alive_count.
|
||||
pub mod project {
|
||||
use super::*;
|
||||
|
||||
/// `oaknode_project_init`: new project, refcount 1.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_project_init() -> CHandle {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_project_free`: NULL/empty no-op.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_project_free(project: *mut CHandle) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_project_initialize`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_project_initialize(project: CHandle) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_project_clear`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_project_clear(project: CHandle) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_project_load` (path on disk).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_project_load(path: *const c_char) -> CHandle {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_project_save`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_project_save(project: CHandle) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// … every other declaration in include/node/project.h follows the
|
||||
// same shape; see the header for the authoritative list.
|
||||
}
|
||||
|
||||
/// `include/node/node.h` exports (complete inventory):
|
||||
/// node_type_name/id/category/description, add/remove_input,
|
||||
/// set_input_name/flag/property, set_standard_value,
|
||||
/// get_standard_value, set_input_at_time(_undoable/_into),
|
||||
/// connect_edge / disconnect_edge, input_get_connected_node,
|
||||
/// input_array_size/append/remove, copy_inputs, node_get_project,
|
||||
/// node_identity, node_from_identity, sequence_from_node,
|
||||
/// sequence_set_default_parameters, find_input_footage,
|
||||
/// node_get_markers / get_work_area / get_video_frame_cache,
|
||||
/// command_create_* family, debug_alive_count.
|
||||
pub mod node {
|
||||
use super::*;
|
||||
|
||||
/// `oaknode_node_identity`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_node_identity(node: CHandle) -> usize {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_node_from_identity` (registry lookup).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_node_from_identity(id: usize) -> CHandle {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_node_connect_edge`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_node_connect_edge(
|
||||
from: CHandle,
|
||||
to: CHandle,
|
||||
input: *const c_char,
|
||||
element: c_int,
|
||||
) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// … remainder per include/node/node.h.
|
||||
}
|
||||
|
||||
/// `include/node/sequence.h` exports (complete inventory):
|
||||
/// sequence_create / as_node / from_node / set_default_parameters /
|
||||
/// add_default_nodes / get_length / track_list / playhead …
|
||||
pub mod sequence {
|
||||
use super::*;
|
||||
|
||||
/// `oaknode_sequence_create`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_sequence_create() -> CHandle {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_sequence_as_node`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_sequence_as_node(sequence: CHandle) -> CHandle {
|
||||
todo!()
|
||||
}
|
||||
|
||||
// … remainder per include/node/sequence.h.
|
||||
}
|
||||
|
||||
/// `include/node/keyframe.h` — keyframe helpers (the handle-based
|
||||
/// keyframe API itself is declared once the keyframe module lands; these
|
||||
/// four are the paste/bezier/sibling helpers of the 2026-08-09 gap list).
|
||||
pub mod keyframe {
|
||||
use super::*;
|
||||
|
||||
/// `oaknode_keyframe_get_valid_bezier_control`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_keyframe_get_valid_bezier_control(
|
||||
keyframe: CHandle,
|
||||
handle: c_int,
|
||||
out_x: *mut f64,
|
||||
out_y: *mut f64,
|
||||
) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_keyframe_opposing_bezier_type`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_keyframe_opposing_bezier_type(
|
||||
type_: c_int,
|
||||
) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_keyframe_compute_paste_value`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_keyframe_compute_paste_value(
|
||||
target_node: CHandle,
|
||||
keyframe: CHandle,
|
||||
out: *mut crate::value::OakNodeValue,
|
||||
) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_keyframe_has_sibling_at_time`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_keyframe_has_sibling_at_time(
|
||||
keyframe: CHandle,
|
||||
time_num: i64,
|
||||
time_den: i64,
|
||||
out_value: *mut c_int,
|
||||
) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// `include/node/dragger.h` exports (complete inventory):
|
||||
/// oaknode_dragger_create / start / drag / end / is_started / free.
|
||||
pub mod dragger {
|
||||
use super::*;
|
||||
|
||||
/// `oaknode_dragger_create`: new dragger, refcount 1.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_dragger_create(
|
||||
node: CHandle,
|
||||
input_id: *const c_char,
|
||||
element: c_int,
|
||||
track: c_int,
|
||||
) -> CHandle {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_dragger_start`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_dragger_start(
|
||||
dragger: CHandle,
|
||||
time_num: i64,
|
||||
time_den: i64,
|
||||
track: c_int,
|
||||
insert_on_all_tracks: c_int,
|
||||
) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_dragger_drag`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_dragger_drag(
|
||||
dragger: CHandle,
|
||||
value: *const crate::value::OakNodeValue,
|
||||
) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_dragger_end` (out_command mirrors the OakUndoCommand
|
||||
/// handle layout, identical to CHandle).
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_dragger_end(
|
||||
dragger: CHandle,
|
||||
out_command: *mut CHandle,
|
||||
) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_dragger_is_started`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_dragger_is_started(
|
||||
dragger: CHandle,
|
||||
out_started: *mut c_int,
|
||||
) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_dragger_free`: NULL/empty no-op.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_dragger_free(dragger: *mut CHandle) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// `include/node/multicam.h` exports (complete inventory): the four
|
||||
/// input-id getters plus source-count / grid-math / current-source.
|
||||
pub mod multicam {
|
||||
use super::*;
|
||||
|
||||
/// `oaknode_multicam_input_current`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_multicam_input_current() -> *const c_char {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_multicam_input_sources`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_multicam_input_sources() -> *const c_char {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_multicam_input_sequence`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_multicam_input_sequence() -> *const c_char {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_multicam_input_sequence_type`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_multicam_input_sequence_type(
|
||||
) -> *const c_char {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_multicam_get_source_count`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_multicam_get_source_count(
|
||||
node: CHandle,
|
||||
out_count: *mut c_int,
|
||||
) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_multicam_get_rows_and_columns`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_multicam_get_rows_and_columns(
|
||||
source_count: c_int,
|
||||
rows: *mut c_int,
|
||||
cols: *mut c_int,
|
||||
) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_multicam_index_to_row_cols`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_multicam_index_to_row_cols(
|
||||
index: c_int,
|
||||
rows: c_int,
|
||||
cols: c_int,
|
||||
out_row: *mut c_int,
|
||||
out_col: *mut c_int,
|
||||
) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_multicam_rows_cols_to_index`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_multicam_rows_cols_to_index(
|
||||
row: c_int,
|
||||
col: c_int,
|
||||
rows: c_int,
|
||||
cols: c_int,
|
||||
) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// `oaknode_multicam_get_current_source`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn oaknode_multicam_get_current_source(
|
||||
node: CHandle,
|
||||
out_source: *mut c_int,
|
||||
) -> c_int {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// `include/node/block.h`, `folder.h`, `footage.h`, `group.h`,
|
||||
/// `colormanager.h`, `factory.h`, `serializer.h`, `traverser.h`: same
|
||||
/// one-submodule-per-header pattern (create them as `block`/`folder`/
|
||||
/// `footage`/`group`/`colormanager`/`factory`/`serializer`/`traverser`).
|
||||
pub mod remaining_headers {
|
||||
// Stubs are added per header when the corresponding internal module
|
||||
// is implemented; keep header order and naming identical to the C
|
||||
// side so bindgen/diff audits stay mechanical.
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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/>.
|
||||
|
||||
//! Bin folders (C++ `olive::Folder`): tree structure over `NodeId`s.
|
||||
|
||||
use crate::id::NodeId;
|
||||
|
||||
/// A folder node behavior: children are folder-tree members (folders
|
||||
/// and footage), not graph edges.
|
||||
pub struct FolderBehavior {
|
||||
/// Display name.
|
||||
pub name: String,
|
||||
/// Child node ids (folders/footage) in bin order.
|
||||
pub children: Vec<NodeId>,
|
||||
}
|
||||
|
||||
impl FolderBehavior {
|
||||
/// New empty folder.
|
||||
pub fn new(name: &str) -> Self {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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/>.
|
||||
|
||||
//! Footage nodes (C++ `olive::Footage`): media file references.
|
||||
//! Probing goes through the oakcodec C ABI (`bridge::codec`) — the C++
|
||||
//! transition-stub probe path does not exist here.
|
||||
|
||||
use crate::value::{AudioParams, VideoParams};
|
||||
|
||||
/// One media stream inside a footage file.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StreamInfo {
|
||||
/// Stream index in the container.
|
||||
pub index: i32,
|
||||
/// True for video streams.
|
||||
pub is_video: bool,
|
||||
/// Video parameters (when `is_video`).
|
||||
pub video: Option<VideoParams>,
|
||||
/// Audio parameters (when not video).
|
||||
pub audio: Option<AudioParams>,
|
||||
/// Duration in stream timebase.
|
||||
pub duration: oakcore_rs::Rational,
|
||||
}
|
||||
|
||||
/// Footage behavior.
|
||||
pub struct FootageBehavior {
|
||||
/// Absolute file path.
|
||||
pub filename: String,
|
||||
/// Probed streams (empty until [`FootageBehavior::probe`]).
|
||||
pub streams: Vec<StreamInfo>,
|
||||
/// Proxy path (C++ set_proxy; empty = none).
|
||||
pub proxy: String,
|
||||
}
|
||||
|
||||
impl FootageBehavior {
|
||||
/// Create for `filename` (unprobed).
|
||||
pub fn new(filename: &str) -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Probe the file through oakcodec (`oakcodec_decoder_probe`),
|
||||
/// filling `streams`. Error on unreadable/corrupt media.
|
||||
pub fn probe(&mut self) -> crate::error::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
// 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/>.
|
||||
|
||||
//! Gizmo data types (C++ `src/node/src/gizmo/{gizmo,draggable,point,line,
|
||||
//! path,polygon,screen,text}.h`; the C++ classes are `olive::NodeGizmo`,
|
||||
//! `DraggableGizmo`, `PointGizmo`, `LineGizmo`, `PathGizmo`,
|
||||
//! `PolygonGizmo`, `ScreenGizmo`, `TextGizmo`, plus the `PointF`/`LineF`/
|
||||
//! `RectF` carriers from `mathtypes.h`/`line.h`/`text.h`).
|
||||
//!
|
||||
//! Data only — drawing, hit testing, and mouse handling live in the
|
||||
//! facade/app layer, so the C++ drag callbacks (`drag_start`/`drag_move`/
|
||||
//! `drag_end`) and `TextGizmo::update_input_html` have no Rust counterpart
|
||||
//! here.
|
||||
//!
|
||||
//! Note: the rough `crate::node::Gizmo` sketch in `node.rs` should
|
||||
//! eventually be replaced by the types in this module.
|
||||
//!
|
||||
//! C++ uses single inheritance (`NodeGizmo` <- `DraggableGizmo` <-
|
||||
//! `PointGizmo`/`PathGizmo`/`PolygonGizmo`/`ScreenGizmo`); Rust models
|
||||
//! this by flattening the base-class fields into each concrete struct
|
||||
//! (the base fields are documented as such on each).
|
||||
|
||||
use oakcore_rs::{Rational, TimeRange};
|
||||
|
||||
use crate::value::{AudioParams, NodeValue, VideoParams};
|
||||
|
||||
/// 2D point (C++ `olive::PointF` in `src/node/src/mathtypes.h`, a de-Qt
|
||||
/// replacement for `QPointF`).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct PointF {
|
||||
/// X coordinate.
|
||||
pub x: f64,
|
||||
/// Y coordinate.
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
impl PointF {
|
||||
/// Construct a point (C++ `PointF(x, y)`).
|
||||
pub fn new(x: f64, y: f64) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
/// Whether both coordinates are zero (C++ `is_null()`).
|
||||
pub fn is_null(&self) -> bool {
|
||||
self.x == 0.0 && self.y == 0.0
|
||||
}
|
||||
|
||||
/// `|x| + |y|` (C++ `manhattan_length()`).
|
||||
pub fn manhattan_length(&self) -> f64 {
|
||||
self.x.abs() + self.y.abs()
|
||||
}
|
||||
}
|
||||
|
||||
/// Line segment (C++ `olive::LineF` in `src/node/src/gizmo/line.h`, a
|
||||
/// de-Qt replacement for `QLineF`; data carrier only).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct LineF {
|
||||
/// Start point (C++ `p1_`).
|
||||
pub p1: PointF,
|
||||
/// End point (C++ `p2_`).
|
||||
pub p2: PointF,
|
||||
}
|
||||
|
||||
/// Rectangle (C++ `olive::RectF` in `src/node/src/gizmo/text.h`, a de-Qt
|
||||
/// replacement for `QRectF`; text gizmo rect).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct RectF {
|
||||
/// Left edge (C++ `x_`).
|
||||
pub x: f64,
|
||||
/// Top edge (C++ `y_`).
|
||||
pub y: f64,
|
||||
/// Width (C++ `width_`).
|
||||
pub width: f64,
|
||||
/// Height (C++ `height_`).
|
||||
pub height: f64,
|
||||
}
|
||||
|
||||
/// Loop mode (C++ `enum class LoopMode` in
|
||||
/// `src/common/src/loopmode.h`).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum LoopMode {
|
||||
/// Play once (C++ `k_loop_mode_off`).
|
||||
#[default]
|
||||
Off,
|
||||
/// Repeat the clip (C++ `k_loop_mode_loop`).
|
||||
Loop,
|
||||
/// Hold first/last frame (C++ `k_loop_mode_clamp`).
|
||||
Clamp,
|
||||
}
|
||||
|
||||
/// Sequence/render globals a gizmo draws against (C++ `NodeGlobals` in
|
||||
/// `src/node/src/globals.h`).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct NodeGlobals {
|
||||
/// Video parameters (C++ `video_params_`).
|
||||
pub video_params: VideoParams,
|
||||
/// Audio parameters (C++ `audio_params_`).
|
||||
pub audio_params: AudioParams,
|
||||
/// Current time range (C++ `time_`).
|
||||
pub time: TimeRange,
|
||||
/// Loop mode (C++ `loop_mode_`).
|
||||
pub loop_mode: LoopMode,
|
||||
}
|
||||
|
||||
/// Reference to one keyframe track of a node input (C++
|
||||
/// `NodeKeyframeTrackReference` in `src/node/src/param.h`). Invalid when
|
||||
/// `track` < 0 (C++ default constructs `track_ = -1`).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct KeyframeTrackReference {
|
||||
/// Input id (C++ `NodeInput`'s input string).
|
||||
pub input_id: String,
|
||||
/// Array element (C++ `NodeInput`'s element; -1 = none).
|
||||
pub element: i32,
|
||||
/// Track index (C++ `track_`; -1 = invalid).
|
||||
pub track: i32,
|
||||
}
|
||||
|
||||
impl Default for KeyframeTrackReference {
|
||||
/// C++ default constructor: empty input, `track = -1`.
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
input_id: String::new(),
|
||||
element: -1,
|
||||
track: -1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyframeTrackReference {
|
||||
/// Whether the reference is usable (C++ `is_valid()`).
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.track >= 0
|
||||
}
|
||||
}
|
||||
|
||||
/// Drag state for one input (C++ `NodeInputDragger` in
|
||||
/// `src/node/src/inputdragger.h`). Data only: the C++ `start`/`drag`/
|
||||
/// `end` methods write keyframes and push undo commands — that behavior
|
||||
/// belongs to the facade/app layer; the C++ static
|
||||
/// `input_being_dragged` flag is global UI state and is not modeled
|
||||
/// here.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct InputDragger {
|
||||
/// Target input/track (C++ `input_`).
|
||||
pub input: KeyframeTrackReference,
|
||||
/// Time at drag start (C++ `time_`).
|
||||
pub time: Rational,
|
||||
/// Value at drag start (C++ `start_value_`).
|
||||
pub start_value: NodeValue,
|
||||
/// Current/last dragged value (C++ `end_value_`).
|
||||
pub end_value: NodeValue,
|
||||
/// Whether a drag is in progress (C++ `is_started()`).
|
||||
pub started: bool,
|
||||
}
|
||||
|
||||
impl Default for InputDragger {
|
||||
/// C++ default constructor: no drag in progress, values unset.
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
input: KeyframeTrackReference::default(),
|
||||
time: Rational::default(),
|
||||
start_value: NodeValue::None,
|
||||
end_value: NodeValue::None,
|
||||
started: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What the X/Y coordinates emitted during a drag mean (C++
|
||||
/// `DraggableGizmo::DragValueBehavior` in
|
||||
/// `src/node/src/gizmo/draggable.h`).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum DragValueBehavior {
|
||||
/// Exact mouse coordinates in sequence pixels (C++ `k_absolute`;
|
||||
/// the C++ default).
|
||||
#[default]
|
||||
Absolute,
|
||||
/// Movement since the last move event (C++
|
||||
/// `k_delta_from_previous`).
|
||||
DeltaFromPrevious,
|
||||
/// Movement from the start of the drag (C++ `k_delta_from_start`).
|
||||
DeltaFromStart,
|
||||
}
|
||||
|
||||
/// Base gizmo data (C++ `olive::NodeGizmo` private members in
|
||||
/// `src/node/src/gizmo/gizmo.h`).
|
||||
///
|
||||
/// The C++ `parent_` node pointer is omitted: ownership and
|
||||
/// registration run through `NodeCore::gizmos` (mirroring
|
||||
/// `Node::add_gizmo()`/`remove_gizmo()`), so a back-pointer has no Rust
|
||||
/// equivalent here.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct GizmoBase {
|
||||
/// Sequence globals the gizmo draws against (C++ `globals_`).
|
||||
pub globals: NodeGlobals,
|
||||
/// Visibility flag (C++ `visible_`).
|
||||
pub visible: bool,
|
||||
}
|
||||
|
||||
/// Draggable base data (C++ `olive::DraggableGizmo` in
|
||||
/// `src/node/src/gizmo/draggable.h`).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DraggableGizmo {
|
||||
/// Base gizmo data (C++ `NodeGizmo` base-class members).
|
||||
pub base: GizmoBase,
|
||||
/// Keyframed inputs this gizmo drags (C++ `inputs_`; appended by
|
||||
/// `add_input()`).
|
||||
pub inputs: Vec<KeyframeTrackReference>,
|
||||
/// Per-input drag state, parallel to `inputs` (C++ `draggers_`).
|
||||
pub draggers: Vec<InputDragger>,
|
||||
/// Drag coordinate semantics (C++ `drag_value_behavior_`).
|
||||
pub drag_value_behavior: DragValueBehavior,
|
||||
}
|
||||
|
||||
/// Point gizmo shape (C++ `PointGizmo::Shape` in
|
||||
/// `src/node/src/gizmo/point.h`).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum PointShape {
|
||||
/// Square handle (C++ `k_square`; the C++ default constructor's
|
||||
/// shape).
|
||||
#[default]
|
||||
Square,
|
||||
/// Circle handle (C++ `k_circle`).
|
||||
Circle,
|
||||
/// Anchor-point handle (C++ `k_anchor_point`).
|
||||
AnchorPoint,
|
||||
}
|
||||
|
||||
/// Point gizmo (C++ `olive::PointGizmo` in
|
||||
/// `src/node/src/gizmo/point.h`; base `DraggableGizmo`).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PointGizmo {
|
||||
/// Draggable base data (C++ `DraggableGizmo` base-class members).
|
||||
pub draggable: DraggableGizmo,
|
||||
/// Handle shape (C++ `shape_`).
|
||||
pub shape: PointShape,
|
||||
/// Handle position in sequence pixels (C++ `point_`).
|
||||
pub point: PointF,
|
||||
/// Render the handle smaller (C++ `smaller_`).
|
||||
pub smaller: bool,
|
||||
}
|
||||
|
||||
/// Line gizmo (C++ `olive::LineGizmo` in `src/node/src/gizmo/line.h`;
|
||||
/// base `NodeGizmo`, not draggable).
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LineGizmo {
|
||||
/// Base gizmo data (C++ `NodeGizmo` base-class members).
|
||||
pub base: GizmoBase,
|
||||
/// The line in sequence pixels (C++ `line_`).
|
||||
pub line: LineF,
|
||||
}
|
||||
|
||||
/// Path gizmo (C++ `olive::PathGizmo` in `src/node/src/gizmo/path.h`;
|
||||
/// base `DraggableGizmo`).
|
||||
///
|
||||
/// The C++ class has no own members — its former `QPainterPath` (a
|
||||
/// drawing/hit-test primitive) was removed; path storage and drawing
|
||||
/// belong to the app layer. The type is kept so the gizmo hierarchy
|
||||
/// remains distinguishable.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PathGizmo {
|
||||
/// Draggable base data (C++ `DraggableGizmo` base-class members).
|
||||
pub draggable: DraggableGizmo,
|
||||
}
|
||||
|
||||
/// Polygon gizmo (C++ `olive::PolygonGizmo` in
|
||||
/// `src/node/src/gizmo/polygon.h`; base `DraggableGizmo`).
|
||||
///
|
||||
/// Point-in-polygon testing (formerly `QPolygonF::containsPoint`)
|
||||
/// belongs to the app layer.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PolygonGizmo {
|
||||
/// Draggable base data (C++ `DraggableGizmo` base-class members).
|
||||
pub draggable: DraggableGizmo,
|
||||
/// Polygon vertices (C++ `polygon_`, de-Qt'd from `QPolygonF` to a
|
||||
/// plain vector of points).
|
||||
pub polygon: Vec<PointF>,
|
||||
}
|
||||
|
||||
/// Screen gizmo (C++ `olive::ScreenGizmo` in
|
||||
/// `src/node/src/gizmo/screen.h`; base `DraggableGizmo`).
|
||||
///
|
||||
/// The C++ class has no own members; it exists only to distinguish the
|
||||
/// whole-screen drag target in the gizmo hierarchy.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ScreenGizmo {
|
||||
/// Draggable base data (C++ `DraggableGizmo` base-class members).
|
||||
pub draggable: DraggableGizmo,
|
||||
}
|
||||
|
||||
/// Text gizmo vertical alignment (C++ `TextGizmo::VerticalAlignment` in
|
||||
/// `src/node/src/gizmo/text.h`, formerly `Qt::Alignment`; values match
|
||||
/// the oakengine facade).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum VerticalAlignment {
|
||||
/// Align to the top (C++ `k_align_top` = 0).
|
||||
#[default]
|
||||
Top,
|
||||
/// Align to the bottom (C++ `k_align_bottom` = 1).
|
||||
Bottom,
|
||||
/// Align vertically centered (C++ `k_align_vcenter` = 2).
|
||||
VCenter,
|
||||
}
|
||||
|
||||
/// Text gizmo (C++ `olive::TextGizmo` in `src/node/src/gizmo/text.h`;
|
||||
/// base `NodeGizmo`, not draggable).
|
||||
///
|
||||
/// `TextGizmo::update_input_html()` (pushing edited HTML back into the
|
||||
/// connected input at a time) is behavioral and belongs to the
|
||||
/// facade/app layer, so it is not declared here.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct TextGizmo {
|
||||
/// Base gizmo data (C++ `NodeGizmo` base-class members).
|
||||
pub base: GizmoBase,
|
||||
/// Text bounds in sequence pixels (C++ `rect_`).
|
||||
pub rect: RectF,
|
||||
/// HTML content (C++ `text_`).
|
||||
pub html: String,
|
||||
/// Input track this gizmo edits (C++ `input_`).
|
||||
pub input: KeyframeTrackReference,
|
||||
/// Vertical alignment (C++ `valign_`).
|
||||
pub vertical_alignment: VerticalAlignment,
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// 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/>.
|
||||
|
||||
//! The graph arena: node storage, edges, traversal order.
|
||||
//!
|
||||
//! Replaces the C++ pointer web (`Node::parent_`, output_connections_)
|
||||
//! with a slab arena + edge set. All structural mutation goes through
|
||||
//! `&mut Graph` methods; evaluation takes `&Graph`.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::id::NodeId;
|
||||
use crate::node::{NodeBehavior, NodeCore};
|
||||
|
||||
/// One arena slot.
|
||||
pub struct NodeEntry {
|
||||
/// Shared data.
|
||||
pub core: NodeCore,
|
||||
/// Polymorphic behavior.
|
||||
pub behavior: Box<dyn NodeBehavior>,
|
||||
/// Generation for stale-id detection.
|
||||
pub generation: u32,
|
||||
/// True when the slot is free.
|
||||
pub vacant: bool,
|
||||
}
|
||||
|
||||
/// A directed edge: `from` node's output feeds `to` node's `input`
|
||||
/// (element for array inputs).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Edge {
|
||||
/// Source node.
|
||||
pub from: NodeId,
|
||||
/// Destination node.
|
||||
pub to: NodeId,
|
||||
/// Destination input id hash + element (string stored arena-side).
|
||||
pub input_key: u64,
|
||||
/// Array element (-1 = scalar input).
|
||||
pub element: i32,
|
||||
}
|
||||
|
||||
/// The node graph. Nodes are owned here, exclusively.
|
||||
pub struct Graph {
|
||||
entries: Vec<NodeEntry>,
|
||||
free_list: Vec<u32>,
|
||||
edges: BTreeSet<Edge>,
|
||||
/// Arena-side storage for edge input ids (key = hash into this map).
|
||||
input_names: std::collections::HashMap<u64, String>,
|
||||
}
|
||||
|
||||
impl Graph {
|
||||
/// Empty graph.
|
||||
pub fn new() -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Insert a node; returns its id.
|
||||
pub fn add_node(&mut self, core: NodeCore, behavior: Box<dyn NodeBehavior>) -> NodeId {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Remove a node and all its edges (C++: ~Node + set_parent(null)
|
||||
/// + disconnect_all side effects — see `// CPP-PARITY: node.cpp`).
|
||||
pub fn remove_node(&mut self, id: NodeId) -> Option<Box<dyn NodeBehavior>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Validated access; `None` for stale ids.
|
||||
pub fn get(&self, id: NodeId) -> Option<&NodeEntry> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Mutable access; `None` for stale ids.
|
||||
pub fn get_mut(&mut self, id: NodeId) -> Option<&mut NodeEntry> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Connect `from`'s output to `to.input[element]`
|
||||
/// (C++ `Node::connect_edge` incl. cycle rejection).
|
||||
pub fn connect(&mut self, from: NodeId, to: NodeId, input: &str, element: i32) -> crate::error::Result<()> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Disconnect one edge (no-op when absent).
|
||||
pub fn disconnect(&mut self, from: NodeId, to: NodeId, input: &str, element: i32) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Nodes directly upstream of `id` (evaluation order helper).
|
||||
pub fn upstream(&self, id: NodeId) -> Vec<NodeId> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Nodes directly downstream of `id` (invalidation fan-out).
|
||||
pub fn downstream(&self, id: NodeId) -> Vec<NodeId> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Topological order from sources to sinks (Kahn; cycles are
|
||||
/// rejected at connect time so this cannot fail).
|
||||
pub fn topological_order(&self) -> Vec<NodeId> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// 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/>.
|
||||
|
||||
//! Refcounted-handle scaffolding. Same pattern as the oakplugin crate
|
||||
//! (`src/plugin/rust/src/handle.rs`); intentionally duplicated rather
|
||||
//! than shared — each module DLL must run its own addref/release code
|
||||
//! (the function pointers in a handle always point into the DLL that
|
||||
//! created the object).
|
||||
|
||||
use std::any::Any;
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use crate::error::OAKNODE_E_FAILED;
|
||||
|
||||
/// ABI version stamped into every handle.
|
||||
pub const OAKNODE_ABI_VERSION: u32 = 1;
|
||||
|
||||
/// Heap box behind a handle's `ctx`.
|
||||
pub struct RefBox<T: ?Sized> {
|
||||
/// Atomic reference count.
|
||||
pub refs: AtomicU32,
|
||||
/// Boxed value.
|
||||
pub value: T,
|
||||
}
|
||||
|
||||
/// `#[repr(C)]` mirror of the public handle structs
|
||||
/// (`{ctx, addref, release, abi_version}`).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CHandle {
|
||||
/// Opaque box pointer.
|
||||
pub ctx: *mut std::ffi::c_void,
|
||||
/// Atomic increment.
|
||||
pub addref: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
|
||||
/// Atomic decrement; destroys at zero.
|
||||
pub release: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
|
||||
/// ABI version.
|
||||
pub abi_version: u32,
|
||||
}
|
||||
|
||||
impl CHandle {
|
||||
/// The empty handle.
|
||||
pub fn null() -> Self {
|
||||
Self {
|
||||
ctx: std::ptr::null_mut(),
|
||||
addref: None,
|
||||
release: None,
|
||||
abi_version: OAKNODE_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// addref 的实现:原子 +1。拥有型与借用型共用——借用型只延长盒子
|
||||
/// 的寿命,不延长被借用对象。
|
||||
unsafe extern "C" fn refbox_addref<T: Any + Send>(ctx: *mut std::ffi::c_void) {
|
||||
unsafe {
|
||||
let rb = ctx as *const RefBox<T>;
|
||||
// 调用方保证句柄在借用期内有效(ctx 非空且未被释放)。
|
||||
(*rb).refs.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// release 的实现(拥有型):原子 -1,归零时回收盒子并销毁内含对象。
|
||||
unsafe extern "C" fn refbox_release_owned<T: Any + Send>(ctx: *mut std::ffi::c_void) {
|
||||
unsafe {
|
||||
let rb = ctx as *mut RefBox<T>;
|
||||
// AcqRel:归零这一侧要能看见最后一次引用前的全部写(含对象
|
||||
// 析构所需的内部状态)。
|
||||
if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
|
||||
drop(Box::from_raw(rb));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// release 的实现(借用型,[`make_borrowed`] 的产物):归零时只回收
|
||||
/// 盒子内存,把内含对象原样忘掉——其所有权仍在借用方手里。
|
||||
unsafe extern "C" fn refbox_release_borrowed<T: Any + Send>(ctx: *mut std::ffi::c_void) {
|
||||
unsafe {
|
||||
let rb = ctx as *mut RefBox<T>;
|
||||
if (*rb).refs.fetch_sub(1, Ordering::AcqRel) == 1 {
|
||||
// 部分 move:把 value 移出临时 Box,Box 析构只释放分配;
|
||||
// value 用 forget 放弃析构(double-free 防线)。
|
||||
std::mem::forget((Box::from_raw(rb)).value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Owned handle with count 1; empty on allocation failure.
|
||||
pub fn make_owned<T: Any + Send>(value: T) -> CHandle {
|
||||
let rb = Box::into_raw(Box::new(RefBox {
|
||||
refs: AtomicU32::new(1),
|
||||
value,
|
||||
}));
|
||||
CHandle {
|
||||
ctx: rb as *mut std::ffi::c_void,
|
||||
addref: Some(refbox_addref::<T>),
|
||||
release: Some(refbox_release_owned::<T>),
|
||||
abi_version: OAKNODE_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrowed handle for an object owned elsewhere (release frees only
|
||||
/// the box).
|
||||
///
|
||||
/// Semantics: bitwise copy ("borrowed copy"); the borrowed object's
|
||||
/// destructor is entirely the caller's responsibility — the box never
|
||||
/// touches it.
|
||||
///
|
||||
/// # Safety
|
||||
/// Caller guarantees `ptr` outlives every derived handle, and that its
|
||||
/// value is not moved or destroyed for the borrow's lifetime.
|
||||
pub unsafe fn make_borrowed<T: Any + Send>(ptr: *mut T) -> CHandle {
|
||||
if ptr.is_null() {
|
||||
return CHandle::null();
|
||||
}
|
||||
let rb = Box::into_raw(Box::new(RefBox {
|
||||
refs: AtomicU32::new(1),
|
||||
value: unsafe { std::ptr::read(ptr) },
|
||||
}));
|
||||
CHandle {
|
||||
ctx: rb as *mut std::ffi::c_void,
|
||||
addref: Some(refbox_addref::<T>),
|
||||
release: Some(refbox_release_borrowed::<T>),
|
||||
abi_version: OAKNODE_ABI_VERSION,
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed view into a handle; `None` for empty handles.
|
||||
///
|
||||
/// # Safety
|
||||
/// `T` must be the boxed type.
|
||||
pub unsafe fn get<T: Any>(h: &CHandle) -> Option<&T> {
|
||||
if h.ctx.is_null() {
|
||||
return None;
|
||||
}
|
||||
unsafe { Some(&(*(h.ctx as *const RefBox<T>)).value) }
|
||||
}
|
||||
|
||||
/// Panic-catching FFI wrapper for i32-returning exports.
|
||||
///
|
||||
/// Panics map to [`OAKNODE_E_FAILED`].
|
||||
pub fn guard<F: FnOnce() -> crate::error::Result<()>>(f: F) -> i32 {
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(Ok(())) => crate::error::OAKNODE_OK,
|
||||
Ok(Err(e)) => e.code(),
|
||||
Err(_) => OAKNODE_E_FAILED,
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic-catching FFI wrapper for handle-returning exports.
|
||||
pub fn guard_handle<F: FnOnce() -> crate::error::Result<CHandle>>(f: F) -> CHandle {
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(Ok(h)) => h,
|
||||
Ok(Err(_)) | Err(_) => CHandle::null(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Panic-catching FFI wrapper for void exports.
|
||||
pub fn guard_void<F: FnOnce()>(f: F) {
|
||||
let _ = catch_unwind(AssertUnwindSafe(f));
|
||||
}
|
||||
@@ -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/>.
|
||||
|
||||
//! Generational identifiers for arena-stored graph objects.
|
||||
//!
|
||||
//! The public C ABI hands out opaque refcounted handle boxes; inside
|
||||
//! the crate, a handle boxes `(Arc<Mutex<Project>>, NodeId)`. The
|
||||
//! generation counter makes a stale `NodeId` (whose slot was reused)
|
||||
//! fail loudly instead of aliasing a different node — this replaces
|
||||
//! the C++ design's dangling-pointer failure mode with a checked one.
|
||||
|
||||
/// Id of a node inside a [`crate::graph::Graph`] arena.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct NodeId {
|
||||
index: u32,
|
||||
generation: u32,
|
||||
}
|
||||
|
||||
impl NodeId {
|
||||
/// Construct from raw parts (arena-internal use).
|
||||
pub(crate) fn new(index: u32, generation: u32) -> NodeId {
|
||||
NodeId { index, generation }
|
||||
}
|
||||
|
||||
/// Slot index.
|
||||
pub fn index(self) -> u32 {
|
||||
self.index
|
||||
}
|
||||
|
||||
/// Generation counter.
|
||||
pub fn generation(self) -> u32 {
|
||||
self.generation
|
||||
}
|
||||
|
||||
/// Stable identity integer for registry keys and XML cross-
|
||||
/// references (replaces the C++ raw-pointer `uintptr_t` identity;
|
||||
/// NOT an address, safe to persist within a session).
|
||||
pub fn identity(self) -> u64 {
|
||||
((self.generation as u64) << 32) | self.index as u64
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Input descriptors: the C++ `Node::Input` record, flags, array
|
||||
//! inputs, and value hints.
|
||||
|
||||
use crate::value::{NodeValue, ValueType};
|
||||
|
||||
/// Input flag bits (values match the C++ `InputFlag` enum — they
|
||||
/// cross the C ABI and project XML as ints).
|
||||
pub mod flags {
|
||||
/// Not connectable to other nodes.
|
||||
pub const NOT_CONNECTABLE: u32 = 1 << 0;
|
||||
/// Not keyframable.
|
||||
pub const NOT_KEYFRAMABLE: u32 = 1 << 1;
|
||||
/// Array input (elements addressable).
|
||||
pub const ARRAY: u32 = 1 << 2;
|
||||
/// Hidden from the parameter UI.
|
||||
pub const HIDDEN: u32 = 1 << 3;
|
||||
/// Does not trigger invalidation on change.
|
||||
pub const IGNORE_INVALIDATIONS: u32 = 1 << 4;
|
||||
}
|
||||
|
||||
/// One input (scalar) or one array element slot's descriptor.
|
||||
pub struct Input {
|
||||
/// Input id (e.g. "tex_in").
|
||||
pub id: String,
|
||||
/// Accepted value type.
|
||||
pub value_type: ValueType,
|
||||
/// Default value (C++ default parameter).
|
||||
pub default: NodeValue,
|
||||
/// Flag bits (`flags::*`).
|
||||
pub flags: u32,
|
||||
/// Display name (C++ `set_input_name`).
|
||||
pub display_name: String,
|
||||
/// Arbitrary properties (C++ `set_input_property` map).
|
||||
pub properties: Vec<(String, NodeValue)>,
|
||||
/// Array size for ARRAY inputs (0 otherwise).
|
||||
pub array_size: usize,
|
||||
}
|
||||
|
||||
/// Value hint (C++ `Node::ValueHint`): accepted type set per input,
|
||||
/// used to convert values on connect.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ValueHint {
|
||||
/// Accepted types in preference order.
|
||||
pub types: Vec<ValueType>,
|
||||
/// Optional index hint.
|
||||
pub index: i32,
|
||||
/// Optional tag (e.g. track reference).
|
||||
pub tag: String,
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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/>.
|
||||
|
||||
//! Keyframes and interpolation (C++ `NodeKeyframe` + track logic).
|
||||
|
||||
use oakcore_rs::Rational;
|
||||
|
||||
use crate::value::NodeValue;
|
||||
|
||||
/// Interpolation mode (values match the C++ `NodeKeyframe::Type`).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Interpolation {
|
||||
/// Constant hold.
|
||||
Hold,
|
||||
/// Linear.
|
||||
Linear,
|
||||
/// Cubic bezier (control points on the keyframe).
|
||||
Bezier,
|
||||
}
|
||||
|
||||
/// A single keyframe.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Keyframe {
|
||||
/// Time.
|
||||
pub time: Rational,
|
||||
/// Value.
|
||||
pub value: NodeValue,
|
||||
/// Interpolation to the next keyframe.
|
||||
pub interpolation: Interpolation,
|
||||
/// Bezier control points (used when `interpolation == Bezier`).
|
||||
pub bezier_in: (f64, f64),
|
||||
/// Bezier out control point.
|
||||
pub bezier_out: (f64, f64),
|
||||
}
|
||||
|
||||
/// Sorted keyframe track for one (input, element).
|
||||
#[derive(Default, Debug)]
|
||||
pub struct KeyframeTrack {
|
||||
keys: Vec<Keyframe>,
|
||||
}
|
||||
|
||||
impl KeyframeTrack {
|
||||
/// Insert or replace the keyframe at `time` (keeps sort order).
|
||||
pub fn set_key(&mut self, key: Keyframe) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Remove the keyframe at `time`; false when absent.
|
||||
pub fn remove_key(&mut self, time: Rational) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Interpolated value at `time`; `None` on an empty track.
|
||||
/// Interpolation math must match the C++ lerp/bezier exactly
|
||||
/// (`// CPP-PARITY: nodekeyframe.cpp`).
|
||||
pub fn value_at(&self, time: Rational) -> Option<NodeValue> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Sorted keyframes view.
|
||||
pub fn keys(&self) -> &[Keyframe] {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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/>.
|
||||
|
||||
//! # oaknode — the node graph engine (Rust)
|
||||
//!
|
||||
//! Reimplements the C++ oaknode module behind its frozen C ABI
|
||||
//! (`include/node/*.h`). See README.md for the architectural mapping
|
||||
//! (inheritance → arena + trait objects, etc.).
|
||||
//!
|
||||
//! ## FFI discipline
|
||||
//!
|
||||
//! Identical to the oakplugin crate: every export goes through
|
||||
//! [`handle::guard*`], handles are opaque refcounted boxes, shared
|
||||
//! state behind `Mutex`.
|
||||
|
||||
#![deny(unsafe_op_in_unsafe_fn)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub mod block;
|
||||
pub mod bridge;
|
||||
pub mod colormanager;
|
||||
pub mod error;
|
||||
pub mod factory;
|
||||
pub mod ffi;
|
||||
pub mod folder;
|
||||
pub mod footage;
|
||||
pub mod graph;
|
||||
pub mod handle;
|
||||
pub mod id;
|
||||
pub mod input;
|
||||
pub mod keyframe;
|
||||
pub mod node;
|
||||
pub mod nodes;
|
||||
pub mod ops;
|
||||
pub mod project;
|
||||
pub mod sequence;
|
||||
pub mod serializer;
|
||||
pub mod track;
|
||||
pub mod traverser;
|
||||
pub mod value;
|
||||
@@ -0,0 +1,310 @@
|
||||
// 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/>.
|
||||
|
||||
//! Node core data and the behavior trait — the C++ `Node` class,
|
||||
//! restructured for Rust. COVERAGE.md maps every C++ method to its
|
||||
//! Rust home; this file carries the virtual surface ([`NodeBehavior`])
|
||||
//! and the shared data ([`NodeCore`]).
|
||||
|
||||
use oakcore_rs::{Rational, TimeRange};
|
||||
|
||||
use crate::id::NodeId;
|
||||
use crate::input::{Input, ValueHint};
|
||||
use crate::keyframe::KeyframeTrack;
|
||||
use crate::value::{NodeValue, NodeValueRow, NodeValueTable};
|
||||
|
||||
/// Node category (mirrors C++ `Node::CategoryID` order).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Category {
|
||||
/// Output nodes.
|
||||
Output,
|
||||
/// Effects.
|
||||
Effect,
|
||||
/// Generators.
|
||||
Generator,
|
||||
/// Inputs (footage).
|
||||
Input,
|
||||
/// Math/combine.
|
||||
Math,
|
||||
/// Color.
|
||||
Color,
|
||||
/// Distort.
|
||||
Distort,
|
||||
/// Filter.
|
||||
Filter,
|
||||
/// Keying.
|
||||
Keying,
|
||||
/// OpenFX plugins.
|
||||
OpenFx,
|
||||
/// Timeline structural (tracks/blocks).
|
||||
Timeline,
|
||||
/// Groups.
|
||||
Group,
|
||||
}
|
||||
|
||||
/// A gizmo: viewport-interaction data object (C++ `NodeGizmo`). Data
|
||||
/// only — drawing and mouse handling live in facade/app.
|
||||
pub struct Gizmo {
|
||||
/// Keyframed position inputs (track references).
|
||||
pub position_inputs: Vec<(String, i32, i32)>,
|
||||
/// Current drag position.
|
||||
pub drag_point: (f64, f64),
|
||||
}
|
||||
|
||||
/// Shared per-node data (the C++ `Node` member fields). Behavior lives
|
||||
/// in [`NodeBehavior`].
|
||||
pub struct NodeCore {
|
||||
/// Inputs by id (array inputs hold multiple elements).
|
||||
pub inputs: Vec<Input>,
|
||||
/// Keyframe tracks per (input, element).
|
||||
pub keyframes: Vec<(String, i32, KeyframeTrack)>,
|
||||
/// Caches as oakrender handles (created via bridge::render).
|
||||
pub caches: NodeCaches,
|
||||
/// Node flags bitmask (hidden, dont-show-in-param-view, ...).
|
||||
pub flags: u64,
|
||||
/// Editor position (serialization only).
|
||||
pub position: (f64, f64),
|
||||
/// User label (C++ label_).
|
||||
pub label: String,
|
||||
/// Color override index (-1 = none).
|
||||
pub override_color: i32,
|
||||
/// Effect input id (C++ effect_input_).
|
||||
pub effect_input: String,
|
||||
/// Value hints per (input, element).
|
||||
pub hints: Vec<((String, i32), ValueHint)>,
|
||||
/// Group-context positions (C++ context_positions_).
|
||||
pub context_positions: Vec<(NodeId, (f64, f64), bool)>,
|
||||
/// Linked nodes (C++ links_).
|
||||
pub links: Vec<NodeId>,
|
||||
/// Bin folder membership (None = not in the bin).
|
||||
pub bin_folder: Option<NodeId>,
|
||||
/// Caches master toggle (C++ caches_enabled_).
|
||||
pub caches_enabled: bool,
|
||||
/// Gizmos owned by this node.
|
||||
pub gizmos: Vec<Gizmo>,
|
||||
/// Currently dragged gizmo index.
|
||||
pub current_gizmo: Option<usize>,
|
||||
}
|
||||
|
||||
/// The node's oakrender caches (frame/thumbnail/audio/waveform),
|
||||
/// owned handles released with the node.
|
||||
pub struct NodeCaches {
|
||||
/// Video frame hash cache.
|
||||
pub video: crate::bridge::render::CacheHandle,
|
||||
/// Thumbnail cache.
|
||||
pub thumbnail: crate::bridge::render::CacheHandle,
|
||||
/// Audio playback cache.
|
||||
pub audio: crate::bridge::render::CacheHandle,
|
||||
/// Waveform cache.
|
||||
pub waveform: crate::bridge::render::CacheHandle,
|
||||
}
|
||||
|
||||
/// The polymorphic surface of a node — every C++ virtual on `Node`
|
||||
/// becomes a method here (see COVERAGE.md §1/§7/§8/§10).
|
||||
pub trait NodeBehavior: Send {
|
||||
/// Human-readable name (C++ `name()`).
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Short menu name (C++ `short_name()`; defaults to [`Self::name`]).
|
||||
fn short_name(&self) -> &str {
|
||||
self.name()
|
||||
}
|
||||
|
||||
/// Stable type id (C++ `id()`).
|
||||
fn type_id(&self) -> &str;
|
||||
|
||||
/// Categories (C++ `category()`).
|
||||
fn categories(&self) -> &[Category] {
|
||||
&[]
|
||||
}
|
||||
|
||||
/// Sub-category (C++ `sub_category()`).
|
||||
fn sub_category(&self) -> &str {
|
||||
""
|
||||
}
|
||||
|
||||
/// Description (C++ `description()`).
|
||||
fn description(&self) -> &str {
|
||||
""
|
||||
}
|
||||
|
||||
/// Localized input name (C++ `get_input_name()` virtual).
|
||||
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
||||
id
|
||||
}
|
||||
|
||||
/// Inputs excluded from rendering (C++ `ignore_inputs_for_rendering()`).
|
||||
fn ignore_inputs_for_rendering(&self) -> &[String] {
|
||||
&[]
|
||||
}
|
||||
|
||||
/// Array elements active at `time` (C++ `get_active_elements_at_time()`).
|
||||
fn active_elements_at_time(&self, input: &str, time: Rational) -> Vec<i32> {
|
||||
let _ = (input, time);
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// Cache ranges (C++ `get_video_cache_range()` /
|
||||
/// `get_audio_cache_range()`).
|
||||
fn video_cache_range(&self, core: &NodeCore) -> TimeRange {
|
||||
let _ = core;
|
||||
TimeRange::default()
|
||||
}
|
||||
|
||||
/// Audio cache range.
|
||||
fn audio_cache_range(&self, core: &NodeCore) -> TimeRange {
|
||||
let _ = core;
|
||||
TimeRange::default()
|
||||
}
|
||||
|
||||
/// Value hint for an input (C++ `get_value_hint_for_input()` virtual).
|
||||
fn value_hint_for_input(&self, input: &str) -> Option<ValueHint> {
|
||||
let _ = input;
|
||||
None
|
||||
}
|
||||
|
||||
/// Render-time connection resolution (C++
|
||||
/// `get_connected_render_output()`; Group overrides).
|
||||
fn connected_render_output(&self, core: &NodeCore, input: &str, element: i32) -> Option<NodeId> {
|
||||
let _ = (core, input, element);
|
||||
None
|
||||
}
|
||||
|
||||
/// Time adjustment through this node (C++
|
||||
/// `input_time_adjustment()`/`output_time_adjustment()`; clips
|
||||
/// override for speed/reverse).
|
||||
fn input_time_adjustment(&self, input: &str, element: i32, time: TimeRange, traverse: bool) -> TimeRange {
|
||||
let _ = (input, element, traverse);
|
||||
time
|
||||
}
|
||||
|
||||
/// Output-side time adjustment.
|
||||
fn output_time_adjustment(&self, input: &str, element: i32, time: TimeRange, traverse: bool) -> TimeRange {
|
||||
let _ = (input, element, traverse);
|
||||
time
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`).
|
||||
fn value(&self, core: &NodeCore, inputs: &NodeValueRow, time: Rational, table: &mut NodeValueTable) {
|
||||
let _ = (core, inputs, time, table);
|
||||
}
|
||||
|
||||
/// Process a span of samples (C++ `process_samples()`).
|
||||
fn process_samples(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
inputs: &NodeValueRow,
|
||||
range: TimeRange,
|
||||
output: &mut crate::value::SampleBuffer,
|
||||
) {
|
||||
let _ = (core, inputs, range, output);
|
||||
}
|
||||
|
||||
/// Direct frame generation (C++ `generate_frame()`; CPU-render
|
||||
/// nodes).
|
||||
fn generate_frame(&self, core: &NodeCore, frame: &mut crate::bridge::render::TextureHandle, time: Rational) {
|
||||
let _ = (core, frame, time);
|
||||
}
|
||||
|
||||
/// Shader code request (C++ `get_shader_code()`; GPU nodes).
|
||||
fn shader_code(&self, request: &str) -> Option<String> {
|
||||
let _ = request;
|
||||
None
|
||||
}
|
||||
|
||||
/// Gizmo transform/positions (C++ `gizmo_transformation()` /
|
||||
/// `update_gizmo_positions()`).
|
||||
fn gizmo_update(&self, core: &NodeCore, row: &NodeValueRow) {
|
||||
let _ = (core, row);
|
||||
}
|
||||
|
||||
/// Gizmo drag callbacks (C++ `gizmo_drag_start/move`).
|
||||
fn gizmo_drag(&mut self, core: &mut NodeCore, start: bool, x: f64, y: f64, modifiers: u32) {
|
||||
let _ = (core, start, x, y, modifiers);
|
||||
}
|
||||
|
||||
/// Input value changed (C++ `InputValueChangedEvent`).
|
||||
fn input_value_changed(&mut self, core: &mut NodeCore, input: &str, element: i32) {
|
||||
let _ = (core, input, element);
|
||||
}
|
||||
|
||||
/// Edge connected to an input (C++ `InputConnectedEvent`).
|
||||
fn input_connected(&mut self, core: &mut NodeCore, input: &str, element: i32, source: NodeId) {
|
||||
let _ = (core, input, element, source);
|
||||
}
|
||||
|
||||
/// Edge disconnected from an input (C++ `InputDisconnectedEvent`).
|
||||
fn input_disconnected(&mut self, core: &mut NodeCore, input: &str, element: i32, source: NodeId) {
|
||||
let _ = (core, input, element, source);
|
||||
}
|
||||
|
||||
/// Someone connected to this node's output (C++
|
||||
/// `OutputConnectedEvent`).
|
||||
fn output_connected(&mut self, core: &mut NodeCore, target: NodeId, input: &str, element: i32) {
|
||||
let _ = (core, target, input, element);
|
||||
}
|
||||
|
||||
/// Output disconnected (C++ `OutputDisconnectedEvent`).
|
||||
fn output_disconnected(&mut self, core: &mut NodeCore, target: NodeId, input: &str, element: i32) {
|
||||
let _ = (core, target, input, element);
|
||||
}
|
||||
|
||||
/// Attached to a preview/viewer (C++ `ConnectedToPreviewEvent`).
|
||||
fn connected_to_preview(&mut self, core: &mut NodeCore) {
|
||||
let _ = core;
|
||||
}
|
||||
|
||||
/// Inserted into / removed from a project graph (C++
|
||||
/// `AddedToGraphEvent` / `RemovedFromGraphEvent`).
|
||||
fn added_to_graph(&mut self, core: &mut NodeCore) {
|
||||
let _ = core;
|
||||
}
|
||||
|
||||
/// See [`NodeBehavior::added_to_graph`].
|
||||
fn removed_from_graph(&mut self, core: &mut NodeCore) {
|
||||
let _ = core;
|
||||
}
|
||||
|
||||
/// Node links changed (C++ `LinkChangeEvent`).
|
||||
fn link_changed(&mut self, core: &mut NodeCore) {
|
||||
let _ = core;
|
||||
}
|
||||
|
||||
/// Deep copy (C++ `copy()`); None = not copiable.
|
||||
fn duplicate(&self, core: &NodeCore) -> Option<Box<dyn NodeBehavior>>;
|
||||
|
||||
/// Custom load/save (C++ `load_custom()`/`save_custom()`).
|
||||
fn load_custom(&mut self, core: &mut NodeCore, reader: &mut dyn crate::serializer::XmlRead) -> bool {
|
||||
let _ = (core, reader);
|
||||
true
|
||||
}
|
||||
|
||||
/// See [`NodeBehavior::load_custom`].
|
||||
fn save_custom(&self, core: &NodeCore, writer: &mut dyn crate::serializer::XmlWrite) {
|
||||
let _ = (core, writer);
|
||||
}
|
||||
|
||||
/// Post-load fixups (C++ `PostLoadEvent` / `LoadFinishedEvent`).
|
||||
fn post_load(&mut self, core: &mut NodeCore) {
|
||||
let _ = core;
|
||||
}
|
||||
|
||||
/// Legacy input id mapping for old project versions (C++
|
||||
/// `get_input_id_for_legacy_id()`; default identity).
|
||||
fn map_legacy_input_id<'a>(&self, id: &'a str) -> &'a str {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
// 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/>.
|
||||
|
||||
//! Blur filter (C++ `src/node/src/filter/blur/blur.{h,cpp}`,
|
||||
//! `olive::BlurFilterNode`).
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
use crate::node::{Category, NodeBehavior, NodeCore};
|
||||
|
||||
/// Texture input id (C++ `k_texture_input`). Type: texture; flags:
|
||||
/// not-keyframable; this is the node's effect input.
|
||||
pub const TEXTURE_INPUT: &str = "tex_in";
|
||||
|
||||
/// Blur method input id (C++ `k_method_input`). Type: combo; default
|
||||
/// `k_gaussian` (1); flags: not-keyframable, not-connectable; combo box
|
||||
/// strings: "Box", "Gaussian", "Directional", "Radial".
|
||||
pub const METHOD_INPUT: &str = "method_in";
|
||||
|
||||
/// Blur radius input id (C++ `k_radius_input`). Type: float; default
|
||||
/// `10.0`; properties: `min = 0.0`.
|
||||
pub const RADIUS_INPUT: &str = "radius_in";
|
||||
|
||||
/// Horizontal blur toggle input id (C++ `k_horiz_input`). Type: bool;
|
||||
/// default `true`; hidden unless the method is box or gaussian.
|
||||
pub const HORIZ_INPUT: &str = "horiz_in";
|
||||
|
||||
/// Vertical blur toggle input id (C++ `k_vert_input`). Type: bool;
|
||||
/// default `true`; hidden unless the method is box or gaussian.
|
||||
pub const VERT_INPUT: &str = "vert_in";
|
||||
|
||||
/// Repeat-edge-pixels input id (C++ `k_repeat_edge_pixels_input`). Type:
|
||||
/// bool; default `true`.
|
||||
pub const REPEAT_EDGE_PIXELS_INPUT: &str = "repeat_edge_pixels_in";
|
||||
|
||||
/// Directional angle input id (C++ `k_directional_degrees_input`). Type:
|
||||
/// float; default `0.0`; hidden unless the method is directional.
|
||||
pub const DIRECTIONAL_DEGREES_INPUT: &str = "directional_degrees_in";
|
||||
|
||||
/// Radial center input id (C++ `k_radial_center_input`). Type: vec2;
|
||||
/// default `(0.0, 0.0)`; hidden unless the method is radial; property
|
||||
/// `offset` is set to half the texture resolution at gizmo-update time.
|
||||
pub const RADIAL_CENTER_INPUT: &str = "radial_center_in";
|
||||
|
||||
/// Blur method enum (C++ `BlurFilterNode::Method`).
|
||||
pub enum Method {
|
||||
/// Box blur.
|
||||
Box,
|
||||
/// Gaussian blur (the default).
|
||||
Gaussian,
|
||||
/// Directional blur.
|
||||
Directional,
|
||||
/// Radial blur.
|
||||
Radial,
|
||||
}
|
||||
|
||||
/// Blur filter node. Box/gaussian/directional/radial blur, implemented
|
||||
/// as one iterative shader.
|
||||
///
|
||||
/// C++ member `radial_center_gizmo_` is a Qt `PointGizmo` (GUI type);
|
||||
/// gizmos live in `NodeCore::gizmos`, so no field is kept here.
|
||||
pub struct BlurFilterNode;
|
||||
|
||||
/// Fragment shader (C++ `get_shader_code()` loads
|
||||
/// `:/shaders/blur.frag` via FileFunctions for any request). Text
|
||||
/// copied verbatim from `engine/shaders/blur.frag`.
|
||||
const SHADER_FRAG: &str = r#"uniform sampler2D tex_in;
|
||||
uniform int method_in;
|
||||
uniform float radius_in;
|
||||
uniform bool horiz_in;
|
||||
uniform bool vert_in;
|
||||
uniform bool repeat_edge_pixels_in;
|
||||
uniform vec2 resolution_in;
|
||||
|
||||
// Directional
|
||||
uniform float directional_degrees_in;
|
||||
|
||||
// Radial
|
||||
uniform vec2 radial_center_in;
|
||||
|
||||
uniform int ove_iteration;
|
||||
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
// Gaussian function uses PI
|
||||
#define M_PI 3.1415926535897932384626433832795
|
||||
|
||||
// Methods
|
||||
#define METHOD_BOX_BLUR 0
|
||||
#define METHOD_GAUSSIAN_BLUR 1
|
||||
#define METHOD_DIRECTIONAL_BLUR 2
|
||||
#define METHOD_RADIAL_BLUR 3
|
||||
|
||||
// Mode
|
||||
#define MODE_NONE 0
|
||||
#define MODE_HORIZONTAL 1
|
||||
#define MODE_VERTICAL 2
|
||||
|
||||
// Single gaussian formula (unused, mainly here for documentation/just in case)
|
||||
//float gaussian(float x, float sigma) {
|
||||
// return (1.0/(sigma*sqrt(2.0*M_PI)))*exp(-0.5*pow(x/sigma, 2.0));
|
||||
//}
|
||||
|
||||
// Double gaussian formula, actually used in the code below
|
||||
// Should be faster than the single gaussian above since it doesn't need sqrt()
|
||||
float gaussian2(float x, float y, float sigma) {
|
||||
return (1.0/((sigma*sigma)*2.0*M_PI))*exp(-0.5*(((x*x) + (y*y))/(sigma*sigma)));
|
||||
}
|
||||
|
||||
int determine_mode() {
|
||||
if (radius_in == 0.0) {
|
||||
return MODE_NONE;
|
||||
}
|
||||
|
||||
if (!horiz_in && !vert_in) {
|
||||
return MODE_NONE;
|
||||
}
|
||||
|
||||
if (horiz_in && !vert_in) {
|
||||
return MODE_HORIZONTAL;
|
||||
}
|
||||
|
||||
if (vert_in && !horiz_in) {
|
||||
return MODE_VERTICAL;
|
||||
}
|
||||
|
||||
if (ove_iteration == 0) {
|
||||
return MODE_HORIZONTAL;
|
||||
}
|
||||
|
||||
if (ove_iteration == 1) {
|
||||
return MODE_VERTICAL;
|
||||
}
|
||||
}
|
||||
|
||||
vec4 add_to_composite(vec4 composite, vec2 pixel_coord, float weight)
|
||||
{
|
||||
if (repeat_edge_pixels_in
|
||||
|| (pixel_coord.x >= 0.0
|
||||
&& pixel_coord.x < 1.0
|
||||
&& pixel_coord.y >= 0.0
|
||||
&& pixel_coord.y < 1.0)) {
|
||||
composite += texture(tex_in, pixel_coord) * weight;
|
||||
}
|
||||
|
||||
return composite;
|
||||
}
|
||||
|
||||
void main(void) {
|
||||
int mode = determine_mode();
|
||||
|
||||
if (mode == MODE_NONE) {
|
||||
frag_color = texture(tex_in, ove_texcoord);
|
||||
return;
|
||||
}
|
||||
|
||||
// We only sample on hard pixels, so we don't accept decimal radii
|
||||
float real_radius = ceil(radius_in);
|
||||
|
||||
vec4 composite = vec4(0.0);
|
||||
|
||||
float divider, sigma;
|
||||
|
||||
if (method_in == METHOD_DIRECTIONAL_BLUR || method_in == METHOD_RADIAL_BLUR) {
|
||||
// Despite similar math, these are lighter methods perceptually, so we double the radius to
|
||||
// better match box/gaussian
|
||||
real_radius *= 2.0;
|
||||
}
|
||||
|
||||
if (method_in == METHOD_BOX_BLUR || method_in == METHOD_DIRECTIONAL_BLUR) {
|
||||
|
||||
// Calculate the weight of each pixel based on the radius
|
||||
divider = 1.0 / real_radius;
|
||||
|
||||
} else if (method_in == METHOD_GAUSSIAN_BLUR) {
|
||||
|
||||
// Using (radius = 3 * sigma) because 3 standard deviations covers 97% of the blur according to this document:
|
||||
// http://chemaguerra.com/gaussian-filter-radius/
|
||||
sigma = real_radius;
|
||||
real_radius *= 3.0;
|
||||
|
||||
// Use gaussian formula to calculate the weight of all pixels
|
||||
divider = 0.0;
|
||||
for (float i = -real_radius + 0.5; i <= real_radius; i += 2.0) {
|
||||
divider += gaussian2(i, 0.0, sigma);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (method_in == METHOD_BOX_BLUR || method_in == METHOD_GAUSSIAN_BLUR) {
|
||||
for (float i = -real_radius + 0.5; i <= real_radius; i += 2.0) {
|
||||
float weight;
|
||||
|
||||
if (method_in == METHOD_BOX_BLUR) {
|
||||
weight = divider;
|
||||
} else if (method_in == METHOD_GAUSSIAN_BLUR) {
|
||||
weight = gaussian2(i, 0.0, sigma) / divider;
|
||||
}
|
||||
|
||||
vec2 pixel_coord = ove_texcoord;
|
||||
if (mode == MODE_HORIZONTAL) {
|
||||
pixel_coord.x += i / resolution_in.x;
|
||||
} else if (mode == MODE_VERTICAL) {
|
||||
pixel_coord.y += i / resolution_in.y;
|
||||
}
|
||||
|
||||
composite = add_to_composite(composite, pixel_coord, weight);
|
||||
}
|
||||
} else if (method_in == METHOD_DIRECTIONAL_BLUR || method_in == METHOD_RADIAL_BLUR) {
|
||||
float angle;
|
||||
|
||||
if (method_in == METHOD_DIRECTIONAL_BLUR) {
|
||||
// Convert directional degrees to radians
|
||||
angle = (directional_degrees_in*M_PI)/180.0;
|
||||
} else {
|
||||
// Calculate angle from distance of center to current coordinate
|
||||
vec2 distance = (ove_texcoord - 0.5) * (resolution_in) - radial_center_in;
|
||||
angle = atan(distance.y/distance.x);
|
||||
|
||||
float multiplier = length(distance) / resolution_in.y * 2.0;
|
||||
|
||||
real_radius = ceil(radius_in * multiplier);
|
||||
divider = 1.0 / real_radius;
|
||||
}
|
||||
|
||||
// Get angles
|
||||
float sin_angle = sin(angle);
|
||||
float cos_angle = cos(angle);
|
||||
|
||||
for (float i = -real_radius + 0.5; i <= real_radius; i += 2.0) {
|
||||
vec2 pixel_coord = ove_texcoord;
|
||||
|
||||
pixel_coord.y += sin_angle * i / resolution_in.y;
|
||||
pixel_coord.x += cos_angle * i / resolution_in.x;
|
||||
|
||||
composite = add_to_composite(composite, pixel_coord, divider);
|
||||
}
|
||||
}
|
||||
|
||||
frag_color = composite;
|
||||
}
|
||||
"#;
|
||||
|
||||
impl BlurFilterNode {
|
||||
/// Fragment shader for any request (C++ `get_shader_code()` ignores
|
||||
/// the request id and always returns `blur.frag`).
|
||||
fn shader_frag() -> &'static str {
|
||||
SHADER_FRAG
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeBehavior for BlurFilterNode {
|
||||
/// Human-readable name (C++ `name()`).
|
||||
fn name(&self) -> &str {
|
||||
"Blur"
|
||||
}
|
||||
|
||||
/// Stable type id (C++ `id()`).
|
||||
fn type_id(&self) -> &str {
|
||||
"org.olivevideoeditor.Olive.blur"
|
||||
}
|
||||
|
||||
/// Categories (C++ `category()`).
|
||||
fn categories(&self) -> &[Category] {
|
||||
&[Category::Filter]
|
||||
}
|
||||
|
||||
/// Description (C++ `description()`).
|
||||
fn description(&self) -> &str {
|
||||
"Blurs an image."
|
||||
}
|
||||
|
||||
/// Localized input names (C++ `retranslate()`): `tex_in` -> "Input",
|
||||
/// `method_in` -> "Method" (combo strings "Box", "Gaussian",
|
||||
/// "Directional", "Radial"), `radius_in` -> "Radius", `horiz_in` ->
|
||||
/// "Horizontal", `vert_in` -> "Vertical", `repeat_edge_pixels_in` ->
|
||||
/// "Repeat Edge Pixels", `directional_degrees_in` -> "Direction",
|
||||
/// `radial_center_in` -> "Center".
|
||||
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): no texture -> push nothing;
|
||||
/// radius <= 0.0, or box/gaussian with both horiz and vert unchecked
|
||||
/// -> pass-through push of the input texture; otherwise push a shader
|
||||
/// job with `resolution_in` set to the texture's virtual resolution,
|
||||
/// running 2 iterations for box/gaussian when both horiz and vert are
|
||||
/// checked (1 otherwise).
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
time: oakcore_rs::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Shader code request (C++ `get_shader_code()`): the request id is
|
||||
/// ignored; always returns the blur fragment shader.
|
||||
fn shader_code(&self, request: &str) -> Option<String> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Gizmo positions (C++ `update_gizmo_positions()`): when the method
|
||||
/// is radial and a texture is present, show the radial-center gizmo
|
||||
/// at half the texture resolution plus the center input, and set the
|
||||
/// input's `offset` property to half the resolution; otherwise hide
|
||||
/// the gizmo.
|
||||
fn gizmo_update(&self, core: &NodeCore, row: &crate::value::NodeValueRow) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Gizmo drag (C++ `gizmo_drag_move()`): when the current gizmo is
|
||||
/// the radial-center gizmo, drag its x/y input draggers by the drag
|
||||
/// delta.
|
||||
fn gizmo_drag(&mut self, core: &mut NodeCore, start: bool, x: f64, y: f64, modifiers: u32) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Input value changed (C++ `InputValueChangedEvent()`): on
|
||||
/// `method_in` changes, re-run the hidden-flag update (`horiz_in` /
|
||||
/// `vert_in` shown only for box/gaussian, `directional_degrees_in`
|
||||
/// only for directional, `radial_center_in` only for radial), then
|
||||
/// defer to the base implementation.
|
||||
fn input_value_changed(&mut self, core: &mut NodeCore, input: &str, element: i32) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Deep copy (C++ `copy()`).
|
||||
fn duplicate(&self, core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor (C++ `BlurFilterNode::BlurFilterNode()`): adds `tex_in`,
|
||||
/// `method_in` (default gaussian), `radius_in`, `horiz_in`/`vert_in`,
|
||||
/// `directional_degrees_in`, `radial_center_in` with the defaults and
|
||||
/// properties documented on the constants, hides the method-specific
|
||||
/// inputs for the default method, adds `repeat_edge_pixels_in`, sets
|
||||
/// the video-effect flag and the effect input, and adds a draggable
|
||||
/// anchor-point gizmo bound to both tracks of `radial_center_in`.
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_blur_filter` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.blur",
|
||||
name: "Blur",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
// 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/>.
|
||||
|
||||
//! Chroma Key effect (C++ `src/node/src/keying/chromakey/chromakey.{h,cpp}`,
|
||||
//! `olive::ChromaKeyNode`, derived from `olive::OCIOBaseNode`).
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
use crate::node::{Category, NodeBehavior, NodeCore};
|
||||
|
||||
/// Key color input id (C++ `k_color_input`). Type: color; default
|
||||
/// `Color(0.0, 1.0, 0.0, 1.0)` (opaque green).
|
||||
pub const COLOR_INPUT: &str = "color_key";
|
||||
|
||||
/// Show-mask-only toggle input id (C++ `k_mask_only_input`). Type:
|
||||
/// boolean; default `false`.
|
||||
pub const MASK_ONLY_INPUT: &str = "mask_only_in";
|
||||
|
||||
/// Invert-mask toggle input id (C++ `k_invert_input`). Type: boolean;
|
||||
/// default `false`.
|
||||
pub const INVERT_INPUT: &str = "invert_in";
|
||||
|
||||
/// Upper tolerance input id (C++ `k_upper_tolerance_input`). Type:
|
||||
/// float; default `25.0`; properties: `base = 0.1` (the `min` property
|
||||
/// tracking the lower tolerance is present but disabled in C++ — see
|
||||
/// the FIXME in the constructor).
|
||||
pub const UPPER_TOLERANCE_INPUT: &str = "upper_tolerance_in";
|
||||
|
||||
/// Lower tolerance input id (C++ `k_lower_tolerance_input`). Type:
|
||||
/// float; default `5.0`; properties: `min = 0.0`, `base = 0.1`.
|
||||
pub const LOWER_TOLERANCE_INPUT: &str = "lower_tolerance_in";
|
||||
|
||||
/// Garbage matte texture input id (C++ `k_garbage_matte_input`). Type:
|
||||
/// texture; flags: not-keyframable.
|
||||
pub const GARBAGE_MATTE_INPUT: &str = "garbage_in";
|
||||
|
||||
/// Core matte texture input id (C++ `k_core_matte_input`). Type:
|
||||
/// texture; flags: not-keyframable.
|
||||
pub const CORE_MATTE_INPUT: &str = "core_in";
|
||||
|
||||
/// Shadows input id (C++ `k_shadows_input`). Type: float; default
|
||||
/// `100.0`; properties: `min = 0.0`, `base = 0.1`.
|
||||
pub const SHADOWS_INPUT: &str = "shadows_in";
|
||||
|
||||
/// Highlights input id (C++ `k_highlights_input`). Type: float;
|
||||
/// default `100.0`; properties: `min = 0.0`, `base = 0.1`.
|
||||
pub const HIGHLIGHTS_INPUT: &str = "highlights_in";
|
||||
|
||||
/// Chroma key node: keys on the CIE Lab distance from a selected
|
||||
/// color, with optional garbage/core mattes.
|
||||
///
|
||||
/// The C++ class derives from `OCIOBaseNode`, which owns the `tex_in`
|
||||
/// texture input (C++ `OCIOBaseNode::k_texture_input = "tex_in"`, the
|
||||
/// effect input), the color manager pointer, and the OCIO color
|
||||
/// processor handle; that state is held here via the shared
|
||||
/// `super::ociobase` helper. The class has no other own members (the
|
||||
/// private `generate_processor()` is a method, not state).
|
||||
pub struct ChromaKeyNode {
|
||||
/// OCIO base state (C++ base class `OCIOBaseNode`: `manager_` and
|
||||
/// `processor_`).
|
||||
base: super::ociobase::OcioBase,
|
||||
}
|
||||
|
||||
/// Fragment shader (C++ loads the `:/shaders/chromakey.frag` resource
|
||||
/// in `get_shader_code`). Text copied verbatim from
|
||||
/// `engine/shaders/chromakey.frag`. The `%1` marker is replaced with
|
||||
/// the OCIO-generated shader stub (`request.stub`) at request time;
|
||||
/// the shader calls `SceneLinearToCIEXYZ_d65`, which the stub must
|
||||
/// define. Note the shader still uses the legacy misspelled uniform
|
||||
/// names `upper_tolerence_in`/`lower_tolerence_in`, matching the old
|
||||
/// input ids remapped by `map_legacy_input_id`.
|
||||
const SHADER_FRAG: &str = r#"// Main texture input
|
||||
uniform sampler2D tex_in;
|
||||
uniform vec4 color_key;
|
||||
uniform bool mask_only_in;
|
||||
uniform float upper_tolerence_in;
|
||||
uniform float lower_tolerence_in;
|
||||
|
||||
uniform sampler2D garbage_in;
|
||||
uniform sampler2D core_in;
|
||||
uniform bool garbage_in_enabled;
|
||||
uniform bool core_in_enabled;
|
||||
uniform bool invert_in;
|
||||
|
||||
uniform float highlights_in;
|
||||
uniform float shadows_in;
|
||||
|
||||
|
||||
// Main texture coordinate
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
// Program will replace this with OCIO's auto-generated shader code
|
||||
%1
|
||||
|
||||
// Assume D65 white point
|
||||
float Xn = 95.0489;
|
||||
float Yn = 100.0;
|
||||
float Zn = 108.8840;
|
||||
float delta = 0.20689655172; // 6/29
|
||||
|
||||
float func(float t) {
|
||||
if (t > pow(delta, 3.0)){
|
||||
return pow(t, 1.0/3.0);
|
||||
} else{
|
||||
return (t / (3.0 * pow(delta, 2))) + 4.0/29.0;
|
||||
}
|
||||
}
|
||||
|
||||
vec4 CIExyz_to_Lab(vec4 CIE) {
|
||||
vec4 lab;
|
||||
lab.r = 116.0 * func(CIE.g / Yn) - 16.0;
|
||||
lab.g = 500.0 * (func(CIE.r / Xn) - func(CIE.g / Yn));
|
||||
lab.b = 200.0 * (func(CIE.g / Yn) - func(CIE.b / Zn));
|
||||
lab.w = CIE.w;
|
||||
|
||||
return lab;
|
||||
}
|
||||
|
||||
float colorclose(vec4 col, vec4 key, float tola,float tolb) {
|
||||
// Decides if a color is close to the specified hue
|
||||
float temp = sqrt(((key.g-col.g)*(key.g-col.g))+((key.b-col.b)*(key.b-col.b))+((key.r-col.r)*(key.r-col.r)));
|
||||
if (temp < tola) {return (0.0);}
|
||||
if (temp < tolb) {return ((temp-tola)/(tolb-tola));}
|
||||
return (1.0);
|
||||
}
|
||||
|
||||
|
||||
void main() {
|
||||
|
||||
vec4 col = texture(tex_in, ove_texcoord);
|
||||
|
||||
vec4 unassoc = col;
|
||||
if (unassoc.a > 0) {
|
||||
unassoc.rgb /= unassoc.a;
|
||||
}
|
||||
|
||||
// Perform color conversion
|
||||
vec4 cie_xyz = SceneLinearToCIEXYZ_d65(unassoc);
|
||||
vec4 lab = CIExyz_to_Lab(cie_xyz);
|
||||
|
||||
vec4 cie_xyz_key = SceneLinearToCIEXYZ_d65(color_key);
|
||||
vec4 lab_key = CIExyz_to_Lab(cie_xyz_key);
|
||||
|
||||
float mask = colorclose(lab, lab_key, lower_tolerence_in, upper_tolerence_in);
|
||||
|
||||
mask = clamp(mask, 0.0, 1.0);
|
||||
|
||||
if (garbage_in_enabled) {
|
||||
// Force anything we want to remove to be 0.0
|
||||
vec4 garbage = texture(garbage_in, ove_texcoord);
|
||||
// Assumes garbage is achromatic
|
||||
mask -= garbage.r;
|
||||
mask = clamp(mask, 0.0, 1.0);
|
||||
}
|
||||
|
||||
if (core_in_enabled) {
|
||||
// Force anything we want to keep to be 1.0
|
||||
vec3 core = texture(core_in, ove_texcoord).rgb;
|
||||
// Assumes core is achromatic
|
||||
mask += core.r;
|
||||
mask = clamp(mask, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// Crush blacks and push whites
|
||||
mask = shadows_in * 0.01 * (highlights_in * 0.01 * mask - 1.0) + 1.0;
|
||||
mask = clamp(mask, 0.0, 1.0);
|
||||
|
||||
// Invert
|
||||
if (invert_in) {
|
||||
mask = 1.0 - mask;
|
||||
}
|
||||
|
||||
col *= mask;
|
||||
|
||||
if (!mask_only_in) {
|
||||
frag_color = col;
|
||||
} else {
|
||||
frag_color = vec4(vec3(mask), 1.0);
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
impl ChromaKeyNode {
|
||||
/// Fragment shader with the `%1` OCIO stub marker still in place
|
||||
/// (C++ `get_shader_code()` before the stub substitution).
|
||||
fn shader_frag() -> &'static str {
|
||||
SHADER_FRAG
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeBehavior for ChromaKeyNode {
|
||||
/// Human-readable name (C++ `name()`).
|
||||
fn name(&self) -> &str {
|
||||
"Chroma Key"
|
||||
}
|
||||
|
||||
/// Stable type id (C++ `id()`).
|
||||
fn type_id(&self) -> &str {
|
||||
"org.olivevideoeditor.Olive.chromakey"
|
||||
}
|
||||
|
||||
/// Categories (C++ `category()`).
|
||||
fn categories(&self) -> &[Category] {
|
||||
&[Category::Keying]
|
||||
}
|
||||
|
||||
/// Description (C++ `description()`).
|
||||
fn description(&self) -> &str {
|
||||
"A simple color key based on the distance from the chroma of a selected color."
|
||||
}
|
||||
|
||||
/// Localized input names (C++ `retranslate()`): `tex_in` ->
|
||||
/// "Input", `garbage_in` -> "Garbage Matte", `core_in` ->
|
||||
/// "Core Matte", `color_key` -> "Key Color", `shadows_in` ->
|
||||
/// "Shadows", `highlights_in` -> "Highlights",
|
||||
/// `upper_tolerance_in` -> "Upper Tolerance",
|
||||
/// `lower_tolerance_in` -> "Lower Tolerance", `invert_in` ->
|
||||
/// "Invert Mask", `mask_only_in` -> "Show Mask Only".
|
||||
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Input value changed (C++ `InputValueChangedEvent`): the lower
|
||||
/// tolerance branch that would update the upper tolerance's `min`
|
||||
/// property is disabled in C++ (FIXME); unconditionally
|
||||
/// regenerates the OCIO color processor
|
||||
/// (`generate_processor()`).
|
||||
fn input_value_changed(&mut self, core: &mut NodeCore, input: &str, element: i32) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): no texture on `tex_in` ->
|
||||
/// push nothing; texture present and a valid OCIO processor ->
|
||||
/// push a `ColorTransformJob` wired with the processor, the input
|
||||
/// texture, this node as the custom-shader provider, and the
|
||||
/// function name `SceneLinearToCIEXYZ_d65`.
|
||||
///
|
||||
/// The C++ class also overrides `config_changed()` (pure virtual
|
||||
/// on `OCIOBaseNode`) to regenerate the processor when the OCIO
|
||||
/// config changes; `NodeBehavior` has no equivalent hook — that
|
||||
/// wiring belongs to the facade/event layer.
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
time: oakcore_rs::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Shader code request (C++ `get_shader_code()`): reads the
|
||||
/// fragment shader and replaces every `%1` marker with
|
||||
/// `request.stub` (the OCIO auto-generated shader code).
|
||||
fn shader_code(&self, request: &str) -> Option<String> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Legacy input id mapping (C++ `get_input_id_for_legacy_id()`):
|
||||
/// maps the misspelled `upper_tolerence_in` /
|
||||
/// `lower_tolerence_in` from old project files onto
|
||||
/// [`UPPER_TOLERANCE_INPUT`] / [`LOWER_TOLERANCE_INPUT`];
|
||||
/// anything else defers to the default (identity) mapping.
|
||||
fn map_legacy_input_id<'a>(&self, id: &'a str) -> &'a str {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Deep copy (C++ `copy()` via `NODE_DEFAULT_FUNCTIONS`).
|
||||
fn duplicate(&self, core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor (C++ `ChromaKeyNode::ChromaKeyNode()`): the
|
||||
/// `OCIOBaseNode` base adds `tex_in` (texture, not-keyframable), sets
|
||||
/// the video-effect flag and the effect input; this class then adds
|
||||
/// `color_key`, `lower_tolerance_in`, `upper_tolerance_in`,
|
||||
/// `garbage_in`, `core_in`, `highlights_in`, `shadows_in`,
|
||||
/// `invert_in`, and `mask_only_in` with the defaults and properties
|
||||
/// documented on the constants.
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.chromakey`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.chromakey",
|
||||
name: "Chroma Key",
|
||||
categories: &[Category::Keying],
|
||||
create,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// 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/>.
|
||||
|
||||
//! Color Difference Key effect (C++
|
||||
//! `src/node/src/keying/colordifferencekey/colordifferencekey.{h,cpp}`,
|
||||
//! `olive::ColorDifferenceKeyNode`).
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
use crate::node::{Category, NodeBehavior, NodeCore};
|
||||
|
||||
/// Texture input id (C++ `k_texture_input`). Type: texture; flags:
|
||||
/// not-keyframable; this is the node's effect input.
|
||||
pub const TEXTURE_INPUT: &str = "tex_in";
|
||||
|
||||
/// Garbage matte texture input id (C++ `k_garbage_matte_input`). Type:
|
||||
/// texture; flags: not-keyframable.
|
||||
pub const GARBAGE_MATTE_INPUT: &str = "garbage_in";
|
||||
|
||||
/// Core matte texture input id (C++ `k_core_matte_input`). Type:
|
||||
/// texture; flags: not-keyframable.
|
||||
pub const CORE_MATTE_INPUT: &str = "core_in";
|
||||
|
||||
/// Key color input id (C++ `k_color_input`). Type: combo; default `0`;
|
||||
/// combo strings: "Green", "Blue" (set in `retranslate()`).
|
||||
pub const COLOR_INPUT: &str = "color_in";
|
||||
|
||||
/// Shadows input id (C++ `k_shadows_input`). Type: float; default
|
||||
/// `1.0`; properties: `min = 0.0`, `base = 0.01`.
|
||||
pub const SHADOWS_INPUT: &str = "shadows_in";
|
||||
|
||||
/// Highlights input id (C++ `k_highlights_input`). Type: float;
|
||||
/// default `1.0`; properties: `min = 0.0`, `base = 0.01`.
|
||||
pub const HIGHLIGHTS_INPUT: &str = "highlights_in";
|
||||
|
||||
/// Show-mask-only toggle input id (C++ `k_mask_only_input`). Type:
|
||||
/// boolean; default `false`.
|
||||
pub const MASK_ONLY_INPUT: &str = "mask_only_in";
|
||||
|
||||
/// Color difference key node: keys on how far one channel (green or
|
||||
/// blue) stands out from the other two, with optional garbage/core
|
||||
/// mattes. The C++ class has no own members.
|
||||
pub struct ColorDifferenceKeyNode;
|
||||
|
||||
/// Fragment shader (C++ loads the `:/shaders/colordifferencekey.frag`
|
||||
/// resource in `get_shader_code`). Text copied verbatim from
|
||||
/// `engine/shaders/colordifferencekey.frag`.
|
||||
const SHADER_FRAG: &str = r#"uniform sampler2D tex_in;
|
||||
uniform sampler2D garbage_in;
|
||||
uniform sampler2D core_in;
|
||||
uniform int color_in;
|
||||
uniform bool garbage_in_enabled;
|
||||
uniform bool core_in_enabled;
|
||||
uniform float highlights_in;
|
||||
uniform float shadows_in;
|
||||
uniform bool mask_only_in;
|
||||
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
#define SCREEN_COLOR_GREEN 0
|
||||
#define SCREEN_COLOR_BLUE 1
|
||||
|
||||
void main(void) {
|
||||
vec4 tex_col = texture(tex_in, ove_texcoord);
|
||||
|
||||
// Unassociate RGB before calculating values
|
||||
vec4 unassoc = tex_col;
|
||||
if (unassoc.a > 0) {
|
||||
unassoc.rgb /= unassoc.a;
|
||||
}
|
||||
|
||||
// Simple keyer, generates a inverted mask (background is white, foreground black)
|
||||
float mask;
|
||||
if (color_in == SCREEN_COLOR_GREEN) {
|
||||
mask = (unassoc.g - max(unassoc.r, unassoc.b));
|
||||
} else{ // Assume SCREEN_COLOR_BLUE
|
||||
mask = (unassoc.b - max(unassoc.r, unassoc.g));
|
||||
}
|
||||
|
||||
mask = clamp(mask, 0.0, 1.0);
|
||||
|
||||
if (garbage_in_enabled) {
|
||||
// Force anything we want to remove to be 1.0
|
||||
vec4 garbage = texture(garbage_in, ove_texcoord);
|
||||
// Assumes garbage is achromatic
|
||||
mask += garbage.r;
|
||||
mask = clamp(mask, 0.0, 1.0);
|
||||
}
|
||||
|
||||
if (core_in_enabled) {
|
||||
// Force anything we want to keep to be 0.1
|
||||
vec3 core = texture(core_in, ove_texcoord).rgb;
|
||||
vec3 core_invert = 1.0 - core.rgb;
|
||||
// Assumes core is achromatic
|
||||
mask *= core_invert.r;
|
||||
mask = clamp(mask, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// Crush blacks and push whites
|
||||
mask = highlights_in * (shadows_in * mask - 1.0) + 1.0;
|
||||
mask = clamp(mask, 0.0, 1.0);
|
||||
|
||||
// Invert mask
|
||||
mask = 1.0 - mask;
|
||||
|
||||
// Multiply color by mask
|
||||
tex_col *= mask;
|
||||
|
||||
if (!mask_only_in) {
|
||||
frag_color = tex_col;
|
||||
} else {
|
||||
frag_color = vec4(vec3(mask), 1.0);
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
impl ColorDifferenceKeyNode {
|
||||
/// Fragment shader (C++ `get_shader_code()`; the request is
|
||||
/// ignored — there is a single shader).
|
||||
fn shader_frag() -> &'static str {
|
||||
SHADER_FRAG
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeBehavior for ColorDifferenceKeyNode {
|
||||
/// Human-readable name (C++ `name()`).
|
||||
fn name(&self) -> &str {
|
||||
"Color Difference Key"
|
||||
}
|
||||
|
||||
/// Stable type id (C++ `id()`).
|
||||
fn type_id(&self) -> &str {
|
||||
"org.olivevideoeditor.Olive.colordifferencekey"
|
||||
}
|
||||
|
||||
/// Categories (C++ `category()`).
|
||||
fn categories(&self) -> &[Category] {
|
||||
&[Category::Keying]
|
||||
}
|
||||
|
||||
/// Description (C++ `description()`).
|
||||
fn description(&self) -> &str {
|
||||
"A simple color key based on the distance of one color from other colors."
|
||||
}
|
||||
|
||||
/// Localized input names (C++ `retranslate()`): `tex_in` ->
|
||||
/// "Input", `garbage_in` -> "Garbage Matte", `core_in` ->
|
||||
/// "Core Matte", `color_in` -> "Key Color" (combo strings
|
||||
/// "Green"/"Blue"), `shadows_in` -> "Shadows", `highlights_in`
|
||||
/// -> "Highlights", `mask_only_in` -> "Show Mask Only".
|
||||
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): no texture on `tex_in` ->
|
||||
/// push nothing; texture present -> push a `ShaderJob` with the
|
||||
/// whole input row inserted.
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
time: oakcore_rs::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Shader code request (C++ `get_shader_code()`): returns the
|
||||
/// single fragment shader regardless of the request id.
|
||||
fn shader_code(&self, request: &str) -> Option<String> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Deep copy (C++ `copy()` via `NODE_DEFAULT_FUNCTIONS`).
|
||||
fn duplicate(&self, core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor (C++ `ColorDifferenceKeyNode::ColorDifferenceKeyNode()`):
|
||||
/// adds `tex_in`, `garbage_in`, `core_in`, `color_in`,
|
||||
/// `highlights_in`, `shadows_in`, and `mask_only_in` with the defaults
|
||||
/// and properties documented on the constants, sets the video-effect
|
||||
/// flag, and makes `tex_in` the effect input.
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.colordifferencekey`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.colordifferencekey",
|
||||
name: "Color Difference Key",
|
||||
categories: &[Category::Keying],
|
||||
create,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
// 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/>.
|
||||
|
||||
//! Corner pin distort effect (C++
|
||||
//! `src/node/src/distort/cornerpin/cornerpindistortnode.{h,cpp}`,
|
||||
//! `olive::CornerPinDistortNode`).
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
use crate::node::{Category, Gizmo, NodeBehavior, NodeCore};
|
||||
|
||||
/// Texture input id (C++ `k_texture_input`). Type: texture; flags:
|
||||
/// not-keyframable; this is the node's effect input.
|
||||
pub const TEXTURE_INPUT: &str = "tex_in";
|
||||
|
||||
/// Perspective-correct interpolation input id (C++
|
||||
/// `k_perspective_input`). Type: bool; default `true`.
|
||||
pub const PERSPECTIVE_INPUT: &str = "perspective_in";
|
||||
|
||||
/// Top-left corner offset input id (C++ `k_top_left_input`). Type:
|
||||
/// vec2; default `(0.0, 0.0)`; property `offset` is set per-frame by
|
||||
/// `update_gizmo_positions()` to the corner's pixel origin.
|
||||
pub const TOP_LEFT_INPUT: &str = "top_left_in";
|
||||
|
||||
/// Top-right corner offset input id (C++ `k_top_right_input`). Type:
|
||||
/// vec2; default `(0.0, 0.0)`; property `offset` is set per-frame to
|
||||
/// `(resolution.x, 0)`.
|
||||
pub const TOP_RIGHT_INPUT: &str = "top_right_in";
|
||||
|
||||
/// Bottom-right corner offset input id (C++ `k_bottom_right_input`).
|
||||
/// Type: vec2; default `(0.0, 0.0)`; property `offset` is set per-frame
|
||||
/// to the full resolution.
|
||||
pub const BOTTOM_RIGHT_INPUT: &str = "bottom_right_in";
|
||||
|
||||
/// Bottom-left corner offset input id (C++ `k_bottom_left_input`).
|
||||
/// Type: vec2; default `(0.0, 0.0)`; property `offset` is set per-frame
|
||||
/// to `(0, resolution.y)`.
|
||||
pub const BOTTOM_LEFT_INPUT: &str = "bottom_left_in";
|
||||
|
||||
/// Number of corner point gizmos (C++ `k_gizmo_corner_count`).
|
||||
pub const GIZMO_CORNER_COUNT: usize = 4;
|
||||
|
||||
/// Corner pin distort node. Warps the image by dragging its four
|
||||
/// corners, optionally with perspective-correct interpolation.
|
||||
pub struct CornerPinDistortNode {
|
||||
/// Corner drag handles, one per corner in top-left, top-right,
|
||||
/// bottom-right, bottom-left order (C++
|
||||
/// `PointGizmo *gizmo_resize_handle_[k_gizmo_corner_count]`; each
|
||||
/// drags both tracks of its corner input).
|
||||
gizmo_resize_handle: [Gizmo; GIZMO_CORNER_COUNT],
|
||||
/// Whole-quad outline gizmo (C++ `PolygonGizmo *gizmo_whole_rect_`;
|
||||
/// draggable but ignored by `gizmo_drag_move`, so dragging it does
|
||||
/// nothing).
|
||||
gizmo_whole_rect: Gizmo,
|
||||
}
|
||||
|
||||
/// Fragment shader (C++ loads the `:/shaders/cornerpin.frag` resource
|
||||
/// in `get_shader_code`). Text copied verbatim from
|
||||
/// `engine/shaders/cornerpin.frag`.
|
||||
const SHADER_FRAG: &str = r#"// Input texture
|
||||
uniform sampler2D ove_maintex;
|
||||
uniform sampler2D tex_in;
|
||||
uniform bool perspective_in;
|
||||
|
||||
// Input texture coordinate
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
in vec2 q;
|
||||
in vec2 b1;
|
||||
in vec2 b2;
|
||||
in vec2 b3;
|
||||
|
||||
float Wedge2D(vec2 v, vec2 w) {
|
||||
return (v.x*w.y) - (v.y*w.x);
|
||||
}
|
||||
|
||||
void main() {
|
||||
if(perspective_in){
|
||||
frag_color = texture(tex_in, ove_texcoord);
|
||||
} else {
|
||||
float A = Wedge2D(b2, b3);
|
||||
float B = Wedge2D(b3, q) - Wedge2D(b1, b2);
|
||||
float C = Wedge2D(b1, q);
|
||||
|
||||
vec2 uv;
|
||||
|
||||
// solve for v
|
||||
if (abs(A) < 0.001) {
|
||||
uv.y = -C/B;
|
||||
} else {
|
||||
float discrim = B*B - 4.0*A*C;
|
||||
uv.y = 0.5 * (-B + sqrt(discrim)) / A;
|
||||
}
|
||||
|
||||
// solve for u
|
||||
vec2 denom = b1 + uv.y * b3;
|
||||
if (abs(denom.x) > abs(denom.y)) {
|
||||
uv.x = (q.x - b2.x * uv.y) / denom.x;
|
||||
} else {
|
||||
uv.x = (q.y - b2.y * uv.y) / denom.y;
|
||||
}
|
||||
|
||||
uv.y = 1.0 - uv.y;
|
||||
|
||||
frag_color = texture(tex_in, uv);
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
/// Vertex shader (C++ loads the `:/shaders/cornerpin.vert` resource in
|
||||
/// `get_shader_code`). Text copied verbatim from
|
||||
/// `engine/shaders/cornerpin.vert`.
|
||||
const SHADER_VERT: &str = r#"uniform bool perspective_in;
|
||||
uniform vec2 top_left_in;
|
||||
uniform vec2 top_right_in;
|
||||
uniform vec2 bottom_left_in;
|
||||
uniform vec2 bottom_right_in;
|
||||
|
||||
uniform vec2 resolution_in;
|
||||
|
||||
uniform mat4 ove_mvpmat;
|
||||
|
||||
in vec4 a_position;
|
||||
in vec2 a_texcoord;
|
||||
|
||||
out vec2 ove_texcoord;
|
||||
|
||||
out vec2 q;
|
||||
out vec2 b1;
|
||||
out vec2 b2;
|
||||
out vec2 b3;
|
||||
|
||||
void main() {
|
||||
// The slider inputs only contain the amount they have changed rather than
|
||||
// their pixel locations so we adjust them here.
|
||||
vec2 t_l = top_left_in;
|
||||
vec2 t_r = top_right_in + vec2(resolution_in.x, 0.0);
|
||||
vec2 b_r = bottom_right_in + resolution_in;
|
||||
vec2 b_l = bottom_left_in + vec2(0.0, resolution_in.y);
|
||||
|
||||
gl_Position = ove_mvpmat * a_position;
|
||||
|
||||
if (perspective_in){
|
||||
// Find the center of the quadrilateral by finding where the two diagonals intersect.
|
||||
// https://www.reedbeta.com/blog/quadrilateral-interpolation-part-1/
|
||||
|
||||
// Here we calculate the gradient and constant (y = mx + c) for each diagonal.
|
||||
float m1 = (t_r.y - b_l.y)/(t_r.x - b_l.x);
|
||||
float c1 = b_l.y - m1 * b_l.x;
|
||||
float m2 = (b_r.y - t_l.y)/(b_r.x - t_l.x);
|
||||
float c2 = t_l.y - m2 * t_l.x;
|
||||
|
||||
// Find the intersection by setting the two line equations equal and rearrange.
|
||||
float mid_x = (c2 - c1) / (m1 - m2);
|
||||
float mid_y = m1 * mid_x + c1;
|
||||
|
||||
// Find the distance from each corner to our center point
|
||||
float d0 = length(vec2(mid_x - b_l.x, mid_y - b_l.y));
|
||||
float d1 = length(vec2(b_r.x - mid_x, mid_y - b_r.y));
|
||||
float d2 = length(vec2(t_r.x - mid_x, t_r.y - mid_y));
|
||||
float d3 = length(vec2(mid_x - t_l.x, t_l.y - mid_y));
|
||||
|
||||
float q = 1.0;
|
||||
|
||||
/*
|
||||
Vertex IDs (aspect ratio irrelevant):
|
||||
0_____1
|
||||
3|\ |
|
||||
| \ |
|
||||
| \ |
|
||||
| \ |
|
||||
|____\|2
|
||||
4 5
|
||||
*/
|
||||
|
||||
if (gl_VertexID == 0 || gl_VertexID == 3) {
|
||||
q = (d1+d3)/d3;
|
||||
} else if (gl_VertexID == 1) {
|
||||
q = (d0+d2)/d2;
|
||||
} else if (gl_VertexID == 2 || gl_VertexID == 5) {
|
||||
q = (d3+d1)/d1;
|
||||
} else {
|
||||
q = (d2+d0)/d0;
|
||||
}
|
||||
|
||||
gl_Position[0] *= q;
|
||||
gl_Position[1] *= q;
|
||||
gl_Position[3] = q;
|
||||
} else{
|
||||
// https://www.reedbeta.com/blog/quadrilateral-interpolation-part-2/
|
||||
vec2 pos;
|
||||
|
||||
if (gl_VertexID == 0 || gl_VertexID == 3) { // top left
|
||||
pos = t_l;
|
||||
} else if (gl_VertexID == 1) { // top right
|
||||
pos = t_r;
|
||||
} else if (gl_VertexID == 2 || gl_VertexID == 5) { // bottom right
|
||||
pos = b_r;
|
||||
} else if (gl_VertexID == 4) { // bottom left
|
||||
pos = b_l;
|
||||
}
|
||||
|
||||
q = pos - b_l;
|
||||
b1 = b_r - b_l;
|
||||
b2 = t_l - b_l;
|
||||
b3 = b_l - b_r - t_l + t_r;
|
||||
}
|
||||
|
||||
|
||||
ove_texcoord = a_texcoord;
|
||||
}
|
||||
"#;
|
||||
|
||||
impl CornerPinDistortNode {
|
||||
/// Fragment shader (C++ `get_shader_code()` frag half; the request
|
||||
/// id is ignored).
|
||||
fn shader_frag() -> &'static str {
|
||||
SHADER_FRAG
|
||||
}
|
||||
|
||||
/// Vertex shader (C++ `get_shader_code()` vert half; the request id
|
||||
/// is ignored).
|
||||
fn shader_vert() -> &'static str {
|
||||
SHADER_VERT
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeBehavior for CornerPinDistortNode {
|
||||
/// Human-readable name (C++ `name()`).
|
||||
fn name(&self) -> &str {
|
||||
"Corner Pin"
|
||||
}
|
||||
|
||||
/// Stable type id (C++ `id()`).
|
||||
fn type_id(&self) -> &str {
|
||||
"org.olivevideoeditor.Olive.cornerpin"
|
||||
}
|
||||
|
||||
/// Categories (C++ `category()`).
|
||||
fn categories(&self) -> &[Category] {
|
||||
&[Category::Distort]
|
||||
}
|
||||
|
||||
/// Description (C++ `description()`).
|
||||
fn description(&self) -> &str {
|
||||
"Distort the image by dragging the corners."
|
||||
}
|
||||
|
||||
/// Localized input names (C++ `retranslate()`): `tex_in` ->
|
||||
/// "Texture", `perspective_in` -> "Perspective", `top_left_in` ->
|
||||
/// "Top Left", `top_right_in` -> "Top Right", `bottom_right_in` ->
|
||||
/// "Bottom Right", `bottom_left_in` -> "Bottom Left".
|
||||
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): no texture -> push nothing;
|
||||
/// all four corner sliders at their `(0, 0)` default -> pass-through
|
||||
/// push of the input texture unchanged; otherwise build a shader job
|
||||
/// with `resolution_in` inserted and custom vertex coordinates: each
|
||||
/// corner offset is converted to pixels via `value_to_pixel` and then
|
||||
/// to clip space (`/ half_resolution - 1.0`) and pushed as two
|
||||
/// triangles (TL, TR, BR / TL, BL, BR).
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
time: oakcore_rs::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Shader code request (C++ `get_shader_code()`): ignores the
|
||||
/// request id and always returns the corner pin fragment and vertex
|
||||
/// shaders together.
|
||||
fn shader_code(&self, request: &str) -> Option<String> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Gizmo positions (C++ `update_gizmo_positions()`): with a texture,
|
||||
/// converts the four corner offsets to pixels (`value_to_pixel`,
|
||||
/// which adds the corner's resolution-based origin), sets each corner
|
||||
/// input's `offset` property to that origin, sets the polygon gizmo
|
||||
/// to the quad TL->TR->BR->BL->TL and each point gizmo to its
|
||||
/// corner. Also covers C++ `value_to_pixel()`.
|
||||
fn gizmo_update(&self, core: &NodeCore, row: &crate::value::NodeValueRow) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Gizmo drag (C++ `gizmo_drag_move()`): for a corner handle, drags
|
||||
/// both its X and Y track draggers by the mouse delta added to their
|
||||
/// drag-start values; dragging the whole-rect polygon gizmo is a
|
||||
/// no-op.
|
||||
fn gizmo_drag(&mut self, core: &mut NodeCore, start: bool, x: f64, y: f64, modifiers: u32) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Deep copy (C++ `copy()`).
|
||||
fn duplicate(&self, core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor (C++ `CornerPinDistortNode::CornerPinDistortNode()`):
|
||||
/// adds `tex_in`, `perspective_in` and the four corner inputs with the
|
||||
/// defaults and flags documented on the constants; creates the polygon
|
||||
/// gizmo and the four corner point gizmos (each bound to both tracks of
|
||||
/// its corner input); sets the video-effect flag and the effect input.
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.cornerpin`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.cornerpin",
|
||||
name: "Corner Pin",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
// 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/>.
|
||||
|
||||
//! Crop distort effect (C++
|
||||
//! `src/node/src/distort/crop/cropdistortnode.{h,cpp}`,
|
||||
//! `olive::CropDistortNode`).
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
use crate::node::{Category, Gizmo, NodeBehavior, NodeCore};
|
||||
|
||||
/// Texture input id (C++ `k_texture_input`). Type: texture; flags:
|
||||
/// not-keyframable; this is the node's effect input.
|
||||
pub const TEXTURE_INPUT: &str = "tex_in";
|
||||
|
||||
/// Left crop input id (C++ `k_left_input`). Type: float; default `0.0`;
|
||||
/// properties: `min = 0.0`, `max = 1.0`, `view = percentage` (created by
|
||||
/// C++ `create_crop_side_input()`).
|
||||
pub const LEFT_INPUT: &str = "left_in";
|
||||
|
||||
/// Top crop input id (C++ `k_top_input`). Type: float; default `0.0`;
|
||||
/// properties: `min = 0.0`, `max = 1.0`, `view = percentage`.
|
||||
pub const TOP_INPUT: &str = "top_in";
|
||||
|
||||
/// Right crop input id (C++ `k_right_input`). Type: float; default
|
||||
/// `0.0`; properties: `min = 0.0`, `max = 1.0`, `view = percentage`.
|
||||
pub const RIGHT_INPUT: &str = "right_in";
|
||||
|
||||
/// Bottom crop input id (C++ `k_bottom_input`). Type: float; default
|
||||
/// `0.0`; properties: `min = 0.0`, `max = 1.0`, `view = percentage`.
|
||||
pub const BOTTOM_INPUT: &str = "bottom_in";
|
||||
|
||||
/// Feather input id (C++ `k_feather_input`). Type: float; default
|
||||
/// `0.0`; properties: `min = 0.0`.
|
||||
pub const FEATHER_INPUT: &str = "feather_in";
|
||||
|
||||
/// Number of crop point gizmos (C++ `k_gizmo_scale_count` from
|
||||
/// `node.h`: top-left, top-center, top-right, bottom-left,
|
||||
/// bottom-center, bottom-right, center-left, center-right).
|
||||
pub const GIZMO_SCALE_COUNT: usize = 8;
|
||||
|
||||
/// Crop distort node. Crops the edges of an image with an optional
|
||||
/// feather.
|
||||
pub struct CropDistortNode {
|
||||
/// Edge/corner drag handles in `k_gizmo_scale_*` order (C++
|
||||
/// `PointGizmo *point_gizmo_[k_gizmo_scale_count]`; each handle drags
|
||||
/// the one or two crop inputs it touches).
|
||||
point_gizmo: [Gizmo; GIZMO_SCALE_COUNT],
|
||||
/// Crop rectangle outline gizmo (C++ `PolygonGizmo *poly_gizmo_`;
|
||||
/// drags all four crop inputs).
|
||||
poly_gizmo: Gizmo,
|
||||
/// Resolution captured by the last `update_gizmo_positions()` call,
|
||||
/// used to normalize drag deltas (C++ `Vector2D temp_resolution_`).
|
||||
temp_resolution: (f64, f64),
|
||||
}
|
||||
|
||||
/// Fragment shader (C++ loads the `:/shaders/crop.frag` resource in
|
||||
/// `get_shader_code`). Text copied verbatim from
|
||||
/// `engine/shaders/crop.frag`.
|
||||
const SHADER_FRAG: &str = r#"// Input variables
|
||||
uniform sampler2D tex_in;
|
||||
uniform float left_in;
|
||||
uniform float top_in;
|
||||
uniform float right_in;
|
||||
uniform float bottom_in;
|
||||
uniform float feather_in;
|
||||
uniform vec2 resolution_in;
|
||||
|
||||
// Input texture coordinate
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
void main() {
|
||||
float multiplier = 1.0;
|
||||
|
||||
vec2 feather_normalized = vec2(feather_in / resolution_in.x, feather_in / resolution_in.y);
|
||||
vec2 feather_normalized_half = feather_normalized * 0.5;
|
||||
|
||||
// Calculate left cropping
|
||||
float left_adjustment;
|
||||
float right_adjustment;
|
||||
float top_adjustment;
|
||||
float bottom_adjustment;
|
||||
|
||||
if (feather_in == 0.0) {
|
||||
if (ove_texcoord.x < left_in
|
||||
|| ove_texcoord.x > (1.0-right_in)
|
||||
|| ove_texcoord.y < (top_in)
|
||||
|| ove_texcoord.y > (1.0-bottom_in)) {
|
||||
multiplier = 0.0;
|
||||
}
|
||||
} else {
|
||||
float left_adjustment = clamp((ove_texcoord.x - (left_in - feather_normalized.x*(1.0-left_in))) / feather_normalized.x, 0.0, 1.0);
|
||||
multiplier *= left_adjustment;
|
||||
|
||||
float right_adjustment = 1.0-clamp((ove_texcoord.x - ((1.0-right_in) - feather_normalized.x*(right_in))) / feather_normalized.x, 0.0, 1.0);
|
||||
multiplier *= right_adjustment;
|
||||
|
||||
float top_adjustment = clamp((ove_texcoord.y - (top_in - feather_normalized.y*(1.0-top_in))) / feather_normalized.y, 0.0, 1.0);
|
||||
multiplier *= top_adjustment;
|
||||
|
||||
float bottom_adjustment = 1.0-clamp((ove_texcoord.y - ((1.0-bottom_in) - feather_normalized.y*(bottom_in))) / feather_normalized.y, 0.0, 1.0);
|
||||
multiplier *= bottom_adjustment;
|
||||
}
|
||||
|
||||
if (multiplier > 0.0) {
|
||||
vec4 color = texture(tex_in, ove_texcoord) * multiplier;
|
||||
frag_color = color;
|
||||
} else {
|
||||
frag_color = vec4(0.0);
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
impl CropDistortNode {
|
||||
/// Fragment shader (C++ `get_shader_code()`; the request id is
|
||||
/// ignored, this is the only shader).
|
||||
fn shader_frag() -> &'static str {
|
||||
SHADER_FRAG
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeBehavior for CropDistortNode {
|
||||
/// Human-readable name (C++ `name()`).
|
||||
fn name(&self) -> &str {
|
||||
"Crop"
|
||||
}
|
||||
|
||||
/// Stable type id (C++ `id()`).
|
||||
fn type_id(&self) -> &str {
|
||||
"org.olivevideoeditor.Olive.crop"
|
||||
}
|
||||
|
||||
/// Categories (C++ `category()`).
|
||||
fn categories(&self) -> &[Category] {
|
||||
&[Category::Distort]
|
||||
}
|
||||
|
||||
/// Description (C++ `description()`).
|
||||
fn description(&self) -> &str {
|
||||
"Crop the edges of an image."
|
||||
}
|
||||
|
||||
/// Localized input names (C++ `retranslate()`): `tex_in` ->
|
||||
/// "Texture", `left_in` -> "Left", `top_in` -> "Top", `right_in` ->
|
||||
/// "Right", `bottom_in` -> "Bottom", `feather_in` -> "Feather".
|
||||
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): copies the whole value row into
|
||||
/// a shader job and inserts `resolution_in` from the texture params;
|
||||
/// no texture -> push nothing; any of left/right/top/bottom != 0.0 ->
|
||||
/// shader job; all zero -> pass-through push of the input texture
|
||||
/// unchanged.
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
time: oakcore_rs::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Shader code request (C++ `get_shader_code()`): ignores the
|
||||
/// request id and always returns the crop fragment shader.
|
||||
fn shader_code(&self, request: &str) -> Option<String> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Gizmo positions (C++ `update_gizmo_positions()`): with a texture,
|
||||
/// caches the resolution in `temp_resolution`, converts the four 0..1
|
||||
/// crop values to pixel points (left/top straight, right/bottom as
|
||||
/// `1.0 - value`), places the eight edge/corner point gizmos (center
|
||||
/// handles at the midpoints) and the rectangle polygon gizmo.
|
||||
fn gizmo_update(&self, core: &NodeCore, row: &crate::value::NodeValueRow) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Gizmo drag (C++ `gizmo_drag_move()`): normalizes the mouse delta
|
||||
/// by `temp_resolution`, then for each dragger of the current gizmo
|
||||
/// adds the delta to its drag-start value with a per-input sign
|
||||
/// (left `+x`, top `+y`, right `-x`, bottom `-y`).
|
||||
fn gizmo_drag(&mut self, core: &mut NodeCore, start: bool, x: f64, y: f64, modifiers: u32) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Deep copy (C++ `copy()`).
|
||||
fn duplicate(&self, core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor (C++ `CropDistortNode::CropDistortNode()`): adds
|
||||
/// `tex_in`; adds the four crop side inputs via
|
||||
/// `create_crop_side_input()` (float, default 0.0, min 0.0, max 1.0,
|
||||
/// percentage view); adds `feather_in` (float, default 0.0, min 0.0);
|
||||
/// creates the rectangle polygon gizmo and the eight point gizmos bound
|
||||
/// to their crop inputs; sets the video-effect flag and the effect
|
||||
/// input.
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.crop`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.crop",
|
||||
name: "Crop",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// 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/>.
|
||||
|
||||
//! Despill effect (C++ `src/node/src/keying/despill/despill.{h,cpp}`,
|
||||
//! `olive::DespillNode`).
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
use crate::node::{Category, NodeBehavior, NodeCore};
|
||||
|
||||
/// Texture input id (C++ `k_texture_input`). Type: texture; flags:
|
||||
/// not-keyframable; this is the node's effect input.
|
||||
pub const TEXTURE_INPUT: &str = "tex_in";
|
||||
|
||||
/// Key color input id (C++ `k_color_input`). Type: combo; default `0`;
|
||||
/// combo strings: "Green", "Blue" (set in `retranslate()`).
|
||||
pub const COLOR_INPUT: &str = "color_in";
|
||||
|
||||
/// Despill method input id (C++ `k_method_input`). Type: combo;
|
||||
/// default `0`; combo strings: "Average", "Double Red Average",
|
||||
/// "Double Average", "Limit" (set in `retranslate()`).
|
||||
pub const METHOD_INPUT: &str = "method_in";
|
||||
|
||||
/// Preserve-luminance toggle input id (C++
|
||||
/// `k_preserve_luminance_input`; note the value ends in `_input`, not
|
||||
/// `_in`). Type: boolean; default `false`.
|
||||
pub const PRESERVE_LUMINANCE_INPUT: &str = "preserve_luminance_input";
|
||||
|
||||
/// Despill node: removes green/blue screen spill from the keyed
|
||||
/// foreground using one of several channel-averaging methods. The C++
|
||||
/// class has no own members.
|
||||
pub struct DespillNode;
|
||||
|
||||
/// Fragment shader (C++ loads the `:/shaders/despill.frag` resource in
|
||||
/// `get_shader_code`). Text copied verbatim from
|
||||
/// `engine/shaders/despill.frag`. The `luma_coeffs` uniform is not a
|
||||
/// node input — it is injected into the shader job by `value()`.
|
||||
const SHADER_FRAG: &str = r#"uniform sampler2D tex_in;
|
||||
uniform int color_in;
|
||||
uniform int method_in;
|
||||
uniform bool preserve_luminance_input;
|
||||
uniform vec3 luma_coeffs;
|
||||
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
#define AVERAGE 0
|
||||
#define DOUBLE_RED_AVERAGE 1
|
||||
#define DOUBLE_AVERAGE 2
|
||||
#define BLUE_LIMIT 3
|
||||
|
||||
void main(void) {
|
||||
vec4 original_col = texture(tex_in, ove_texcoord);
|
||||
vec4 tex_col = original_col;
|
||||
float color_average = 0.0;
|
||||
|
||||
if(color_in == 0) { // Green screen
|
||||
switch (method_in) {
|
||||
case AVERAGE:
|
||||
color_average = dot(tex_col.rb, vec2(0.5)); // (tex_col.r + tex_col.b) / 2.0
|
||||
tex_col.g = tex_col.g > color_average ? color_average: tex_col.g;
|
||||
break;
|
||||
case DOUBLE_RED_AVERAGE:
|
||||
color_average = dot(tex_col.rb, vec2(2.0, 1.0) / 3.0); // (2.0 * tex_col.r + tex_col.b) / 3.0
|
||||
tex_col.g = tex_col.g > color_average ? color_average : tex_col.g;
|
||||
break;
|
||||
case DOUBLE_AVERAGE:
|
||||
color_average = dot(tex_col.br, vec2(2.0, 1.0) / 3.0); // (2.0 * tex_col.b + tex_col.r) / 3.0
|
||||
tex_col.g = tex_col.g > color_average ? color_average : tex_col.g;
|
||||
break;
|
||||
case BLUE_LIMIT:
|
||||
tex_col.g = tex_col.g > tex_col.b ? tex_col.b : tex_col.g;
|
||||
break;
|
||||
}
|
||||
} else { // Blue screen
|
||||
switch (method_in) {
|
||||
case AVERAGE:
|
||||
color_average = dot(tex_col.rg, vec2(0.5)); // (tex_col.r + tex_col.g) / 2.0
|
||||
tex_col.b = tex_col.b > color_average ? color_average : tex_col.b;
|
||||
break;
|
||||
case DOUBLE_RED_AVERAGE:
|
||||
color_average = dot(tex_col.rg, vec2(2.0, 1.0) / 3.0); // (2.0 * tex_col.r + tex_col.g) / 3.0
|
||||
tex_col.b = tex_col.b > color_average ? color_average : tex_col.b;
|
||||
break;
|
||||
case DOUBLE_AVERAGE:
|
||||
color_average = dot(tex_col.gr, vec2(2.0, 1.0) / 3.0); // (2.0 * tex_col.g+ tex_col.r) / 3.0
|
||||
tex_col.b = tex_col.b > color_average ? color_average : tex_col.b;
|
||||
break;
|
||||
case BLUE_LIMIT:
|
||||
tex_col.b = tex_col.b > tex_col.g ? tex_col.g : tex_col.b;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (preserve_luminance_input) {
|
||||
vec4 diff = original_col - tex_col;
|
||||
float luma = dot(abs(diff.rgb), luma_coeffs);
|
||||
tex_col.rgb += vec3(luma);
|
||||
}
|
||||
|
||||
frag_color = tex_col;
|
||||
}
|
||||
"#;
|
||||
|
||||
impl DespillNode {
|
||||
/// Fragment shader (C++ `get_shader_code()`; the request is
|
||||
/// ignored — there is a single shader).
|
||||
fn shader_frag() -> &'static str {
|
||||
SHADER_FRAG
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeBehavior for DespillNode {
|
||||
/// Human-readable name (C++ `name()`).
|
||||
fn name(&self) -> &str {
|
||||
"Despill"
|
||||
}
|
||||
|
||||
/// Stable type id (C++ `id()`).
|
||||
fn type_id(&self) -> &str {
|
||||
"org.olivevideoeditor.Olive.despill"
|
||||
}
|
||||
|
||||
/// Categories (C++ `category()`).
|
||||
fn categories(&self) -> &[Category] {
|
||||
&[Category::Keying]
|
||||
}
|
||||
|
||||
/// Description (C++ `description()`).
|
||||
fn description(&self) -> &str {
|
||||
"Selection of simple despill operations"
|
||||
}
|
||||
|
||||
/// Localized input names (C++ `retranslate()`): `tex_in` ->
|
||||
/// "Input", `color_in` -> "Key Color" (combo strings
|
||||
/// "Green"/"Blue"), `method_in` -> "Method" (combo strings
|
||||
/// "Average"/"Double Red Average"/"Double Average"/"Limit"),
|
||||
/// `preserve_luminance_input` -> "Preserve Luminance".
|
||||
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): builds a `ShaderJob` from
|
||||
/// the whole input row, then inserts a `luma_coeffs` vec3 taken
|
||||
/// from the project's color manager default luma coefficients
|
||||
/// (falling back to Rec.709 `0.2126/0.7152/0.0722` when there is
|
||||
/// no project or color manager); pushes the job only when
|
||||
/// `tex_in` holds a texture.
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
time: oakcore_rs::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Shader code request (C++ `get_shader_code()`): returns the
|
||||
/// single fragment shader regardless of the request id.
|
||||
fn shader_code(&self, request: &str) -> Option<String> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Deep copy (C++ `copy()` via `NODE_DEFAULT_FUNCTIONS`).
|
||||
fn duplicate(&self, core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor (C++ `DespillNode::DespillNode()`): adds `tex_in`,
|
||||
/// `color_in`, `method_in`, and `preserve_luminance_input` with the
|
||||
/// defaults documented on the constants, sets the video-effect flag,
|
||||
/// and makes `tex_in` the effect input.
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.despill`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.despill",
|
||||
name: "Despill",
|
||||
categories: &[Category::Keying],
|
||||
create,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// 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/>.
|
||||
|
||||
//! OCIO display transform node (C++
|
||||
//! `src/node/src/color/displaytransform/displaytransform.{h,cpp}`,
|
||||
//! `olive::DisplayTransformNode`).
|
||||
//!
|
||||
//! Note: OpenColorIO itself is never linked here; it is reached through
|
||||
//! the color manager (`crate::colormanager`) and the oakrender bridge
|
||||
//! (`crate::bridge::render`), like the C++ node's
|
||||
//! `oaknode_colormanager_*` / `oakrender_color_processor_*` calls.
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
use crate::node::{Category, NodeBehavior, NodeCore};
|
||||
|
||||
use super::ociobase::OcioBase;
|
||||
|
||||
/// Display combo input id (C++ `k_display_input`). Type: combo; default
|
||||
/// `0`; flags: not-keyframable, not-connectable. Combo strings are the
|
||||
/// color manager's available displays (refreshed on config change).
|
||||
pub const DISPLAY_INPUT: &str = "display_in";
|
||||
|
||||
/// View combo input id (C++ `k_view_input`). Type: combo; default `0`;
|
||||
/// flags: not-keyframable, not-connectable. Combo strings are the views
|
||||
/// available for the selected display.
|
||||
pub const VIEW_INPUT: &str = "view_in";
|
||||
|
||||
/// Direction combo input id (C++ `k_direction_input`). Type: combo;
|
||||
/// default `0` (forward); flags: not-keyframable, not-connectable.
|
||||
/// Combo strings (set in `retranslate`): "Forward", "Inverse".
|
||||
pub const DIRECTION_INPUT: &str = "dir_in";
|
||||
|
||||
/// Display transform node. Converts an image to or from a display color
|
||||
/// space via an OCIO display/view transform. Owns no members beyond the
|
||||
/// embedded OCIO base state (C++ has no own private members).
|
||||
pub struct DisplayTransformNode {
|
||||
/// Shared OCIO base state (C++ base class `OCIOBaseNode`).
|
||||
base: OcioBase,
|
||||
}
|
||||
|
||||
impl DisplayTransformNode {
|
||||
/// Selected display name (C++ `get_display()`): the display combo
|
||||
/// index mapped through the color manager's display list; empty
|
||||
/// string when no manager is attached or the index is out of range.
|
||||
fn get_display(&self, core: &NodeCore) -> String {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Selected view name (C++ `get_view()`): the view combo index
|
||||
/// mapped through the manager's views for [`Self::get_display`];
|
||||
/// empty when unavailable.
|
||||
fn get_view(&self, core: &NodeCore) -> String {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Transform direction (C++ `get_direction()`): the direction combo
|
||||
/// value cast to `ColorProcessor::Direction` (`0` = normal/forward,
|
||||
/// `1` = inverse).
|
||||
fn get_direction(&self, core: &NodeCore) -> i64 {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Refresh the display combo strings from the manager (C++
|
||||
/// `update_displays()`); no-op without a manager.
|
||||
fn update_displays(&mut self, core: &mut NodeCore) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Refresh the view combo strings for the current display (C++
|
||||
/// `update_views()`); no-op without a manager.
|
||||
fn update_views(&mut self, core: &mut NodeCore) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// (Re)build the color processor (C++ `generate_processor()`): wraps
|
||||
/// the manager, builds a display transform for
|
||||
/// display/view/reference-space with the selected direction via
|
||||
/// `oakrender_color_processor_create_transform`, and stores it with
|
||||
/// [`OcioBase::set_processor`].
|
||||
fn generate_processor(&mut self, core: &mut NodeCore) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// OCIO config change hook (C++ `config_changed()` override):
|
||||
/// refreshes displays and views, then regenerates the processor.
|
||||
fn config_changed(&mut self, core: &mut NodeCore) {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeBehavior for DisplayTransformNode {
|
||||
/// Human-readable name (C++ `name()`).
|
||||
fn name(&self) -> &str {
|
||||
"Display Transform"
|
||||
}
|
||||
|
||||
/// Stable type id (C++ `id()`).
|
||||
fn type_id(&self) -> &str {
|
||||
"org.olivevideoeditor.Olive.displaytransform"
|
||||
}
|
||||
|
||||
/// Categories (C++ `category()`).
|
||||
fn categories(&self) -> &[Category] {
|
||||
&[Category::Color]
|
||||
}
|
||||
|
||||
/// Description (C++ `description()`).
|
||||
fn description(&self) -> &str {
|
||||
"Converts an image to or from a display color space."
|
||||
}
|
||||
|
||||
/// Localized input names (C++ `retranslate()`): `tex_in` -> "Input",
|
||||
/// `display_in` -> "Display", `view_in` -> "View", `dir_in` ->
|
||||
/// "Direction" (also sets the direction combo strings
|
||||
/// "Forward"/"Inverse").
|
||||
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Input value changed (C++ `InputValueChangedEvent`): for
|
||||
/// `display_in`, `view_in` or `dir_in` regenerates the processor;
|
||||
/// a `display_in` change additionally refreshes the view combo.
|
||||
fn input_value_changed(&mut self, core: &mut NodeCore, input: &str, element: i32) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Evaluate outputs: inherited from the C++ base
|
||||
/// (`OCIOBaseNode::value()`), i.e. delegates to
|
||||
/// [`OcioBase::value`] — color-transform job when the processor is
|
||||
/// ready, pass-through otherwise.
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
time: oakcore_rs::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Added to a graph (C++ base `AddedToGraphEvent`): captures the
|
||||
/// project's color manager and runs `config_changed()` via
|
||||
/// [`OcioBase::added_to_graph`].
|
||||
fn added_to_graph(&mut self, core: &mut NodeCore) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Removed from a graph (C++ base `RemovedFromGraphEvent`): clears
|
||||
/// the color manager pointer via [`OcioBase::removed_from_graph`].
|
||||
fn removed_from_graph(&mut self, core: &mut NodeCore) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Deep copy (C++ `copy()` via `NODE_DEFAULT_FUNCTIONS`).
|
||||
fn duplicate(&self, core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor (C++ `DisplayTransformNode::DisplayTransformNode()`):
|
||||
/// builds the base (`tex_in` texture input, effect input, video-effect
|
||||
/// flag) and adds the `display_in`/`view_in`/`dir_in` combo inputs with
|
||||
/// the defaults and flags documented on the constants.
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.displaytransform`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.displaytransform",
|
||||
name: "Display Transform",
|
||||
categories: &[Category::Color],
|
||||
create,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// 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/>.
|
||||
|
||||
//! Drop shadow filter (C++
|
||||
//! `src/node/src/filter/dropshadow/dropshadowfilter.{h,cpp}`,
|
||||
//! `olive::DropShadowFilter`).
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
use crate::node::{Category, NodeBehavior, NodeCore};
|
||||
|
||||
/// Texture input id (C++ `k_texture_input`). Type: texture; flags:
|
||||
/// not-keyframable; this is the node's effect input.
|
||||
pub const TEXTURE_INPUT: &str = "tex_in";
|
||||
|
||||
/// Shadow color input id (C++ `k_color_input`). Type: color; default
|
||||
/// black `(0.0, 0.0, 0.0)`.
|
||||
pub const COLOR_INPUT: &str = "color_in";
|
||||
|
||||
/// Shadow distance input id (C++ `k_distance_input`). Type: float;
|
||||
/// default `10.0`.
|
||||
pub const DISTANCE_INPUT: &str = "distance_in";
|
||||
|
||||
/// Shadow angle input id (C++ `k_angle_input`). Type: float; default
|
||||
/// `135.0`.
|
||||
pub const ANGLE_INPUT: &str = "angle_in";
|
||||
|
||||
/// Shadow softness input id (C++ `k_softness_input`). Type: float;
|
||||
/// default `10.0`; properties: `min = 0.0`.
|
||||
pub const SOFTNESS_INPUT: &str = "radius_in";
|
||||
|
||||
/// Shadow opacity input id (C++ `k_opacity_input`). Type: float;
|
||||
/// default `1.0`; properties: `min = 0.0`, `view = percentage`.
|
||||
pub const OPACITY_INPUT: &str = "opacity_in";
|
||||
|
||||
/// Fast/low-quality toggle input id (C++ `k_fast_input`). Type: bool;
|
||||
/// default `false`.
|
||||
pub const FAST_INPUT: &str = "fast_in";
|
||||
|
||||
/// Drop shadow filter node. Adds a colored, blurred, offset copy of the
|
||||
/// input's alpha behind the image. The C++ class declares no own member
|
||||
/// fields.
|
||||
pub struct DropShadowFilter;
|
||||
|
||||
/// Fragment shader (C++ `get_shader_code()` loads
|
||||
/// `:/shaders/dropshadow.frag` via FileFunctions for any request). Text
|
||||
/// copied verbatim from `engine/shaders/dropshadow.frag`.
|
||||
const SHADER_FRAG: &str = r#"uniform sampler2D tex_in;
|
||||
uniform vec4 color_in;
|
||||
uniform float distance_in;
|
||||
uniform float angle_in;
|
||||
uniform float radius_in;
|
||||
uniform float opacity_in;
|
||||
uniform vec2 resolution_in;
|
||||
uniform sampler2D previous_iteration_in;
|
||||
uniform bool fast_in;
|
||||
|
||||
uniform int ove_iteration;
|
||||
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
// Gaussian function uses PI
|
||||
#define M_PI 3.1415926535897932384626433832795
|
||||
|
||||
// Single gaussian formula (unused, mainly here for documentation/just in case)
|
||||
//float gaussian(float x, float sigma) {
|
||||
// return (1.0/(sigma*sqrt(2.0*M_PI)))*exp(-0.5*pow(x/sigma, 2.0));
|
||||
//}
|
||||
|
||||
// Double gaussian formula, actually used in the code below
|
||||
// Should be faster than the single gaussian above since it doesn't need sqrt()
|
||||
float gaussian2(float x, float y, float sigma) {
|
||||
return (1.0/((sigma*sigma)*2.0*M_PI))*exp(-0.5*(((x*x) + (y*y))/(sigma*sigma)));
|
||||
}
|
||||
|
||||
void main(void) {
|
||||
if (ove_iteration == 2 || radius_in == 0.0) {
|
||||
|
||||
// Merge step
|
||||
vec4 composite = texture(tex_in, ove_texcoord);
|
||||
if (composite.a < 1.0) {
|
||||
// Convert degrees to radians
|
||||
float shadow_angle = ((angle_in + 90.0)*M_PI)/180.0;
|
||||
|
||||
vec2 shadow_offset = vec2(cos(shadow_angle) * distance_in, sin(shadow_angle) * distance_in);
|
||||
shadow_offset /= resolution_in;
|
||||
shadow_offset += ove_texcoord;
|
||||
|
||||
vec4 shadow_color = texture(previous_iteration_in, shadow_offset);
|
||||
|
||||
shadow_color.rgb = color_in.rgb * shadow_color.a;
|
||||
shadow_color *= 1.0 - composite.a;
|
||||
shadow_color *= opacity_in;
|
||||
|
||||
composite += shadow_color;
|
||||
}
|
||||
frag_color = composite;
|
||||
|
||||
} else {
|
||||
// We only sample on hard pixels, so we don't accept decimal radii
|
||||
float real_radius = ceil(radius_in);
|
||||
|
||||
vec4 composite = vec4(0.0);
|
||||
|
||||
float divider, sigma;
|
||||
|
||||
if (fast_in) {
|
||||
|
||||
// Calculate the weight of each pixel based on the radius
|
||||
divider = 1.0 / real_radius;
|
||||
|
||||
} else {
|
||||
|
||||
// Using (radius = 3 * sigma) because 3 standard deviations covers 97% of the blur according to this document:
|
||||
// http://chemaguerra.com/gaussian-filter-radius/
|
||||
sigma = real_radius;
|
||||
real_radius *= 3.0;
|
||||
|
||||
// Use gaussian formula to calculate the weight of all pixels
|
||||
divider = 0.0;
|
||||
for (float i = -real_radius + 0.5; i <= real_radius; i += 2.0) {
|
||||
divider += gaussian2(i, 0.0, sigma);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for (float i = -real_radius + 0.5; i <= real_radius; i += 2.0) {
|
||||
float weight;
|
||||
|
||||
if (fast_in) {
|
||||
weight = divider;
|
||||
} else {
|
||||
weight = gaussian2(i, 0.0, sigma) / divider;
|
||||
}
|
||||
|
||||
vec2 pixel_coord = ove_texcoord;
|
||||
vec4 tex_col;
|
||||
if (ove_iteration == 0) {
|
||||
pixel_coord.x += i / resolution_in.x;
|
||||
|
||||
// Pull from main texture
|
||||
tex_col = texture(tex_in, pixel_coord);
|
||||
} else if (ove_iteration == 1) {
|
||||
pixel_coord.y += i / resolution_in.y;
|
||||
|
||||
// Pull from previous iteration
|
||||
tex_col = texture(previous_iteration_in, pixel_coord);
|
||||
}
|
||||
|
||||
composite += tex_col * weight;
|
||||
}
|
||||
|
||||
frag_color = composite;
|
||||
}
|
||||
|
||||
}
|
||||
"#;
|
||||
|
||||
impl DropShadowFilter {
|
||||
/// Fragment shader for any request (C++ `get_shader_code()` ignores
|
||||
/// the request id and always returns `dropshadow.frag`).
|
||||
fn shader_frag() -> &'static str {
|
||||
SHADER_FRAG
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeBehavior for DropShadowFilter {
|
||||
/// Human-readable name (C++ `name()`).
|
||||
fn name(&self) -> &str {
|
||||
"Drop Shadow"
|
||||
}
|
||||
|
||||
/// Stable type id (C++ `id()`).
|
||||
fn type_id(&self) -> &str {
|
||||
"org.olivevideoeditor.Olive.dropshadow"
|
||||
}
|
||||
|
||||
/// Categories (C++ `category()`).
|
||||
fn categories(&self) -> &[Category] {
|
||||
&[Category::Filter]
|
||||
}
|
||||
|
||||
/// Description (C++ `description()`).
|
||||
fn description(&self) -> &str {
|
||||
"Adds a drop shadow to an image."
|
||||
}
|
||||
|
||||
/// Localized input names (C++ `retranslate()`): `tex_in` ->
|
||||
/// "Texture", `color_in` -> "Color", `distance_in` -> "Distance",
|
||||
/// `angle_in` -> "Angle", `radius_in` -> "Softness", `opacity_in` ->
|
||||
/// "Opacity", `fast_in` -> "Faster (Lower Quality)".
|
||||
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): no texture -> push nothing;
|
||||
/// otherwise push a shader job with `resolution_in` set to the
|
||||
/// texture's virtual resolution and `previous_iteration_in` bound to
|
||||
/// the input texture; when softness is non-zero the job runs 3
|
||||
/// iterations feeding back through `previous_iteration_in`.
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
time: oakcore_rs::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Shader code request (C++ `get_shader_code()`): the request id is
|
||||
/// ignored; always returns the dropshadow fragment shader.
|
||||
fn shader_code(&self, request: &str) -> Option<String> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Deep copy (C++ `copy()`).
|
||||
fn duplicate(&self, core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor (C++ `DropShadowFilter::DropShadowFilter()`): adds
|
||||
/// `tex_in`, `color_in`, `distance_in`, `angle_in`, `radius_in`
|
||||
/// (softness), `opacity_in`, `fast_in` with the defaults and properties
|
||||
/// documented on the constants, then sets the effect input and the
|
||||
/// video-effect flag.
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_drop_shadow_filter` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.dropshadow",
|
||||
name: "Drop Shadow",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// 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/>.
|
||||
|
||||
//! Flip distort effect (C++
|
||||
//! `src/node/src/distort/flip/flipdistortnode.{h,cpp}`,
|
||||
//! `olive::FlipDistortNode`).
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
use crate::node::{Category, NodeBehavior, NodeCore};
|
||||
|
||||
/// Texture input id (C++ `k_texture_input`). Type: texture; flags:
|
||||
/// not-keyframable; this is the node's effect input.
|
||||
pub const TEXTURE_INPUT: &str = "tex_in";
|
||||
|
||||
/// Horizontal flip input id (C++ `k_horizontal_input`). Type: bool;
|
||||
/// default `false`.
|
||||
pub const HORIZONTAL_INPUT: &str = "horiz_in";
|
||||
|
||||
/// Vertical flip input id (C++ `k_vertical_input`). Type: bool;
|
||||
/// default `false`.
|
||||
pub const VERTICAL_INPUT: &str = "vert_in";
|
||||
|
||||
/// Flip distort node. Mirrors the image horizontally and/or vertically.
|
||||
/// Has no own member fields in C++ (state lives in the `Node` inputs).
|
||||
pub struct FlipDistortNode;
|
||||
|
||||
/// Fragment shader (C++ loads the `:/shaders/flip.frag` resource in
|
||||
/// `get_shader_code`). Text copied verbatim from
|
||||
/// `engine/shaders/flip.frag`.
|
||||
const SHADER_FRAG: &str = r#"uniform sampler2D tex_in;
|
||||
uniform bool horiz_in;
|
||||
uniform bool vert_in;
|
||||
|
||||
in vec2 ove_texcoord;
|
||||
out vec4 frag_color;
|
||||
|
||||
void main(void) {
|
||||
if (!horiz_in && !vert_in) {
|
||||
frag_color = texture(tex_in, ove_texcoord);
|
||||
return;
|
||||
}
|
||||
|
||||
vec2 new_coord = ove_texcoord;
|
||||
|
||||
if (horiz_in) new_coord.x = 1.0 - new_coord.x;
|
||||
if (vert_in) new_coord.y = 1.0 - new_coord.y;
|
||||
|
||||
frag_color = texture(tex_in, new_coord);
|
||||
}
|
||||
"#;
|
||||
|
||||
impl FlipDistortNode {
|
||||
/// Fragment shader (C++ `get_shader_code()`; the request id is
|
||||
/// ignored, this is the only shader).
|
||||
fn shader_frag() -> &'static str {
|
||||
SHADER_FRAG
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeBehavior for FlipDistortNode {
|
||||
/// Human-readable name (C++ `name()`).
|
||||
fn name(&self) -> &str {
|
||||
"Flip"
|
||||
}
|
||||
|
||||
/// Stable type id (C++ `id()`).
|
||||
fn type_id(&self) -> &str {
|
||||
"org.olivevideoeditor.Olive.flip"
|
||||
}
|
||||
|
||||
/// Categories (C++ `category()`).
|
||||
fn categories(&self) -> &[Category] {
|
||||
&[Category::Distort]
|
||||
}
|
||||
|
||||
/// Description (C++ `description()`).
|
||||
fn description(&self) -> &str {
|
||||
"Flips an image horizontally or vertically"
|
||||
}
|
||||
|
||||
/// Localized input names (C++ `retranslate()`): `tex_in` ->
|
||||
/// "Input", `horiz_in` -> "Horizontal", `vert_in` -> "Vertical".
|
||||
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): no texture -> push nothing;
|
||||
/// either flip flag set -> shader job over the whole value row;
|
||||
/// neither set -> pass-through push of the input texture unchanged.
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
time: oakcore_rs::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Shader code request (C++ `get_shader_code()`): ignores the
|
||||
/// request id and always returns the flip fragment shader.
|
||||
fn shader_code(&self, request: &str) -> Option<String> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Deep copy (C++ `copy()`).
|
||||
fn duplicate(&self, core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor (C++ `FlipDistortNode::FlipDistortNode()`): adds
|
||||
/// `tex_in`, `horiz_in` and `vert_in` with the defaults and flags
|
||||
/// documented on the constants, sets the video-effect flag and the
|
||||
/// effect input.
|
||||
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.flip`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.flip",
|
||||
name: "Flip",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user