diff --git a/include/codec/conform.h b/include/codec/conform.h
new file mode 100644
index 000000000..d02afed11
--- /dev/null
+++ b/include/codec/conform.h
@@ -0,0 +1,106 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_EDITOR_CODEC_CONFORM_H
+#define OAK_EDITOR_CODEC_CONFORM_H
+
+#include
+
+#include "error.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file conform.h
+ * @brief C ABI for the oakcodec audio conform manager
+ * (olive::ConformManager): pcm waveform cache files used for fast
+ * audio scrubbing.
+ *
+ * Interim state (pre-M8): actual conform work is delegated to the global
+ * task submit callback (see task.h). While no callback is registered,
+ * state queries report OAKCODEC_CONFORM_UNAVAILABLE.
+ */
+
+#define OAKCODEC_CONFORM_EXISTS 0
+#define OAKCODEC_CONFORM_GENERATING 1
+#define OAKCODEC_CONFORM_UNAVAILABLE 2
+
+/**
+ * @brief Create the ConformManager singleton (no-op when it exists).
+ */
+OAKCODEC_API int oakcodec_conform_create_instance(void);
+
+/**
+ * @brief Destroy the ConformManager singleton (no-op when absent).
+ */
+OAKCODEC_API int oakcodec_conform_destroy_instance(void);
+
+/**
+ * @brief Query the conform state of one audio stream, starting the
+ * conform when needed and possible.
+ *
+ * Addresses the source by filename/stream_index and the target audio
+ * format by sample_rate/channel_layout/sample_format
+ * (olive::core::SampleFormat::Format as int).
+ *
+ * When the conform files do not exist and a task submit callback is
+ * registered (task.h), the conform is submitted synchronously and the
+ * filesystem is re-checked; `wait` only controls whether a post-submit
+ * miss is reported as OAKCODEC_CONFORM_UNAVAILABLE (wait != 0) or
+ * OAKCODEC_CONFORM_GENERATING (wait == 0). Without a registrar the
+ * result is always OAKCODEC_CONFORM_UNAVAILABLE.
+ *
+ * @return One of OAKCODEC_CONFORM_* (non-negative), or a negative
+ * OAKCODEC_E_* code for invalid arguments.
+ */
+OAKCODEC_API int oakcodec_conform_get_state(const char *cache_path,
+ const char *source_filename, int stream_index,
+ int sample_rate, uint64_t channel_layout,
+ int sample_format, int wait);
+
+/**
+ * @brief Number of conform (pcm) files for the given stream/params โ one
+ * per channel; 0 on invalid arguments.
+ */
+OAKCODEC_API int oakcodec_conform_filename_count(const char *cache_path,
+ const char *source_filename, int stream_index,
+ int sample_rate, uint64_t channel_layout,
+ int sample_format);
+
+/**
+ * @brief The `index`-th conform filename (buf/size getter).
+ *
+ * @return Required buffer size including NUL (non-negative), or a
+ * negative OAKCODEC_E_* code (OAKCODEC_E_NOT_FOUND when index is
+ * out of range).
+ */
+OAKCODEC_API int oakcodec_conform_filename_at(const char *cache_path,
+ const char *source_filename,
+ int stream_index, int sample_rate,
+ uint64_t channel_layout, int sample_format,
+ int index, char *buf, int buf_size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //OAK_EDITOR_CODEC_CONFORM_H
diff --git a/include/codec/decoder.h b/include/codec/decoder.h
new file mode 100644
index 000000000..59e4a54c4
--- /dev/null
+++ b/include/codec/decoder.h
@@ -0,0 +1,212 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_EDITOR_CODEC_DECODER_H
+#define OAK_EDITOR_CODEC_DECODER_H
+
+#include
+
+#include "error.h"
+#include "frame.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file decoder.h
+ * @brief C ABI for oakcodec media decoders (olive::Decoder and its
+ * FFmpeg/OIIO implementations): probing, stream enumeration and
+ * CPU-frame decoding.
+ *
+ * Handles follow the neutral by-value convention documented in frame.h
+ * (and oakcommon's common/handle.h). Two usage patterns share the
+ * OakDecoder handle:
+ *
+ * - Probe: oakcodec_decoder_probe() inspects a file WITHOUT opening a
+ * decode session; the stream getters describe what was found.
+ * - Decode: oakcodec_decoder_init() + oakcodec_decoder_open() attach a
+ * decoder instance to one (filename, stream) pair; the decode
+ * functions then produce frames/audio.
+ */
+
+typedef struct OakDecoder {
+ 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; /**< OAKCODEC_ABI_VERSION. */
+} OakDecoder;
+
+/**
+ * @brief POD description of one probed video stream.
+ *
+ * duration_ts counts units of the stream's time base;
+ * time_base_num/den is seconds per time-base unit. color_primaries and
+ * color_trc carry the ISO/IEC 23001-8 code points the decoder reports
+ * (0 = unknown). interlaced is 1 when the stream is interlaced.
+ * format is an OakPixelFormat value (the decoder's native delivery
+ * format), channel_count its plane channel count.
+ */
+typedef struct oakcodec_video_stream_info {
+ int stream_index;
+ int width;
+ int height;
+ int frame_rate_num;
+ int frame_rate_den;
+ int64_t duration_ts;
+ int time_base_num;
+ int time_base_den;
+ int format;
+ int channel_count;
+ int color_primaries;
+ int color_trc;
+ int interlaced;
+} oakcodec_video_stream_info;
+
+/**
+ * @brief POD description of one probed audio stream.
+ *
+ * channel_layout is the ffmpeg-style channel mask (e.g. 0x3 = stereo).
+ */
+typedef struct oakcodec_audio_stream_info {
+ int stream_index;
+ int sample_rate;
+ uint64_t channel_layout;
+ int channel_count;
+ int64_t duration_ts;
+ int time_base_num;
+ int time_base_den;
+} oakcodec_audio_stream_info;
+
+/* ---- Probe (stateless inspection) ---------------------------------------- */
+
+/**
+ * @brief Probe a media file: decoder name plus stream inventory.
+ *
+ * Tries each available decoder implementation (FFmpeg, then OIIO) and
+ * wraps the first one that recognizes the file. The returned handle only
+ * carries probe results; it cannot decode (use init + open for that).
+ *
+ * @return Handle with reference count 1, or an empty handle (ctx == NULL)
+ * when no decoder recognizes the file (oakcodec_probe_last_error()
+ * carries the reason).
+ */
+OAKCODEC_API OakDecoder oakcodec_decoder_probe(const char *filename);
+
+/**
+ * @brief Thread-local error detail of the last failed probe on this
+ * thread (buf/size string getter convention).
+ */
+OAKCODEC_API int oakcodec_probe_last_error(char *buf, int buf_size);
+
+/** @brief Probed decoder id ("ffmpeg"/"oiio", buf/size getter). */
+OAKCODEC_API int oakcodec_decoder_probe_decoder_name(OakDecoder probe, char *buf,
+ int buf_size);
+
+OAKCODEC_API int oakcodec_decoder_probe_video_stream_count(OakDecoder probe);
+OAKCODEC_API int oakcodec_decoder_probe_audio_stream_count(OakDecoder probe);
+OAKCODEC_API int oakcodec_decoder_probe_subtitle_stream_count(OakDecoder probe);
+
+/**
+ * @brief Fill `out` with the video stream at `index` (0-based within the
+ * video stream list).
+ *
+ * @return OAKCODEC_OK, OAKCODEC_E_INVALID, or OAKCODEC_E_NOT_FOUND when
+ * index is out of range.
+ */
+OAKCODEC_API int oakcodec_decoder_probe_get_video_stream(OakDecoder probe, int index,
+ oakcodec_video_stream_info *out);
+OAKCODEC_API int oakcodec_decoder_probe_get_audio_stream(OakDecoder probe, int index,
+ oakcodec_audio_stream_info *out);
+
+/* ---- Decode session ------------------------------------------------------- */
+
+/**
+ * @brief Create a closed decoder handle (count 1).
+ */
+OAKCODEC_API OakDecoder oakcodec_decoder_init(void);
+
+/**
+ * @brief Release one reference to a decoder. No-op on NULL/empty.
+ */
+OAKCODEC_API void oakcodec_decoder_free(OakDecoder *decoder);
+
+/**
+ * @brief Open `filename`'s stream `stream_index` for decoding.
+ *
+ * The decoder implementation is chosen automatically from the probe
+ * results. Opening an already-open decoder on the same stream is a
+ * successful no-op.
+ *
+ * @return OAKCODEC_OK on success, OAKCODEC_E_NOT_FOUND when the file
+ * does not exist, OAKCODEC_E_FAILED otherwise (see
+ * oakcodec_decoder_last_error()).
+ */
+OAKCODEC_API int oakcodec_decoder_open(OakDecoder decoder, const char *filename,
+ int stream_index);
+
+/** @brief Close the current stream (safe when closed). */
+OAKCODEC_API int oakcodec_decoder_close(OakDecoder decoder);
+
+/** @brief 1 when a stream is open, 0 otherwise. */
+OAKCODEC_API int oakcodec_decoder_is_open(OakDecoder decoder);
+
+/**
+ * @brief Decode the video frame at `numerator/denominator` seconds.
+ *
+ * Before the start of the footage the first frame is returned, after the
+ * end the last frame.
+ *
+ * @return A frame handle with reference count 1 (caller releases), or an
+ * empty handle (ctx == NULL) on error/EOF โ check
+ * oakcodec_decoder_last_error().
+ */
+OAKCODEC_API OakFrame oakcodec_decoder_decode_video(OakDecoder decoder, int numerator,
+ int denominator);
+
+/**
+ * @brief Decode audio into a float buffer.
+ *
+ * Decodes the interleaved audio covering [in, out) seconds (rational
+ * pairs), resampled/laid out to `sample_rate`/`channel_layout`.
+ * `buf` must hold at least `buf_frames` frames worth of interleaved
+ * floats.
+ *
+ * @return The number of frames written (>= 0), or a negative
+ * OAKCODEC_E_* code. Conform generation is NOT triggered by this
+ * family in the current intermediate state (no task registrar);
+ * media requiring a conform yields OAKCODEC_E_STATE.
+ */
+OAKCODEC_API int oakcodec_decoder_decode_audio(OakDecoder decoder, int in_num, int in_den,
+ int out_num, int out_den, int sample_rate,
+ uint64_t channel_layout, float *buf,
+ int buf_frames);
+
+/**
+ * @brief Human-readable detail of the last error on this decoder
+ * (buf/size string getter convention).
+ */
+OAKCODEC_API int oakcodec_decoder_last_error(OakDecoder decoder, char *buf, int buf_size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //OAK_EDITOR_CODEC_DECODER_H
diff --git a/include/codec/encoder.h b/include/codec/encoder.h
new file mode 100644
index 000000000..cd5403b20
--- /dev/null
+++ b/include/codec/encoder.h
@@ -0,0 +1,192 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_EDITOR_CODEC_ENCODER_H
+#define OAK_EDITOR_CODEC_ENCODER_H
+
+#include
+
+#include "error.h"
+#include "frame.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file encoder.h
+ * @brief C ABI for oakcodec media encoders (olive::Encoder and its
+ * FFmpeg/OIIO implementations).
+ *
+ * Handles follow the neutral by-value convention documented in frame.h.
+ * The workflow is: fill an oakcodec_encoding_params POD (all fields,
+ * zeroed = disabled) -> oakcodec_encoder_init() ->
+ * oakcodec_encoder_open() -> oakcodec_encoder_write_*() ->
+ * oakcodec_encoder_flush(). Encoder-specific options
+ * (e.g. "crf" = "18") go through oakcodec_encoder_set_video_option()
+ * between init and open.
+ *
+ * Enum int fields carry the engine's own enum values
+ * (olive::ExportFormat::Format, olive::ExportCodec::Codec,
+ * OakPixelFormat, olive::VideoParams::Interlacing,
+ * olive::core::SampleFormat::Format) โ the same values
+ * oakengine/encoding.h documents.
+ */
+
+typedef struct OakEncoder {
+ 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; /**< OAKCODEC_ABI_VERSION. */
+} OakEncoder;
+
+/** @brief olive::VideoParams::Interlacing values. */
+#define OAKCODEC_INTERLACE_NONE 0
+#define OAKCODEC_INTERLACE_TOP_FIRST 1
+#define OAKCODEC_INTERLACE_BOTTOM_FIRST 2
+
+/** @brief EncodingParams::VideoScalingMethod values. */
+#define OAKCODEC_ENCODING_SCALING_FIT 0
+#define OAKCODEC_ENCODING_SCALING_STRETCH 1
+#define OAKCODEC_ENCODING_SCALING_CROP 2
+
+/**
+ * @brief Flattened encoding parameters (olive::EncodingParams).
+ *
+ * A zeroed struct describes an all-tracks-disabled configuration. The
+ * filename (and image-sequence "[#####]" template when
+ * video_is_image_sequence is set) lives in `filename`.
+ * video_time_base_* is the frame duration (frame rate flipped), matching
+ * oak_video_params' convention.
+ */
+typedef struct oakcodec_encoding_params {
+ char filename[1024];
+ int format; /**< olive::ExportFormat::Format. */
+
+ int video_enabled; /**< 1/0. */
+ int video_codec; /**< olive::ExportCodec::Codec. */
+ int video_width;
+ int video_height;
+ int video_time_base_num; /**< Frame duration numerator. */
+ int video_time_base_den;
+ int video_pixel_format; /**< OakPixelFormat (delivery format). */
+ int video_interlacing; /**< OAKCODEC_INTERLACE_*. */
+ int video_pixel_aspect_num;
+ int video_pixel_aspect_den;
+ int64_t video_bit_rate; /**< bit/s, 0 = codec default. */
+ int64_t video_min_bit_rate;
+ int64_t video_max_bit_rate;
+ int64_t video_buffer_size; /**< bytes. */
+ int video_threads; /**< 0 = auto. */
+ char video_pix_fmt[64]; /**< Encoded pixel format name ("yuv420p"). */
+ int video_is_image_sequence; /**< 1/0. */
+ int video_scaling_method; /**< OAKCODEC_ENCODING_SCALING_*. */
+
+ int audio_enabled; /**< 1/0. */
+ int audio_codec; /**< olive::ExportCodec::Codec. */
+ int audio_sample_rate;
+ uint64_t audio_channel_layout; /**< ffmpeg-style channel mask. */
+ int audio_sample_format; /**< olive::core::SampleFormat::Format. */
+ int64_t audio_bit_rate; /**< bit/s. */
+
+ int subtitles_enabled; /**< 1/0. */
+ int subtitles_codec; /**< olive::ExportCodec::Codec. */
+ int subtitles_are_sidecar; /**< 1/0. */
+ int subtitles_sidecar_format; /**< olive::ExportFormat::Format. */
+
+ /** Output OCIO colorspace name; empty = reference space (no transform). */
+ char color_transform_output[256];
+
+ int export_length_num; /**< Export length in seconds (rational). */
+ int export_length_den;
+} oakcodec_encoding_params;
+
+/**
+ * @brief Create an encoder for `params` (count 1).
+ *
+ * The implementation (FFmpeg/OIIO) is chosen from params.format and the
+ * enabled tracks. The file is NOT opened yet. Returns an empty handle
+ * (ctx == NULL) when the configuration is invalid.
+ */
+OAKCODEC_API OakEncoder oakcodec_encoder_init(const oakcodec_encoding_params *params);
+
+/** @brief Release one reference to an encoder. No-op on NULL/empty. */
+OAKCODEC_API void oakcodec_encoder_free(OakEncoder *encoder);
+
+/**
+ * @brief Set an encoder-specific video option (e.g. "crf" = "18").
+ *
+ * Only valid between init and open.
+ *
+ * @return OAKCODEC_OK, or OAKCODEC_E_STATE when already open.
+ */
+OAKCODEC_API int oakcodec_encoder_set_video_option(OakEncoder encoder, const char *key,
+ const char *value);
+
+/**
+ * @brief Open the output file and write stream headers.
+ *
+ * @return OAKCODEC_OK, OAKCODEC_E_STATE (already open), or
+ * OAKCODEC_E_FAILED (see oakcodec_encoder_last_error()).
+ */
+OAKCODEC_API int oakcodec_encoder_open(OakEncoder encoder);
+
+/**
+ * @brief Encode one video frame.
+ *
+ * The frame's parameters must match the encoding parameters (the encoder
+ * converts the delivery pixel format to the encoded one internally).
+ */
+OAKCODEC_API int oakcodec_encoder_write_video(OakEncoder encoder, OakFrame frame);
+
+/**
+ * @brief Encode interleaved float audio samples.
+ *
+ * @param samples frame_count * channel_count interleaved floats.
+ * @return OAKCODEC_OK or a negative OAKCODEC_E_* code.
+ */
+OAKCODEC_API int oakcodec_encoder_write_audio(OakEncoder encoder, const float *samples,
+ int frame_count);
+
+/**
+ * @brief Encode one subtitle entry (times in seconds).
+ */
+OAKCODEC_API int oakcodec_encoder_write_subtitle(OakEncoder encoder, const char *text,
+ double in_seconds, double out_seconds);
+
+/**
+ * @brief Flush the encoders, write the trailer and close the file.
+ *
+ * Idempotent; after a successful flush the encoder cannot be written to
+ * (write calls return OAKCODEC_E_STATE).
+ */
+OAKCODEC_API int oakcodec_encoder_flush(OakEncoder encoder);
+
+/**
+ * @brief Human-readable detail of the last error on this encoder
+ * (buf/size string getter convention).
+ */
+OAKCODEC_API int oakcodec_encoder_last_error(OakEncoder encoder, char *buf, int buf_size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //OAK_EDITOR_CODEC_ENCODER_H
diff --git a/include/codec/error.h b/include/codec/error.h
new file mode 100644
index 000000000..a2236e758
--- /dev/null
+++ b/include/codec/error.h
@@ -0,0 +1,61 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_EDITOR_CODEC_ERROR_H
+#define OAK_EDITOR_CODEC_ERROR_H
+
+/**
+ * @brief Status and error codes shared by all oakcodec C API families.
+ *
+ * Return-code convention (mirrors the other split modules):
+ * 0 (OAKCODEC_OK) on success, a negative OAKCODEC_E_* error code on
+ * failure. String getters return the required buffer size in bytes
+ * (including the terminating NUL) as a non-negative value instead.
+ */
+#define OAKCODEC_OK 0 /**< Success. */
+#define OAKCODEC_E_INVALID (-1) /**< NULL handle or invalid argument. */
+#define OAKCODEC_E_STATE (-2) /**< Call not valid in the current state. */
+#define OAKCODEC_E_FAILED (-3) /**< The underlying operation failed. */
+#define OAKCODEC_E_NOT_FOUND (-4) /**< Index out of range / entry not found. */
+#define OAKCODEC_E_NOMEM (-5) /**< Allocation failed. */
+#define OAKCODEC_E_CANCELLED (-6) /**< The operation was cancelled. */
+
+/**
+ * @brief Current ABI version stamped into every oakcodec handle.
+ *
+ * Bump whenever the handle layout or the semantics of any exported
+ * function change incompatibly. Consumers should compare a handle's
+ * abi_version field against the value they were compiled with before
+ * dereferencing ctx.
+ */
+#define OAKCODEC_ABI_VERSION 1
+
+/**
+ * @brief Export macro for the oakcodec C ABI.
+ *
+ * oakcodec is built with -fvisibility=hidden (01 ยง1 rule 5): only the
+ * oakcodec_* functions marked with this macro leave the shared library.
+ * This also keeps codec-internal C++ classes (whose olive::* names may
+ * collide with transition stubs inside other modules) from participating
+ * in cross-library weak-symbol coalescing.
+ */
+#define OAKCODEC_API __attribute__((visibility("default")))
+
+#endif //OAK_EDITOR_CODEC_ERROR_H
diff --git a/include/codec/frame.h b/include/codec/frame.h
new file mode 100644
index 000000000..74456d5a4
--- /dev/null
+++ b/include/codec/frame.h
@@ -0,0 +1,162 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_EDITOR_CODEC_FRAME_H
+#define OAK_EDITOR_CODEC_FRAME_H
+
+#include
+
+#include "common/videoparams.h"
+#include "error.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file frame.h
+ * @brief C ABI for the oakcodec frame object (olive::Frame), a CPU pixel
+ * buffer plus an OakVideoParams parameter set.
+ *
+ * Handle convention (all oakcodec families): neutral by-value handles with
+ * the same four fields as oakcommon (see oakcommon's common/handle.h):
+ *
+ * typedef struct OakFrame {
+ * void *ctx; // opaque, points to the impl
+ * void (*addref)(void *ctx); // atomic +1, owner-DLL code
+ * void (*release)(void *ctx); // atomic -1, destroys at 0
+ * uint32_t abi_version; // OAKCODEC_ABI_VERSION
+ * } OakFrame;
+ *
+ * oakcodec_frame_init*() returns a handle whose underlying object has
+ * reference count 1. Copying the struct copies the pointer, not the
+ * count: call handle.addref(handle.ctx) for every additional long-lived
+ * copy and handle.release(handle.ctx) (or oakcodec_frame_free()) when
+ * done with each copy. Functions that only use a handle take it BY
+ * VALUE; an empty handle (ctx == NULL) is reported as
+ * OAKCODEC_E_INVALID. oakcodec_frame_free() takes a pointer so it can
+ * null out the caller's ctx; NULL and ctx == NULL are no-ops.
+ */
+typedef struct OakFrame {
+ 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; /**< OAKCODEC_ABI_VERSION. */
+} OakFrame;
+
+/**
+ * @brief Create an empty frame with default (invalid) video parameters.
+ *
+ * @return Handle with reference count 1; ctx is NULL on allocation
+ * failure.
+ */
+OAKCODEC_API OakFrame oakcodec_frame_init(void);
+
+/**
+ * @brief Create a frame with a copy of the given parameter set.
+ *
+ * The params handle is addref'd internally; the caller keeps its own
+ * reference. The frame is not allocated; call oakcodec_frame_allocate().
+ *
+ * @return Handle with reference count 1; ctx is NULL on failure.
+ */
+OAKCODEC_API OakFrame oakcodec_frame_init_with_params(OakVideoParams params);
+
+/**
+ * @brief Release one reference to a frame.
+ *
+ * Convenience wrapper around handle.release(handle.ctx); nulls ctx
+ * afterwards. No-op when frame is NULL or frame->ctx is NULL.
+ */
+OAKCODEC_API void oakcodec_frame_free(OakFrame *frame);
+
+/**
+ * @brief Get a copy of the frame's parameter set.
+ *
+ * @param out Receives an addref'd OakVideoParams; the caller must release
+ * it with oakcommon_videoparams_free().
+ * @return OAKCODEC_OK, or OAKCODEC_E_INVALID for bad arguments.
+ */
+OAKCODEC_API int oakcodec_frame_get_params(OakFrame frame, OakVideoParams *out);
+
+/**
+ * @brief Replace the frame's parameter set (the handle is addref'd
+ * internally). Recomputes the line sizes; does not reallocate the
+ * buffer.
+ */
+OAKCODEC_API int oakcodec_frame_set_params(OakFrame frame, OakVideoParams params);
+
+/**
+ * @brief Allocate the pixel buffer from the current parameters.
+ *
+ * @return OAKCODEC_OK on success (including already-allocated),
+ * OAKCODEC_E_STATE when the parameters are invalid,
+ * OAKCODEC_E_INVALID for an empty handle.
+ */
+OAKCODEC_API int oakcodec_frame_allocate(OakFrame frame);
+
+/** @brief 1 when the pixel buffer is allocated, 0 otherwise. */
+OAKCODEC_API int oakcodec_frame_is_allocated(OakFrame frame);
+
+/** @brief Writable pixel buffer, or NULL when unallocated/empty. */
+OAKCODEC_API void *oakcodec_frame_data(OakFrame frame);
+
+/** @brief Const variant of oakcodec_frame_data(). */
+OAKCODEC_API const void *oakcodec_frame_const_data(OakFrame frame);
+
+/** @brief Size of the pixel buffer in bytes (0 when unallocated). */
+OAKCODEC_API int oakcodec_frame_allocated_size(OakFrame frame);
+
+/** @brief Distance between two rows in bytes (0 when params are unset). */
+OAKCODEC_API int oakcodec_frame_linesize_bytes(OakFrame frame);
+
+/** @brief Distance between two rows in pixels. */
+OAKCODEC_API int oakcodec_frame_linesize_pixels(OakFrame frame);
+
+/* Query helpers; all return 0 / OAKCOMMON_PIXEL_FORMAT_INVALID on an
+ * empty handle. */
+OAKCODEC_API int oakcodec_frame_width(OakFrame frame);
+OAKCODEC_API int oakcodec_frame_height(OakFrame frame);
+OAKCODEC_API int oakcodec_frame_format(OakFrame frame); /**< OakPixelFormat value. */
+OAKCODEC_API int oakcodec_frame_channel_count(OakFrame frame);
+
+/**
+ * @brief Frame timestamp as a rational number of seconds.
+ *
+ * @return OAKCODEC_OK, or OAKCODEC_E_INVALID for bad arguments.
+ */
+OAKCODEC_API int oakcodec_frame_get_timestamp(OakFrame frame, int *numerator,
+ int *denominator);
+OAKCODEC_API int oakcodec_frame_set_timestamp(OakFrame frame, int numerator,
+ int denominator);
+
+/**
+ * @brief Number of live oakcodec handle objects (debug/leak checking).
+ *
+ * Counts every boxed object created by oakcodec_*_init*() that has not
+ * been released yet, across all families (frame/decoder/encoder/...).
+ */
+OAKCODEC_API int oakcodec_debug_alive_count(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //OAK_EDITOR_CODEC_FRAME_H
diff --git a/include/codec/proxy.h b/include/codec/proxy.h
new file mode 100644
index 000000000..e6514ed7b
--- /dev/null
+++ b/include/codec/proxy.h
@@ -0,0 +1,140 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_EDITOR_CODEC_PROXY_H
+#define OAK_EDITOR_CODEC_PROXY_H
+
+#include
+
+#include "error.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @file proxy.h
+ * @brief C ABI for the oakcodec proxy generation singleton
+ * (olive::ProxyManager).
+ *
+ * Interim state (pre-M8): actual transcodes are delegated to the global
+ * task submit callback (see task.h). While no callback is registered,
+ * oakcodec_proxy_get_or_start() reports the proxy as missing instead of
+ * starting background work.
+ */
+
+#define OAKCODEC_PROXY_STATE_MISSING 0
+#define OAKCODEC_PROXY_STATE_GENERATING 1
+#define OAKCODEC_PROXY_STATE_READY 2
+#define OAKCODEC_PROXY_STATE_FAILED 3
+
+/**
+ * @brief POD proxy generation parameters (olive::ProxyManager::ProxyParams).
+ *
+ * divider: source resolution divider (1 = use absolute width/height,
+ * 2/4/8 = fraction of the source resolution). extension/preset are the
+ * ffmpeg output container and encoder preset (e.g. "mp4"/"veryfast").
+ */
+typedef struct oakcodec_proxy_params {
+ int width;
+ int height;
+ int divider;
+ int version;
+ int crf;
+ int include_audio; /**< 1/0. */
+ char extension[32];
+ char preset[32];
+} oakcodec_proxy_params;
+
+typedef struct oakcodec_proxy_result {
+ int state; /**< OAKCODEC_PROXY_STATE_* */
+ char filename[1024];
+} oakcodec_proxy_result;
+
+/**
+ * @brief Create the ProxyManager singleton (no-op when it exists).
+ */
+OAKCODEC_API int oakcodec_proxy_create_instance(void);
+
+/**
+ * @brief Destroy the ProxyManager singleton (no-op when absent).
+ */
+OAKCODEC_API int oakcodec_proxy_destroy_instance(void);
+
+/**
+ * @brief Compiled-in default proxy parameters (1280x720, divider 1, mp4,
+ * crf 23, "veryfast", audio included). Interim state: until the config
+ * milestone wires a real store these do not reflect user settings.
+ */
+OAKCODEC_API int oakcodec_proxy_params_default(oakcodec_proxy_params *out);
+
+/**
+ * @brief State of a proxy file on disk (OAKCODEC_PROXY_STATE_*;
+ * OAKCODEC_PROXY_STATE_MISSING for NULL/empty/absent).
+ */
+OAKCODEC_API int oakcodec_proxy_get_state(const char *proxy_filename);
+
+/** @brief Human-readable string for a proxy state (buf/size getter). */
+OAKCODEC_API int oakcodec_proxy_state_to_string(int state, char *buf, int buf_size);
+
+/** @brief Proxy directory for a project cache path (buf/size getter). */
+OAKCODEC_API int oakcodec_proxy_get_proxy_directory(const char *cache_path, char *buf,
+ int buf_size);
+
+/**
+ * @brief Deterministic proxy filename for a source stream (buf/size
+ * getter).
+ */
+OAKCODEC_API int oakcodec_proxy_get_proxy_filename(const char *cache_path,
+ const char *source_filename,
+ int stream_index,
+ const oakcodec_proxy_params *params,
+ char *buf, int buf_size);
+
+/** @brief Working (in-progress) filename of a proxy (buf/size getter). */
+OAKCODEC_API int oakcodec_proxy_get_working_filename(const char *proxy_filename,
+ char *buf, int buf_size);
+
+/**
+ * @brief Get or start generating a proxy for `source_filename`.
+ *
+ * `cache_path` is the project cache directory. On return `out->state`
+ * and `out->filename` describe the proxy. When a task submit callback is
+ * registered (task.h) and no proxy exists, generation is submitted
+ * synchronously before the state is re-derived; without a registrar the
+ * state stays OAKCODEC_PROXY_STATE_MISSING.
+ */
+OAKCODEC_API int oakcodec_proxy_get_or_start(const char *cache_path,
+ const char *source_filename, int stream_index,
+ const oakcodec_proxy_params *params,
+ oakcodec_proxy_result *out);
+
+/**
+ * @brief Locate an ffmpeg executable for proxy generation (buf/size
+ * getter; empty string when none is found).
+ */
+OAKCODEC_API int oakcodec_proxy_find_ffmpeg(const char *configured_path, char *buf,
+ int buf_size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //OAK_EDITOR_CODEC_PROXY_H
diff --git a/include/codec/task.h b/include/codec/task.h
new file mode 100644
index 000000000..aba9bcb4c
--- /dev/null
+++ b/include/codec/task.h
@@ -0,0 +1,113 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_EDITOR_CODEC_TASK_H
+#define OAK_EDITOR_CODEC_TASK_H
+
+#include
+
+#include "error.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Background task submission hook for oakcodec (interim state).
+ *
+ * The codec module occasionally needs background work (audio conforms,
+ * proxy transcodes). The task system itself is split out at milestone M8;
+ * until then oakcodec exposes a single global submit callback. A host
+ * (M8: oaktask) registers a callback with oakcodec_set_task_submit_cb();
+ * the conform/proxy managers call it whenever they need a task.
+ *
+ * While no callback is registered, managers report the work as
+ * unavailable (they never crash and never block).
+ */
+
+/**
+ * @brief Kinds of background tasks oakcodec can request.
+ */
+enum OakCodecTaskKind {
+ OAKCODEC_TASK_CONFORM = 0, /**< Audio conform to pcm cache files. */
+ OAKCODEC_TASK_PROXY = 1 /**< Video proxy transcode. */
+};
+
+/**
+ * @brief 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.
+ *
+ * Field usage by kind:
+ * - OAKCODEC_TASK_CONFORM: input_filename (source media), stream_index
+ * (audio stream), output_filename (final path of the FIRST channel's
+ * pcm file; the task derives the sibling per-channel paths and the
+ * ".working" temporary names from the deterministic naming rule),
+ * sample_rate / channel_layout / sample_format (target audio params,
+ * sample_format is olive::core::SampleFormat::Format as int).
+ * - OAKCODEC_TASK_PROXY: input_filename (source media), stream_index
+ * (video stream), output_filename (final proxy path; the task owns
+ * the ".working.mp4" temporary name and the rename on success),
+ * proxy_width / proxy_height (absolute target size, both 0 when the
+ * request is divider-based).
+ */
+typedef struct OakCodecTaskRequest {
+ int kind; /**< OakCodecTaskKind. */
+ const char *input_filename; /**< Source media filename. */
+ const char *output_filename; /**< Final destination path (see above). */
+ int stream_index; /**< Stream inside the source media. */
+ int sample_rate; /**< conform: target sample rate. */
+ uint64_t channel_layout; /**< conform: target channel layout mask. */
+ int sample_format; /**< conform: target sample format (enum as int). */
+ int proxy_width; /**< proxy: target width, 0 = unspecified/divider. */
+ int proxy_height; /**< proxy: target height, 0 = unspecified/divider. */
+} OakCodecTaskRequest;
+
+/**
+ * @brief Task submit callback.
+ *
+ * @return 0 (OAKCODEC_OK) if the task was accepted - either completed
+ * synchronously or queued; a negative OAKCODEC_E_* code if the request
+ * was rejected.
+ */
+typedef int (*oakcodec_task_submit_fn)(const OakCodecTaskRequest *req,
+ void *userdata);
+
+/**
+ * @brief Registers (or replaces) the global task submit callback.
+ *
+ * Thread-safe. Pass cb == NULL to unregister. Interim state (pre-M8):
+ * nobody registers and all task-dependent work reports unavailable.
+ */
+OAKCODEC_API void oakcodec_set_task_submit_cb(oakcodec_task_submit_fn cb, void *userdata);
+
+/**
+ * @brief Returns 1 if a submit callback is currently registered, else 0.
+ *
+ * Thread-safe.
+ */
+OAKCODEC_API int oakcodec_task_submit_is_registered(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //OAK_EDITOR_CODEC_TASK_H
diff --git a/include/common/colortransform.h b/include/common/colortransform.h
index 3e7a1e7f9..fa3a80be5 100644
--- a/include/common/colortransform.h
+++ b/include/common/colortransform.h
@@ -22,24 +22,39 @@
#define OAK_EDITOR_COLORTRANSFORM_H
#include "common/error.h"
+#include "common/handle.h"
#ifdef __cplusplus
+namespace olive
+{
+class ColorTransform;
+}
extern "C" {
#endif
/**
- * @brief Opaque handle to a color transform description
+ * @brief Neutral by-value handle to a color transform description
* (olive::ColorTransform).
+ *
+ * Ownership/count semantics follow the convention in common/handle.h:
+ * init functions return a handle whose object has reference count 1,
+ * addref(ctx)/release(ctx) adjust it atomically, and release destroys
+ * the object at zero. abi_version is always OAKCOMMON_ABI_VERSION.
*/
-typedef struct OakCommonColorTransform OakCommonColorTransform;
+typedef struct OakColorTransform {
+ 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; /**< OAKCOMMON_ABI_VERSION. */
+} OakColorTransform;
/**
* @brief Create a plain output-colorspace transform.
*
* @param output Output colorspace name. Must not be NULL.
- * @return Transform handle, or NULL on failure.
+ * @return Handle with reference count 1; ctx is NULL on failure.
*/
-OakCommonColorTransform *oakcommon_colortransform_init_output(
+OakColorTransform oakcommon_colortransform_init_output(
const char *output);
/**
@@ -47,15 +62,47 @@ OakCommonColorTransform *oakcommon_colortransform_init_output(
*
* All three strings must not be NULL.
*
- * @return Transform handle, or NULL on failure.
+ * @return Handle with reference count 1; ctx is NULL on failure.
*/
-OakCommonColorTransform *oakcommon_colortransform_init_display(
+OakColorTransform oakcommon_colortransform_init_display(
const char *display, const char *view, const char *look);
+#ifdef __cplusplus
/**
- * @brief Destroy a transform. No-op on NULL.
+ * @brief Copy a native olive::ColorTransform into a new handle.
+ *
+ * The source object is deep-copied; the handle does not keep any
+ * reference to @p src, which may be destroyed immediately afterwards.
+ * Only visible to C++ consumers.
+ *
+ * @return Handle with reference count 1; ctx is NULL if src is NULL or
+ * on allocation failure.
*/
-void oakcommon_colortransform_free(OakCommonColorTransform *transform);
+OakColorTransform oakcommon_colortransform_init_from_native(
+ const olive::ColorTransform *src);
+
+/**
+ * @brief Borrow the native object behind a handle.
+ *
+ * The returned pointer is borrowed: it stays valid while the caller
+ * holds a reference to the handle (i.e. until the matching release).
+ * Only visible to C++ consumers.
+ *
+ * @return Borrowed pointer, or NULL if transform is NULL or
+ * transform->ctx is NULL.
+ */
+const olive::ColorTransform *oakcommon_colortransform_get_native(
+ OakColorTransform transform);
+#endif
+
+/**
+ * @brief Release one reference to a transform.
+ *
+ * Convenience wrapper around handle.release(handle.ctx): decrements the
+ * atomic reference count and destroys the object when it reaches zero.
+ * No-op when transform is NULL or transform->ctx is NULL.
+ */
+void oakcommon_colortransform_free(OakColorTransform *transform);
/**
* @brief Query whether this is a display/view/look transform.
@@ -63,7 +110,7 @@ void oakcommon_colortransform_free(OakCommonColorTransform *transform);
* @param is_display Receives the result. Must not be NULL.
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_colortransform_is_display(OakCommonColorTransform *transform,
+int oakcommon_colortransform_is_display(OakColorTransform transform,
int *is_display);
/**
@@ -72,7 +119,7 @@ int oakcommon_colortransform_is_display(OakCommonColorTransform *transform,
* @return Required buffer size in bytes including the terminating NUL
* (non-negative), or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_colortransform_get_display(OakCommonColorTransform *transform,
+int oakcommon_colortransform_get_display(OakColorTransform transform,
char *buf, int buf_size);
/**
@@ -81,7 +128,7 @@ int oakcommon_colortransform_get_display(OakCommonColorTransform *transform,
* @return Required buffer size in bytes including the terminating NUL
* (non-negative), or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_colortransform_get_output(OakCommonColorTransform *transform,
+int oakcommon_colortransform_get_output(OakColorTransform transform,
char *buf, int buf_size);
/**
@@ -90,7 +137,7 @@ int oakcommon_colortransform_get_output(OakCommonColorTransform *transform,
* @return Required buffer size in bytes including the terminating NUL
* (non-negative), or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_colortransform_get_view(OakCommonColorTransform *transform,
+int oakcommon_colortransform_get_view(OakColorTransform transform,
char *buf, int buf_size);
/**
@@ -99,7 +146,7 @@ int oakcommon_colortransform_get_view(OakCommonColorTransform *transform,
* @return Required buffer size in bytes including the terminating NUL
* (non-negative), or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_colortransform_get_look(OakCommonColorTransform *transform,
+int oakcommon_colortransform_get_look(OakColorTransform transform,
char *buf, int buf_size);
#ifdef __cplusplus
diff --git a/include/common/commandlineparser.h b/include/common/commandlineparser.h
index 68389cec4..4d67078be 100644
--- a/include/common/commandlineparser.h
+++ b/include/common/commandlineparser.h
@@ -26,56 +26,82 @@
#endif
#include "common/error.h"
+#include "common/handle.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
- * @brief Opaque handle to a command-line parser instance.
+ * @brief Neutral by-value handle to a command-line parser instance.
+ *
+ * Ownership/count semantics follow the convention in common/handle.h:
+ * init returns a handle whose object has reference count 1,
+ * addref(ctx)/release(ctx) adjust it atomically, and release destroys
+ * the object at zero. abi_version is always OAKCOMMON_ABI_VERSION.
*/
-typedef struct OakCommonCommandLineParser OakCommonCommandLineParser;
+typedef struct OakCommandLineParser {
+ 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; /**< OAKCOMMON_ABI_VERSION. */
+} OakCommandLineParser;
/**
- * @brief Opaque handle to a registered command-line option.
+ * @brief Neutral by-value handle to a registered command-line option.
*
- * The handle wrapper is freed with oakcommon_commandlineoption_free();
- * the underlying option is owned by the parser and stays valid until
- * the parser is freed.
+ * The handle is released with oakcommon_commandlineoption_free() (or
+ * handle.release(handle.ctx)); the underlying option is owned by the
+ * parser and stays valid until the parser is destroyed. abi_version is
+ * always OAKCOMMON_ABI_VERSION.
*/
-typedef struct OakCommonCommandLineOption OakCommonCommandLineOption;
+typedef struct OakCommandLineOption {
+ 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; /**< OAKCOMMON_ABI_VERSION. */
+} OakCommandLineOption;
/**
- * @brief Opaque handle to a registered positional argument.
+ * @brief Neutral by-value handle to a registered positional argument.
*
- * The handle wrapper is freed with
- * oakcommon_commandlinepositionalargument_free(); the underlying argument
- * is owned by the parser and stays valid until the parser is freed.
+ * The handle is released with
+ * oakcommon_commandlinepositionalargument_free() (or
+ * handle.release(handle.ctx)); the underlying argument is owned by the
+ * parser and stays valid until the parser is destroyed. abi_version is
+ * always OAKCOMMON_ABI_VERSION.
*/
-typedef struct OakCommonCommandLinePositionalArgument
- OakCommonCommandLinePositionalArgument;
+typedef struct OakCommandLinePositionalArgument {
+ 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; /**< OAKCOMMON_ABI_VERSION. */
+} OakCommandLinePositionalArgument;
/**
* @brief Create a command-line parser.
*
- * @return Parser handle, or NULL on allocation failure.
+ * @return Handle with reference count 1; ctx is NULL on allocation
+ * failure.
*/
-OakCommonCommandLineParser *oakcommon_commandlineparser_init(void);
+OakCommandLineParser oakcommon_commandlineparser_init(void);
/**
- * @brief Destroy a command-line parser.
+ * @brief Release one reference to a command-line parser.
*
- * Destroys all option and positional-argument handles created from it.
- * NULL is a no-op.
+ * Convenience wrapper around handle.release(handle.ctx): decrements the
+ * atomic reference count and destroys the parser (invalidating all
+ * option and positional-argument handles created from it) when the
+ * count reaches zero. No-op when parser is NULL or parser->ctx is NULL.
*/
-void oakcommon_commandlineparser_free(OakCommonCommandLineParser *parser);
+void oakcommon_commandlineparser_free(OakCommandLineParser *parser);
/**
* @brief Set the application name/version shown by print_help.
*
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_commandlineparser_set_app_info(OakCommonCommandLineParser *parser,
+int oakcommon_commandlineparser_set_app_info(OakCommandLineParser parser,
const char *name,
const char *version);
@@ -88,26 +114,28 @@ int oakcommon_commandlineparser_set_app_info(OakCommonCommandLineParser *parser,
* @param takes_arg Non-zero if the option consumes the following argument.
* @param arg_placeholder Placeholder shown in help, may be NULL.
* @param hidden Non-zero to omit from help output.
- * @param out_option Receives the option handle. May be NULL if unused.
+ * @param out_option Receives the option handle (reference count 1).
+ * May be NULL if unused.
*
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
int oakcommon_commandlineparser_add_option(
- OakCommonCommandLineParser *parser, const char *const *names, int name_count,
+ OakCommandLineParser parser, const char *const *names, int name_count,
const char *description, int takes_arg, const char *arg_placeholder,
- int hidden, OakCommonCommandLineOption **out_option);
+ int hidden, OakCommandLineOption *out_option);
/**
* @brief Register a positional argument.
*
- * @param out_argument Receives the argument handle. May be NULL if unused.
+ * @param out_argument Receives the argument handle (reference count 1).
+ * May be NULL if unused.
*
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
int oakcommon_commandlineparser_add_positional_argument(
- OakCommonCommandLineParser *parser, const char *name,
+ OakCommandLineParser parser, const char *name,
const char *description, int required,
- OakCommonCommandLinePositionalArgument **out_argument);
+ OakCommandLinePositionalArgument *out_argument);
/**
* @brief Parse an argv-style argument list.
@@ -116,7 +144,7 @@ int oakcommon_commandlineparser_add_positional_argument(
*
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_commandlineparser_process(OakCommonCommandLineParser *parser,
+int oakcommon_commandlineparser_process(OakCommandLineParser parser,
const char *const *argv, int argc);
/**
@@ -124,7 +152,7 @@ int oakcommon_commandlineparser_process(OakCommonCommandLineParser *parser,
*
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_commandlineparser_print_help(OakCommonCommandLineParser *parser,
+int oakcommon_commandlineparser_print_help(OakCommandLineParser parser,
const char *filename);
/**
@@ -134,15 +162,17 @@ int oakcommon_commandlineparser_print_help(OakCommonCommandLineParser *parser,
*
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_commandlineoption_is_set(OakCommonCommandLineOption *option,
+int oakcommon_commandlineoption_is_set(OakCommandLineOption option,
bool *is_set);
/**
- * @brief Free an option handle wrapper.
+ * @brief Release one reference to an option handle.
*
- * Does not unregister the option from the parser. NULL is a no-op.
+ * Convenience wrapper around handle.release(handle.ctx). Does not
+ * unregister the option from the parser. No-op when option is NULL or
+ * option->ctx is NULL.
*/
-void oakcommon_commandlineoption_free(OakCommonCommandLineOption *option);
+void oakcommon_commandlineoption_free(OakCommandLineOption *option);
/**
* @brief Get an option's argument value (two-stage string getter).
@@ -150,7 +180,7 @@ void oakcommon_commandlineoption_free(OakCommonCommandLineOption *option);
* @return Required buffer size in bytes including the terminating NUL
* (non-negative), or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_commandlineoption_get_setting(OakCommonCommandLineOption *option,
+int oakcommon_commandlineoption_get_setting(OakCommandLineOption option,
char *buf, int buf_size);
/**
@@ -158,7 +188,7 @@ int oakcommon_commandlineoption_get_setting(OakCommonCommandLineOption *option,
*
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_commandlineoption_set_setting(OakCommonCommandLineOption *option,
+int oakcommon_commandlineoption_set_setting(OakCommandLineOption option,
const char *value);
/**
@@ -168,7 +198,7 @@ int oakcommon_commandlineoption_set_setting(OakCommonCommandLineOption *option,
* (non-negative), or a negative OAKCOMMON_E_* error code.
*/
int oakcommon_commandlinepositionalargument_get_setting(
- OakCommonCommandLinePositionalArgument *argument, char *buf, int buf_size);
+ OakCommandLinePositionalArgument argument, char *buf, int buf_size);
/**
* @brief Set a positional argument's value.
@@ -176,15 +206,17 @@ int oakcommon_commandlinepositionalargument_get_setting(
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
int oakcommon_commandlinepositionalargument_set_setting(
- OakCommonCommandLinePositionalArgument *argument, const char *value);
+ OakCommandLinePositionalArgument argument, const char *value);
/**
- * @brief Free a positional argument handle wrapper.
+ * @brief Release one reference to a positional argument handle.
*
- * Does not unregister the argument from the parser. NULL is a no-op.
+ * Convenience wrapper around handle.release(handle.ctx). Does not
+ * unregister the argument from the parser. No-op when argument is NULL
+ * or argument->ctx is NULL.
*/
void oakcommon_commandlinepositionalargument_free(
- OakCommonCommandLinePositionalArgument *argument);
+ OakCommandLinePositionalArgument *argument);
#ifdef __cplusplus
}
diff --git a/include/common/current.h b/include/common/current.h
index 5289dfe05..867fafd54 100644
--- a/include/common/current.h
+++ b/include/common/current.h
@@ -22,12 +22,27 @@
#define OAK_EDITOR_CURRENT_H
#include "common/error.h"
+#include "common/handle.h"
#ifdef __cplusplus
extern "C" {
#endif
-typedef struct OakCommonCurrent OakCommonCurrent;
+/**
+ * @brief Neutral by-value handle to the process-wide Current singleton.
+ *
+ * Uses the standard handle layout (see common/handle.h) but with
+ * singleton semantics: ctx points to a statically allocated object that
+ * lives until process exit, so addref() and release() are intentionally
+ * no-ops and never destroy anything. abi_version is always
+ * OAKCOMMON_ABI_VERSION.
+ */
+typedef struct OakCurrent {
+ void *ctx; /**< Opaque pointer to the singleton object. */
+ void (*addref)(void *ctx); /**< No-op (singleton). */
+ void (*release)(void *ctx); /**< No-op (singleton). */
+ uint32_t abi_version; /**< OAKCOMMON_ABI_VERSION. */
+} OakCurrent;
/**
* @brief Destructor callback for objects handed to Current slots.
@@ -35,23 +50,24 @@ typedef struct OakCommonCurrent OakCommonCurrent;
* Called when the slot is overwritten or cleared. May be NULL if the
* caller keeps ownership of the object.
*/
-typedef void (*OakCommonDestroyFn)(void *obj);
+typedef void (*OakDestroyFn)(void *obj);
/**
* @brief Return a handle to the process-wide Current singleton.
*
- * The returned handle is borrowed: it is valid for the lifetime of the
- * process and must not be freed with oakcommon_current_free() more
- * than out of symmetry (free is a no-op for the singleton).
+ * The returned handle is borrowed: its ctx is valid for the lifetime of
+ * the process. addref/release on it are no-ops; calling
+ * oakcommon_current_free() is allowed for symmetry and does nothing.
*/
-OakCommonCurrent *oakcommon_current_instance(void);
+OakCurrent oakcommon_current_instance(void);
/**
* @brief Release a Current handle.
*
- * No-op: the underlying object is a singleton. Safe to call with NULL.
+ * No-op: the underlying object is a singleton whose release() never
+ * destroys anything. Safe to call with NULL or a ctx == NULL handle.
*/
-void oakcommon_current_free(OakCommonCurrent *self);
+void oakcommon_current_free(OakCurrent *self);
/**
* @brief Store a pointer in a Current slot, taking over destruction.
@@ -63,16 +79,16 @@ void oakcommon_current_free(OakCommonCurrent *self);
* @param obj Opaque pointer to the external object (e.g. a
* VideoParams), or NULL to clear.
* @param destroy Optional destructor invoked when the slot is replaced.
- * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if self is NULL.
+ * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if self.ctx is NULL.
*/
-int oakcommon_current_set_video_params(OakCommonCurrent *self, void *obj,
- OakCommonDestroyFn destroy);
-int oakcommon_current_set_audio_params(OakCommonCurrent *self, void *obj,
- OakCommonDestroyFn destroy);
-int oakcommon_current_set_plugin_host(OakCommonCurrent *self, void *obj,
- OakCommonDestroyFn destroy);
-int oakcommon_current_set_plugin_cache(OakCommonCurrent *self, void *obj,
- OakCommonDestroyFn destroy);
+int oakcommon_current_set_video_params(OakCurrent self, void *obj,
+ OakDestroyFn destroy);
+int oakcommon_current_set_audio_params(OakCurrent self, void *obj,
+ OakDestroyFn destroy);
+int oakcommon_current_set_plugin_host(OakCurrent self, void *obj,
+ OakDestroyFn destroy);
+int oakcommon_current_set_plugin_cache(OakCurrent self, void *obj,
+ OakDestroyFn destroy);
/**
* @brief Fetch the raw pointer currently stored in a slot.
@@ -82,23 +98,23 @@ int oakcommon_current_set_plugin_cache(OakCommonCurrent *self, void *obj,
*
* @param self Handle from oakcommon_current_instance().
* @param out Receives the stored pointer.
- * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if self or out
- * is NULL.
+ * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if self.ctx or
+ * out is NULL.
*/
-int oakcommon_current_get_video_params(OakCommonCurrent *self, void **out);
-int oakcommon_current_get_audio_params(OakCommonCurrent *self, void **out);
-int oakcommon_current_get_plugin_host(OakCommonCurrent *self, void **out);
-int oakcommon_current_get_plugin_cache(OakCommonCurrent *self, void **out);
+int oakcommon_current_get_video_params(OakCurrent self, void **out);
+int oakcommon_current_get_audio_params(OakCurrent self, void **out);
+int oakcommon_current_get_plugin_host(OakCurrent self, void **out);
+int oakcommon_current_get_plugin_cache(OakCurrent self, void **out);
/**
* @brief Query whether the session is interactive.
*
* @param self Handle from oakcommon_current_instance().
* @param out Receives 1 for interactive, 0 otherwise.
- * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if self or out
- * is NULL.
+ * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if self.ctx or
+ * out is NULL.
*/
-int oakcommon_current_is_interactive(OakCommonCurrent *self, int *out);
+int oakcommon_current_is_interactive(OakCurrent self, int *out);
#ifdef __cplusplus
}
diff --git a/include/common/debug.h b/include/common/debug.h
index 4415b3dbe..6b89470a2 100644
--- a/include/common/debug.h
+++ b/include/common/debug.h
@@ -32,7 +32,7 @@ extern "C" {
*
* Mirrors olive::DebugLevel in src/common/src/debug.h.
*/
-enum OakCommonDebugLevel {
+enum OakDebugLevel {
OAKCOMMON_DEBUG_DEBUG = 0, /**< Verbose debug message. */
OAKCOMMON_DEBUG_INFO = 1, /**< Informational message. */
OAKCOMMON_DEBUG_WARNING = 2, /**< Warning message. */
@@ -46,7 +46,7 @@ enum OakCommonDebugLevel {
* De-Qt replacement for the old Qt message handler. The line is
* flushed immediately.
*
- * @param level One of OakCommonDebugLevel; out-of-range values print
+ * @param level One of OakDebugLevel; out-of-range values print
* as "UNKNOWN".
* @param msg NUL-terminated message; NULL is treated as an empty
* string.
@@ -60,7 +60,7 @@ int oakcommon_debug_log(int level, const char *msg);
* Two-segment string getter: if buf is NULL or buf_size is too small,
* nothing is written.
*
- * @param level One of OakCommonDebugLevel.
+ * @param level One of OakDebugLevel.
* @param buf Destination buffer, may be NULL to query the size.
* @param buf_size Size of buf in bytes.
* @return Required buffer size in bytes including the terminating NUL
@@ -68,6 +68,44 @@ int oakcommon_debug_log(int level, const char *msg);
*/
int oakcommon_debug_level_name(int level, char *buf, int buf_size);
+/**
+ * @brief printf-style filtered log, replacing qDebug()/qInfo()/
+ * qWarning()/qCritical() call sites.
+ *
+ * The message is formatted with vsnprintf into a dynamically sized
+ * buffer (arbitrary length, no truncation, no fixed stack buffer) and
+ * emitted as "[LEVEL] message\n" unless @p level is below the current
+ * filter level (see oakcommon_log_set_level()).
+ *
+ * @param level One of OakDebugLevel; out-of-range values print
+ * as "UNKNOWN" and are never filtered out below FATAL.
+ * @param fmt printf-style format string. Must not be NULL.
+ * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if fmt is NULL,
+ * OAKCOMMON_E_FAILED if formatting failed.
+ */
+int oakcommon_log(int level, const char *fmt, ...);
+
+/**
+ * @brief Set the minimum level emitted by oakcommon_log().
+ *
+ * Messages with a lower level are dropped. The default is
+ * OAKCOMMON_DEBUG_INFO.
+ *
+ * @param level One of OakDebugLevel.
+ * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if level is
+ * outside the OakDebugLevel range.
+ */
+int oakcommon_log_set_level(int level);
+
+/**
+ * @brief Query the current minimum level emitted by oakcommon_log().
+ *
+ * @param out_level Receives one of OakDebugLevel. Must not be NULL.
+ * @return OAKCOMMON_OK on success, OAKCOMMON_E_INVALID if out_level is
+ * NULL.
+ */
+int oakcommon_log_get_level(int *out_level);
+
#ifdef __cplusplus
}
#endif
diff --git a/include/common/dropworkflowbehavior.h b/include/common/dropworkflowbehavior.h
index c94d02935..1fe7bc0b6 100644
--- a/include/common/dropworkflowbehavior.h
+++ b/include/common/dropworkflowbehavior.h
@@ -35,7 +35,7 @@ extern "C" {
* src/common/src/dropworkflowbehavior.h; enumerator order and values
* must stay identical because the config layer persists them as ints.
*/
-enum OakCommonDropWorkflowBehavior {
+enum OakDropWorkflowBehavior {
OAKCOMMON_DWS_ASK = 0, /**< Ask the user every time. */
OAKCOMMON_DWS_AUTO = 1, /**< Automatically create a sequence. */
OAKCOMMON_DWS_MANUAL = 2, /**< Never create; import manually. */
@@ -43,7 +43,7 @@ enum OakCommonDropWorkflowBehavior {
};
/**
- * @brief Check whether value is a valid OakCommonDropWorkflowBehavior.
+ * @brief Check whether value is a valid OakDropWorkflowBehavior.
*
* @param value Integer behavior value (e.g. read from config).
* @return 1 if valid, 0 otherwise (this is a predicate, not a status
@@ -57,7 +57,7 @@ int oakcommon_drop_workflow_behavior_is_valid(int value);
* Two-segment string getter: if buf is NULL or buf_size is too small,
* nothing is written. Invalid values yield "UNKNOWN".
*
- * @param value One of OakCommonDropWorkflowBehavior.
+ * @param value One of OakDropWorkflowBehavior.
* @param buf Destination buffer, may be NULL to query the size.
* @param buf_size Size of buf in bytes.
* @return Required buffer size in bytes including the terminating NUL
diff --git a/include/common/error.h b/include/common/error.h
index cd8ad49c7..e5d4f69fe 100644
--- a/include/common/error.h
+++ b/include/common/error.h
@@ -30,7 +30,7 @@
* (including the terminating NUL) as a non-negative value instead.
*/
#define OAKCOMMON_OK 0 /**< Success. */
-#define OAKCOMMON_E_INVALID (-1) /**< NULL handle or invalid argument. */
+#define OAKCOMMON_E_INVALID (-1) /**< Empty handle (ctx == NULL) or invalid argument. */
#define OAKCOMMON_E_STATE (-2) /**< Call not valid in the current state. */
#define OAKCOMMON_E_FAILED (-3) /**< The underlying operation failed. */
#define OAKCOMMON_E_NOT_FOUND (-4) /**< Index out of range / entry not found. */
diff --git a/include/common/filefunctions.h b/include/common/filefunctions.h
index a627e03b7..ff26a1ede 100644
--- a/include/common/filefunctions.h
+++ b/include/common/filefunctions.h
@@ -22,30 +22,44 @@
#define OAK_EDITOR_FILEFUNCTIONS_H
#include "common/error.h"
+#include "common/handle.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
- * @brief Opaque handle for the filefunctions family
+ * @brief Neutral by-value handle for the filefunctions family
*
* File functions are stateless; the handle only exists to keep the C API
- * shape uniform across oakcommon families.
+ * shape uniform across oakcommon families. Ownership/count semantics
+ * follow the convention in common/handle.h: init returns a handle whose
+ * (empty) object has reference count 1, addref(ctx)/release(ctx) adjust
+ * it atomically, and release destroys it at zero. abi_version is always
+ * OAKCOMMON_ABI_VERSION.
*/
-typedef struct OakCommonFileFunctions OakCommonFileFunctions;
+typedef struct OakFileFunctions {
+ 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; /**< OAKCOMMON_ABI_VERSION. */
+} OakFileFunctions;
/**
* @brief Creates a filefunctions handle
*
- * @return A new handle, or NULL on failure.
+ * @return Handle with reference count 1; ctx is NULL on failure.
*/
-OakCommonFileFunctions *oakcommon_filefunctions_init(void);
+OakFileFunctions oakcommon_filefunctions_init(void);
/**
- * @brief Destroys a filefunctions handle (NULL is a no-op)
+ * @brief Releases one reference to a filefunctions handle
+ *
+ * Convenience wrapper around handle.release(handle.ctx): decrements the
+ * atomic reference count and destroys the object when it reaches zero.
+ * No-op when self is NULL or self->ctx is NULL.
*/
-void oakcommon_filefunctions_free(OakCommonFileFunctions *self);
+void oakcommon_filefunctions_free(OakFileFunctions *self);
/**
* @brief Returns a deterministic identifier string for a file
@@ -55,20 +69,20 @@ void oakcommon_filefunctions_free(OakCommonFileFunctions *self);
* the file does not exist.
*/
int oakcommon_filefunctions_get_unique_file_identifier(
- OakCommonFileFunctions *self, const char *filename, char *buf,
+ OakFileFunctions self, const char *filename, char *buf,
int buf_size);
int oakcommon_filefunctions_get_configuration_location(
- OakCommonFileFunctions *self, char *buf, int buf_size);
+ OakFileFunctions self, char *buf, int buf_size);
int oakcommon_filefunctions_get_application_path(
- OakCommonFileFunctions *self, char *buf, int buf_size);
+ OakFileFunctions self, char *buf, int buf_size);
int oakcommon_filefunctions_get_temp_file_path(
- OakCommonFileFunctions *self, char *buf, int buf_size);
+ OakFileFunctions self, char *buf, int buf_size);
int oakcommon_filefunctions_get_auto_recovery_root(
- OakCommonFileFunctions *self, char *buf, int buf_size);
+ OakFileFunctions self, char *buf, int buf_size);
/**
* @brief Checks whether `source` can be copied to `dest` without
@@ -77,13 +91,13 @@ int oakcommon_filefunctions_get_auto_recovery_root(
* @param out Receives 1 (safe) or 0 (would overwrite).
*/
int oakcommon_filefunctions_can_copy_directory_without_overwriting(
- OakCommonFileFunctions *self, const char *source, const char *dest,
+ OakFileFunctions self, const char *source, const char *dest,
int *out);
/**
* @brief Recursively copies a directory
*/
-int oakcommon_filefunctions_copy_directory(OakCommonFileFunctions *self,
+int oakcommon_filefunctions_copy_directory(OakFileFunctions self,
const char *source,
const char *dest, int overwrite);
@@ -93,7 +107,7 @@ int oakcommon_filefunctions_copy_directory(OakCommonFileFunctions *self,
* @param out Receives 1 (valid) or 0 (invalid).
*/
int oakcommon_filefunctions_directory_is_valid(
- OakCommonFileFunctions *self, const char *dir,
+ OakFileFunctions self, const char *dir,
int try_to_create_if_not_exists, int *out);
/**
@@ -103,7 +117,7 @@ int oakcommon_filefunctions_directory_is_valid(
* OAKCOMMON_E_* error code.
*/
int oakcommon_filefunctions_ensure_filename_extension(
- OakCommonFileFunctions *self, const char *filename,
+ OakFileFunctions self, const char *filename,
const char *extension, char *buf, int buf_size);
/**
@@ -114,7 +128,7 @@ int oakcommon_filefunctions_ensure_filename_extension(
* the file cannot be read.
*/
int oakcommon_filefunctions_read_file_as_string(
- OakCommonFileFunctions *self, const char *filename, char *buf,
+ OakFileFunctions self, const char *filename, char *buf,
int buf_size);
/**
@@ -124,7 +138,7 @@ int oakcommon_filefunctions_read_file_as_string(
* OAKCOMMON_E_* error code.
*/
int oakcommon_filefunctions_get_safe_temporary_filename(
- OakCommonFileFunctions *self, const char *original, char *buf,
+ OakFileFunctions self, const char *original, char *buf,
int buf_size);
/**
@@ -133,7 +147,7 @@ int oakcommon_filefunctions_get_safe_temporary_filename(
* @param out Receives 1 (renamed) or 0 (failed).
*/
int oakcommon_filefunctions_rename_file_allow_overwrite(
- OakCommonFileFunctions *self, const char *from, const char *to,
+ OakFileFunctions self, const char *from, const char *to,
int *out);
/**
@@ -143,7 +157,7 @@ int oakcommon_filefunctions_rename_file_allow_overwrite(
* OAKCOMMON_E_* error code.
*/
int oakcommon_filefunctions_get_formatted_executable_for_platform(
- OakCommonFileFunctions *self, const char *unformatted, char *buf,
+ OakFileFunctions self, const char *unformatted, char *buf,
int buf_size);
#ifdef __cplusplus
diff --git a/include/common/handle.h b/include/common/handle.h
new file mode 100644
index 000000000..8a6be7900
--- /dev/null
+++ b/include/common/handle.h
@@ -0,0 +1,69 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAK_EDITOR_HANDLE_H
+#define OAK_EDITOR_HANDLE_H
+
+#include
+
+/**
+ * @brief Current ABI version stamped into every oakcommon handle.
+ *
+ * Bump whenever the handle layout or the semantics of any exported
+ * function change incompatibly. Consumers should compare a handle's
+ * abi_version field against the value they were compiled with before
+ * dereferencing ctx.
+ */
+#define OAKCOMMON_ABI_VERSION 1
+
+/**
+ * @brief Neutral handle convention shared by all oakcommon wrappers.
+ *
+ * Every wrapper type is a by-value struct with the same four fields:
+ *
+ * typedef struct OakXxx {
+ * void *ctx; // opaque, points to the impl
+ * void (*addref)(void *ctx); // atomic +1, owner-DLL code
+ * void (*release)(void *ctx); // atomic -1, destroys at 0
+ * uint32_t abi_version; // OAKCOMMON_ABI_VERSION
+ * } OakXxx;
+ *
+ * Rules:
+ * - oakcommon__init*() returns a handle whose underlying object
+ * has reference count 1.
+ * - Copying the struct copies the pointer, not the count: call
+ * handle.addref(handle.ctx) for every additional long-lived copy and
+ * handle.release(handle.ctx) (or the oakcommon__free()
+ * convenience wrapper) when done with each copy.
+ * - release() decrements the atomic count and destroys the underlying
+ * object when it reaches zero; the destructor runs in the DLL that
+ * created the object, so cross-DLL handing is safe.
+ * - The struct itself carries no ownership: it is never heap-allocated
+ * by the API, so it needs no destruction of its own.
+ * - Functions that only read a handle take it BY VALUE (OakXxx self);
+ * an empty handle (ctx == NULL) is reported as OAKCOMMON_E_INVALID.
+ * oakcommon__free() deliberately stays a pointer API
+ * (OakXxx *h, like av_frame_unref()/av_buffer_unref()) so it can
+ * null out the caller's ctx after the final release; NULL and
+ * ctx == NULL are no-ops. Out parameters that produce a handle
+ * (e.g. option/positional-argument registration) also stay pointers.
+ */
+
+#endif //OAK_EDITOR_HANDLE_H
diff --git a/include/common/loopmode.h b/include/common/loopmode.h
index 79aa85205..d78bb9738 100644
--- a/include/common/loopmode.h
+++ b/include/common/loopmode.h
@@ -31,7 +31,7 @@ extern "C" {
* The numeric values must stay in sync with src/common/src/loopmode.h.
* Pure enum: no functions are needed.
*/
-enum OakCommonLoopMode {
+enum OakLoopMode {
OAKCOMMON_LOOP_MODE_OFF = 0, /**< Looping disabled. */
OAKCOMMON_LOOP_MODE_LOOP = 1, /**< Loop playback. */
OAKCOMMON_LOOP_MODE_CLAMP = 2 /**< Clamp at the end. */
diff --git a/include/common/ocioutils.h b/include/common/ocioutils.h
index 6bad146c1..f3be72246 100644
--- a/include/common/ocioutils.h
+++ b/include/common/ocioutils.h
@@ -22,6 +22,7 @@
#define OAK_EDITOR_OCIOUTILS_H
#include "common/error.h"
+#include "common/handle.h"
#ifdef __cplusplus
extern "C" {
@@ -33,7 +34,7 @@ extern "C" {
* The numeric values must stay in sync with
* olive/core/render/pixelformat.h (Format enum).
*/
-enum OakCommonPixelFormat {
+enum OakPixelFormat {
OAKCOMMON_PIXEL_FORMAT_INVALID = -1, /**< Invalid/unknown format. */
OAKCOMMON_PIXEL_FORMAT_U8 = 0, /**< 8-bit unsigned integer. */
OAKCOMMON_PIXEL_FORMAT_U10 = 1, /**< 10-bit unsigned integer. */
@@ -52,34 +53,50 @@ enum OakCommonPixelFormat {
* BitDepth enum: 0 = unknown, 1 = uint8, 2 = uint10, 3 = uint12,
* 4 = uint14, 5 = uint16, 6 = uint32, 7 = f16, 8 = f32 (OCIO v2).
*/
-typedef struct OakCommonOCIOUtils OakCommonOCIOUtils;
+/**
+ * @brief Neutral by-value handle for the OCIO utils family
+ *
+ * The object is stateless; the handle exists only to satisfy the C API
+ * lifetime contract. Ownership/count semantics follow common/handle.h:
+ * init returns a handle whose (empty) object has reference count 1 and
+ * release destroys it at zero. abi_version is always
+ * OAKCOMMON_ABI_VERSION.
+ */
+typedef struct OakOCIOUtils {
+ 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; /**< OAKCOMMON_ABI_VERSION. */
+} OakOCIOUtils;
/**
* @brief Creates an OCIOUtils handle
*
- * The object is stateless; the handle exists only to satisfy the C API
- * lifetime contract. Returns NULL on failure.
+ * @return Handle with reference count 1; ctx is NULL on failure.
*/
-OakCommonOCIOUtils *oakcommon_ocioutils_init(void);
+OakOCIOUtils oakcommon_ocioutils_init(void);
/**
- * @brief Destroys an OCIOUtils handle; no-op on NULL
+ * @brief Releases one reference to an OCIOUtils handle
+ *
+ * Convenience wrapper around handle.release(handle.ctx); no-op when
+ * self is NULL or self->ctx is NULL.
*/
-void oakcommon_ocioutils_free(OakCommonOCIOUtils *self);
+void oakcommon_ocioutils_free(OakOCIOUtils *self);
/**
* @brief Maps a native pixel format to an OCIO bit depth
*
* @param self handle from oakcommon_ocioutils_init()
- * @param pixel_format one of the OakCommonPixelFormat values
+ * @param pixel_format one of the OakPixelFormat values
* @param out_bit_depth receives the OCIO bit depth as an int (see the
- * OakCommonOCIOUtils typedef documentation); set to 0
+ * OakOCIOUtils typedef documentation); set to 0
* (BIT_DEPTH_UNKNOWN) for invalid formats
- * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self or
+ * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self.ctx or
* out_bit_depth is NULL or pixel_format is not a known code
*/
int oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format(
- OakCommonOCIOUtils *self, int pixel_format, int *out_bit_depth);
+ OakOCIOUtils self, int pixel_format, int *out_bit_depth);
#ifdef __cplusplus
}
diff --git a/include/common/oiioutils.h b/include/common/oiioutils.h
index f7ae5a709..ee48dfab8 100644
--- a/include/common/oiioutils.h
+++ b/include/common/oiioutils.h
@@ -23,7 +23,7 @@
#include "common/error.h"
-/* Reuses the OakCommonPixelFormat enum (mirroring
+/* Reuses the OakPixelFormat enum (mirroring
* olive::core::PixelFormat) rather than redefining it here. */
#include "common/ocioutils.h"
@@ -41,48 +41,64 @@ extern "C" {
* 13 = STRING, 14 = PTR. OIIO >= 2.5 adds 15 = USTRINGHASH and shifts
* LASTBASE, so the exact LASTBASE value is version-dependent.
*/
-typedef struct OakCommonOIIOUtils OakCommonOIIOUtils;
+/**
+ * @brief Neutral by-value handle for the OIIO utils family
+ *
+ * The object is stateless; the handle exists only to satisfy the C API
+ * lifetime contract. Ownership/count semantics follow common/handle.h:
+ * init returns a handle whose (empty) object has reference count 1 and
+ * release destroys it at zero. abi_version is always
+ * OAKCOMMON_ABI_VERSION.
+ */
+typedef struct OakOIIOUtils {
+ 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; /**< OAKCOMMON_ABI_VERSION. */
+} OakOIIOUtils;
/**
* @brief Creates an OIIOUtils handle
*
- * The object is stateless; the handle exists only to satisfy the C API
- * lifetime contract. Returns NULL on failure.
+ * @return Handle with reference count 1; ctx is NULL on failure.
*/
-OakCommonOIIOUtils *oakcommon_oiioutils_init(void);
+OakOIIOUtils oakcommon_oiioutils_init(void);
/**
- * @brief Destroys an OIIOUtils handle; no-op on NULL
+ * @brief Releases one reference to an OIIOUtils handle
+ *
+ * Convenience wrapper around handle.release(handle.ctx); no-op when
+ * self is NULL or self->ctx is NULL.
*/
-void oakcommon_oiioutils_free(OakCommonOIIOUtils *self);
+void oakcommon_oiioutils_free(OakOIIOUtils *self);
/**
* @brief Maps a native pixel format to an OIIO base type
*
* @param self handle from oakcommon_oiioutils_init()
- * @param pixel_format one of the OakCommonPixelFormat values
+ * @param pixel_format one of the OakPixelFormat values
* @param out_base_type receives the OIIO base type as an int (see the
- * OakCommonOIIOUtils typedef documentation); set to 0
+ * OakOIIOUtils typedef documentation); set to 0
* (TypeDesc::UNKNOWN) for invalid or unmappable formats
- * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self or out_base_type
- * is NULL or pixel_format is not a known code
+ * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self.ctx or
+ * out_base_type is NULL or pixel_format is not a known code
*/
int oakcommon_oiioutils_get_oiio_base_type_from_format(
- OakCommonOIIOUtils *self, int pixel_format, int *out_base_type);
+ OakOIIOUtils self, int pixel_format, int *out_base_type);
/**
* @brief Maps an OIIO base type to a native pixel format
*
* @param self handle from oakcommon_oiioutils_init()
* @param base_type an OIIO TypeDesc::BASETYPE value as an int
- * @param out_pixel_format receives one of the OakCommonPixelFormat
+ * @param out_pixel_format receives one of the OakPixelFormat
* values; set to OAKCOMMON_PIXEL_FORMAT_INVALID for unknown or
* unmappable base types
- * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self or
+ * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self.ctx or
* out_pixel_format is NULL or base_type is negative
*/
int oakcommon_oiioutils_get_format_from_oiio_basetype(
- OakCommonOIIOUtils *self, int base_type, int *out_pixel_format);
+ OakOIIOUtils self, int base_type, int *out_pixel_format);
/**
* @brief Converts a PixelAspectRatio attribute value to a rational
@@ -95,11 +111,11 @@ int oakcommon_oiioutils_get_format_from_oiio_basetype(
* @param pixel_aspect_ratio the PixelAspectRatio attribute value
* @param out_numerator receives the rational numerator
* @param out_denominator receives the rational denominator
- * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self, out_numerator
- * or out_denominator is NULL
+ * @return OAKCOMMON_OK, or OAKCOMMON_E_INVALID if self.ctx,
+ * out_numerator or out_denominator is NULL
*/
int oakcommon_oiioutils_get_pixel_aspect_ratio(
- OakCommonOIIOUtils *self, double pixel_aspect_ratio, int *out_numerator,
+ OakOIIOUtils self, double pixel_aspect_ratio, int *out_numerator,
int *out_denominator);
#ifdef __cplusplus
diff --git a/include/common/subtitleparams.h b/include/common/subtitleparams.h
index ccd8cee5e..b25af15bc 100644
--- a/include/common/subtitleparams.h
+++ b/include/common/subtitleparams.h
@@ -22,53 +22,89 @@
#define OAK_EDITOR_SUBTITLEPARAMS_H
#include "common/error.h"
+#include "common/handle.h"
#ifdef __cplusplus
+namespace olive
+{
+class SubtitleParams;
+}
extern "C" {
#endif
/**
- * @brief Opaque handle to a subtitle parameter set (olive::SubtitleParams).
+ * @brief Neutral by-value handle to a subtitle parameter set
+ * (olive::SubtitleParams).
+ *
+ * Ownership/count semantics follow the convention in common/handle.h:
+ * init functions return a handle whose object has reference count 1,
+ * addref(ctx)/release(ctx) adjust it atomically, and release destroys
+ * the object at zero. abi_version is always OAKCOMMON_ABI_VERSION.
*/
-typedef struct OakCommonSubtitleParams OakCommonSubtitleParams;
+typedef struct OakSubtitleParams {
+ 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; /**< OAKCOMMON_ABI_VERSION. */
+} OakSubtitleParams;
/**
* @brief Create an empty subtitle parameter set.
*
- * @return Params handle, or NULL on allocation failure.
+ * @return Handle with reference count 1; ctx is NULL on allocation
+ * failure.
*/
-OakCommonSubtitleParams *oakcommon_subtitleparams_init(void);
+OakSubtitleParams oakcommon_subtitleparams_init(void);
+
+#ifdef __cplusplus
+/**
+ * @brief Copy a native olive::SubtitleParams into a new handle.
+ *
+ * The source object is deep-copied; the handle does not keep any
+ * reference to @p src, which may be destroyed immediately afterwards.
+ * Only visible to C++ consumers.
+ *
+ * @return Handle with reference count 1; ctx is NULL if src is NULL or
+ * on allocation failure.
+ */
+OakSubtitleParams oakcommon_subtitleparams_init_from_native(
+ const olive::SubtitleParams *src);
+#endif
/**
- * @brief Destroy a subtitle parameter set. No-op on NULL.
+ * @brief Release one reference to a subtitle parameter set.
+ *
+ * Convenience wrapper around handle.release(handle.ctx): decrements the
+ * atomic reference count and destroys the object when it reaches zero.
+ * No-op when params is NULL or params->ctx is NULL.
*/
-void oakcommon_subtitleparams_free(OakCommonSubtitleParams *params);
+void oakcommon_subtitleparams_free(OakSubtitleParams *params);
int oakcommon_subtitleparams_get_stream_index(
- OakCommonSubtitleParams *params, int *index);
+ OakSubtitleParams params, int *index);
int oakcommon_subtitleparams_set_stream_index(
- OakCommonSubtitleParams *params, int index);
-int oakcommon_subtitleparams_get_enabled(OakCommonSubtitleParams *params,
+ OakSubtitleParams params, int index);
+int oakcommon_subtitleparams_get_enabled(OakSubtitleParams params,
int *enabled);
-int oakcommon_subtitleparams_set_enabled(OakCommonSubtitleParams *params,
+int oakcommon_subtitleparams_set_enabled(OakSubtitleParams params,
int enabled);
/**
* @brief Query whether the set contains at least one subtitle.
*/
-int oakcommon_subtitleparams_is_valid(OakCommonSubtitleParams *params,
+int oakcommon_subtitleparams_is_valid(OakSubtitleParams params,
int *is_valid);
/**
* @brief Number of subtitle entries.
*/
-int oakcommon_subtitleparams_count(OakCommonSubtitleParams *params,
+int oakcommon_subtitleparams_count(OakSubtitleParams params,
int *count);
/**
* @brief Out time of the last subtitle (0/1 when empty).
*/
-int oakcommon_subtitleparams_duration(OakCommonSubtitleParams *params,
+int oakcommon_subtitleparams_duration(OakSubtitleParams params,
int *numerator, int *denominator);
/**
@@ -77,14 +113,14 @@ int oakcommon_subtitleparams_duration(OakCommonSubtitleParams *params,
* @param text Subtitle text. Must not be NULL.
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_subtitleparams_add_subtitle(OakCommonSubtitleParams *params,
+int oakcommon_subtitleparams_add_subtitle(OakSubtitleParams params,
int in_num, int in_den, int out_num,
int out_den, const char *text);
/**
* @brief Remove all subtitle entries.
*/
-int oakcommon_subtitleparams_clear(OakCommonSubtitleParams *params);
+int oakcommon_subtitleparams_clear(OakSubtitleParams params);
/**
* @brief Get the time range of the subtitle at @p index.
@@ -92,7 +128,7 @@ int oakcommon_subtitleparams_clear(OakCommonSubtitleParams *params);
* @return OAKCOMMON_OK, OAKCOMMON_E_NOT_FOUND if @p index is out of range,
* or another negative OAKCOMMON_E_* error code.
*/
-int oakcommon_subtitleparams_get_subtitle(OakCommonSubtitleParams *params,
+int oakcommon_subtitleparams_get_subtitle(OakSubtitleParams params,
int index, int *in_num, int *in_den,
int *out_num, int *out_den);
@@ -103,7 +139,7 @@ int oakcommon_subtitleparams_get_subtitle(OakCommonSubtitleParams *params,
* (non-negative), OAKCOMMON_E_NOT_FOUND if @p index is out of
* range, or another negative OAKCOMMON_E_* error code.
*/
-int oakcommon_subtitleparams_get_subtitle_text(OakCommonSubtitleParams *params,
+int oakcommon_subtitleparams_get_subtitle_text(OakSubtitleParams params,
int index, char *buf,
int buf_size);
@@ -121,7 +157,7 @@ int oakcommon_subtitleparams_generate_ass_header(char *buf, int buf_size);
* @param xml NUL-terminated XML text. Must not be NULL.
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_subtitleparams_load_xml(OakCommonSubtitleParams *params,
+int oakcommon_subtitleparams_load_xml(OakSubtitleParams params,
const char *xml);
/**
@@ -130,7 +166,7 @@ int oakcommon_subtitleparams_load_xml(OakCommonSubtitleParams *params,
* @return Required buffer size in bytes including the terminating NUL
* (non-negative), or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_subtitleparams_save_xml(OakCommonSubtitleParams *params,
+int oakcommon_subtitleparams_save_xml(OakSubtitleParams params,
char *buf, int buf_size);
#ifdef __cplusplus
diff --git a/include/common/videoparams.h b/include/common/videoparams.h
index 1d00a2381..2dfb143e2 100644
--- a/include/common/videoparams.h
+++ b/include/common/videoparams.h
@@ -28,21 +28,37 @@
#include
#include "common/error.h"
+#include "common/handle.h"
#include "common/ocioutils.h"
#ifdef __cplusplus
+namespace olive
+{
+class VideoParams;
+}
extern "C" {
#endif
/**
- * @brief Opaque handle to a video parameter set (olive::VideoParams).
+ * @brief Neutral by-value handle to a video parameter set
+ * (olive::VideoParams).
+ *
+ * Ownership/count semantics follow the convention in common/handle.h:
+ * init functions return a handle whose object has reference count 1,
+ * addref(ctx)/release(ctx) adjust it atomically, and release destroys
+ * the object at zero. abi_version is always OAKCOMMON_ABI_VERSION.
*/
-typedef struct OakCommonVideoParams OakCommonVideoParams;
+typedef struct OakVideoParams {
+ 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; /**< OAKCOMMON_ABI_VERSION. */
+} OakVideoParams;
/**
* @brief Interlacing modes, mirroring olive::VideoParams::Interlacing.
*/
-enum OakCommonVideoInterlacing {
+enum OakVideoInterlacing {
OAKCOMMON_VIDEO_INTERLACE_NONE = 0,
OAKCOMMON_VIDEO_INTERLACED_TOP_FIRST = 1,
OAKCOMMON_VIDEO_INTERLACED_BOTTOM_FIRST = 2
@@ -51,7 +67,7 @@ enum OakCommonVideoInterlacing {
/**
* @brief Video stream types, mirroring olive::VideoParams::Type.
*/
-enum OakCommonVideoType {
+enum OakVideoType {
OAKCOMMON_VIDEO_TYPE_VIDEO = 0,
OAKCOMMON_VIDEO_TYPE_STILL = 1,
OAKCOMMON_VIDEO_TYPE_IMAGE_SEQUENCE = 2
@@ -60,7 +76,7 @@ enum OakCommonVideoType {
/**
* @brief Color range codes, mirroring olive::VideoParams::ColorRange.
*/
-enum OakCommonVideoColorRange {
+enum OakVideoColorRange {
OAKCOMMON_COLOR_RANGE_LIMITED = 0, /**< 16-235 */
OAKCOMMON_COLOR_RANGE_FULL = 1 /**< 0-255 */
};
@@ -68,17 +84,19 @@ enum OakCommonVideoColorRange {
/**
* @brief Create a default (invalid) video parameter set.
*
- * @return Params handle, or NULL on allocation failure.
+ * @return Handle with reference count 1; ctx is NULL on allocation
+ * failure.
*/
-OakCommonVideoParams *oakcommon_videoparams_init(void);
+OakVideoParams oakcommon_videoparams_init(void);
/**
* @brief Create a video parameter set without a time base.
*
- * @param pixel_format One of the OakCommonPixelFormat values.
- * @return Params handle, or NULL on allocation failure.
+ * @param pixel_format One of the OakPixelFormat values.
+ * @return Handle with reference count 1; ctx is NULL on allocation
+ * failure.
*/
-OakCommonVideoParams *oakcommon_videoparams_init_basic(
+OakVideoParams oakcommon_videoparams_init_basic(
int width, int height, int pixel_format, int nb_channels,
int pixel_aspect_num, int pixel_aspect_den, int interlacing, int divider);
@@ -87,103 +105,136 @@ OakCommonVideoParams *oakcommon_videoparams_init_basic(
*
* The frame rate is derived as the flipped time base.
*
- * @param pixel_format One of the OakCommonPixelFormat values.
- * @return Params handle, or NULL on allocation failure.
+ * @param pixel_format One of the OakPixelFormat values.
+ * @return Handle with reference count 1; ctx is NULL on allocation
+ * failure.
*/
-OakCommonVideoParams *oakcommon_videoparams_init_with_time_base(
+OakVideoParams oakcommon_videoparams_init_with_time_base(
int width, int height, int time_base_num, int time_base_den,
int pixel_format, int nb_channels, int pixel_aspect_num,
int pixel_aspect_den, int interlacing, int divider);
+#ifdef __cplusplus
/**
- * @brief Destroy a video parameter set. No-op on NULL.
+ * @brief Copy a native olive::VideoParams into a new handle.
+ *
+ * The source object is deep-copied; the handle does not keep any
+ * reference to @p src, which may be destroyed immediately afterwards.
+ * Only visible to C++ consumers.
+ *
+ * @return Handle with reference count 1; ctx is NULL if src is NULL or
+ * on allocation failure.
*/
-void oakcommon_videoparams_free(OakCommonVideoParams *params);
+OakVideoParams oakcommon_videoparams_init_from_native(
+ const olive::VideoParams *src);
-int oakcommon_videoparams_get_width(OakCommonVideoParams *params, int *width);
-int oakcommon_videoparams_set_width(OakCommonVideoParams *params, int width);
-int oakcommon_videoparams_get_height(OakCommonVideoParams *params, int *height);
-int oakcommon_videoparams_set_height(OakCommonVideoParams *params, int height);
-int oakcommon_videoparams_get_depth(OakCommonVideoParams *params, int *depth);
-int oakcommon_videoparams_set_depth(OakCommonVideoParams *params, int depth);
-int oakcommon_videoparams_get_is_3d(OakCommonVideoParams *params, int *is_3d);
+/**
+ * @brief Borrow the native object behind a handle.
+ *
+ * The returned pointer is borrowed: it stays valid while the caller
+ * holds a reference to the handle (i.e. until the matching release).
+ * Only visible to C++ consumers.
+ *
+ * @return Borrowed pointer, or NULL if params is NULL or params->ctx is
+ * NULL.
+ */
+const olive::VideoParams *oakcommon_videoparams_get_native(
+ OakVideoParams params);
+#endif
+
+/**
+ * @brief Release one reference to a video parameter set.
+ *
+ * Convenience wrapper around handle.release(handle.ctx): decrements the
+ * atomic reference count and destroys the object when it reaches zero.
+ * No-op when params is NULL or params->ctx is NULL.
+ */
+void oakcommon_videoparams_free(OakVideoParams *params);
+
+int oakcommon_videoparams_get_width(OakVideoParams params, int *width);
+int oakcommon_videoparams_set_width(OakVideoParams params, int width);
+int oakcommon_videoparams_get_height(OakVideoParams params, int *height);
+int oakcommon_videoparams_set_height(OakVideoParams params, int height);
+int oakcommon_videoparams_get_depth(OakVideoParams params, int *depth);
+int oakcommon_videoparams_set_depth(OakVideoParams params, int depth);
+int oakcommon_videoparams_get_is_3d(OakVideoParams params, int *is_3d);
/**
* @brief Rational getters return the value as a numerator/denominator pair.
*/
-int oakcommon_videoparams_get_time_base(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_time_base(OakVideoParams params,
int *numerator, int *denominator);
-int oakcommon_videoparams_set_time_base(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_time_base(OakVideoParams params,
int numerator, int denominator);
-int oakcommon_videoparams_get_frame_rate(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_frame_rate(OakVideoParams params,
int *numerator, int *denominator);
-int oakcommon_videoparams_set_frame_rate(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_frame_rate(OakVideoParams params,
int numerator, int denominator);
-int oakcommon_videoparams_frame_rate_as_time_base(OakCommonVideoParams *params,
+int oakcommon_videoparams_frame_rate_as_time_base(OakVideoParams params,
int *numerator,
int *denominator);
-int oakcommon_videoparams_get_pixel_aspect_ratio(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_pixel_aspect_ratio(OakVideoParams params,
int *numerator,
int *denominator);
-int oakcommon_videoparams_set_pixel_aspect_ratio(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_pixel_aspect_ratio(OakVideoParams params,
int numerator, int denominator);
/**
- * @brief Format getters/setters use the OakCommonPixelFormat codes.
+ * @brief Format getters/setters use the OakPixelFormat codes.
*/
-int oakcommon_videoparams_get_format(OakCommonVideoParams *params, int *format);
-int oakcommon_videoparams_set_format(OakCommonVideoParams *params, int format);
-int oakcommon_videoparams_get_channel_count(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_format(OakVideoParams params, int *format);
+int oakcommon_videoparams_set_format(OakVideoParams params, int format);
+int oakcommon_videoparams_get_channel_count(OakVideoParams params,
int *count);
-int oakcommon_videoparams_set_channel_count(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_channel_count(OakVideoParams params,
int count);
-int oakcommon_videoparams_get_interlacing(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_interlacing(OakVideoParams params,
int *interlacing);
-int oakcommon_videoparams_set_interlacing(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_interlacing(OakVideoParams params,
int interlacing);
-int oakcommon_videoparams_get_divider(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_divider(OakVideoParams params,
int *divider);
-int oakcommon_videoparams_set_divider(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_divider(OakVideoParams params,
int divider);
-int oakcommon_videoparams_get_enabled(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_enabled(OakVideoParams params,
int *enabled);
-int oakcommon_videoparams_set_enabled(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_enabled(OakVideoParams params,
int enabled);
-int oakcommon_videoparams_get_x(OakCommonVideoParams *params, float *x);
-int oakcommon_videoparams_set_x(OakCommonVideoParams *params, float x);
-int oakcommon_videoparams_get_y(OakCommonVideoParams *params, float *y);
-int oakcommon_videoparams_set_y(OakCommonVideoParams *params, float y);
-int oakcommon_videoparams_get_stream_index(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_x(OakVideoParams params, float *x);
+int oakcommon_videoparams_set_x(OakVideoParams params, float x);
+int oakcommon_videoparams_get_y(OakVideoParams params, float *y);
+int oakcommon_videoparams_set_y(OakVideoParams params, float y);
+int oakcommon_videoparams_get_stream_index(OakVideoParams params,
int *index);
-int oakcommon_videoparams_set_stream_index(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_stream_index(OakVideoParams params,
int index);
-int oakcommon_videoparams_get_video_type(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_video_type(OakVideoParams params,
int *type);
-int oakcommon_videoparams_set_video_type(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_video_type(OakVideoParams params,
int type);
-int oakcommon_videoparams_get_start_time(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_start_time(OakVideoParams params,
int64_t *start_time);
-int oakcommon_videoparams_set_start_time(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_start_time(OakVideoParams params,
int64_t start_time);
-int oakcommon_videoparams_get_duration(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_duration(OakVideoParams params,
int64_t *duration);
-int oakcommon_videoparams_set_duration(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_duration(OakVideoParams params,
int64_t duration);
-int oakcommon_videoparams_get_premultiplied_alpha(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_premultiplied_alpha(OakVideoParams params,
int *premultiplied);
-int oakcommon_videoparams_set_premultiplied_alpha(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_premultiplied_alpha(OakVideoParams params,
int premultiplied);
-int oakcommon_videoparams_get_color_range(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_color_range(OakVideoParams params,
int *color_range);
-int oakcommon_videoparams_set_color_range(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_color_range(OakVideoParams params,
int color_range);
-int oakcommon_videoparams_get_color_primaries(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_color_primaries(OakVideoParams params,
int *primaries);
-int oakcommon_videoparams_set_color_primaries(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_color_primaries(OakVideoParams params,
int primaries);
-int oakcommon_videoparams_get_color_transfer(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_color_transfer(OakVideoParams params,
int *transfer);
-int oakcommon_videoparams_set_color_transfer(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_color_transfer(OakVideoParams params,
int transfer);
/**
@@ -192,29 +243,29 @@ int oakcommon_videoparams_set_color_transfer(OakCommonVideoParams *params,
* @return Required buffer size in bytes including the terminating NUL
* (non-negative), or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_videoparams_get_colorspace(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_colorspace(OakVideoParams params,
char *buf, int buf_size);
-int oakcommon_videoparams_set_colorspace(OakCommonVideoParams *params,
+int oakcommon_videoparams_set_colorspace(OakVideoParams params,
const char *colorspace);
/**
* @brief Width multiplied by the pixel aspect ratio.
*/
-int oakcommon_videoparams_get_square_pixel_width(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_square_pixel_width(OakVideoParams params,
int *width);
-int oakcommon_videoparams_get_effective_width(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_effective_width(OakVideoParams params,
int *width);
-int oakcommon_videoparams_get_effective_height(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_effective_height(OakVideoParams params,
int *height);
-int oakcommon_videoparams_get_effective_depth(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_effective_depth(OakVideoParams params,
int *depth);
-int oakcommon_videoparams_get_is_valid(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_is_valid(OakVideoParams params,
int *is_valid);
-int oakcommon_videoparams_get_bytes_per_channel(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_bytes_per_channel(OakVideoParams params,
int *bytes);
-int oakcommon_videoparams_get_bytes_per_pixel(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_bytes_per_pixel(OakVideoParams params,
int *bytes);
-int oakcommon_videoparams_get_buffer_size(OakCommonVideoParams *params,
+int oakcommon_videoparams_get_buffer_size(OakVideoParams params,
int *size);
/**
@@ -224,14 +275,14 @@ int oakcommon_videoparams_get_buffer_size(OakCommonVideoParams *params,
* set.
*/
int oakcommon_videoparams_get_time_in_timebase_units(
- OakCommonVideoParams *params, int time_num, int time_den,
+ OakVideoParams params, int time_num, int time_den,
int64_t *timestamp);
/**
* @brief Compare two parameter sets for equality.
*/
-int oakcommon_videoparams_equals(OakCommonVideoParams *params,
- OakCommonVideoParams *other, int *equal);
+int oakcommon_videoparams_equals(OakVideoParams params,
+ OakVideoParams other, int *equal);
/**
* @brief Load parameters from an XML fragment.
@@ -239,7 +290,7 @@ int oakcommon_videoparams_equals(OakCommonVideoParams *params,
* @param xml NUL-terminated XML text. Must not be NULL.
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_videoparams_load_xml(OakCommonVideoParams *params,
+int oakcommon_videoparams_load_xml(OakVideoParams params,
const char *xml);
/**
@@ -248,7 +299,7 @@ int oakcommon_videoparams_load_xml(OakCommonVideoParams *params,
* @return Required buffer size in bytes including the terminating NUL
* (non-negative), or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_videoparams_save_xml(OakCommonVideoParams *params, char *buf,
+int oakcommon_videoparams_save_xml(OakVideoParams params, char *buf,
int buf_size);
/* Static helpers (no handle required). */
@@ -294,6 +345,21 @@ int oakcommon_videoparams_get_format_name(int pixel_format, char *buf,
int oakcommon_videoparams_frame_rate_to_string(int numerator, int denominator,
char *buf, int buf_size);
+/**
+ * @brief Get bytes per channel.
+ *
+ * @return Bytes per channel.
+ */
+int oakcommon_videoparams_static_get_bytes_per_channel(OakPixelFormat format);
+
+/**
+ * @brief Get bytes per pixel.
+ *
+ * @return Bytes per pixel.
+ */
+int oakcommon_videoparams_static_get_bytes_per_pixel(OakPixelFormat format,
+ int channels);
+
#ifdef __cplusplus
}
#endif
diff --git a/include/common/xmlutils.h b/include/common/xmlutils.h
index e83ae0194..8c8b38ca0 100644
--- a/include/common/xmlutils.h
+++ b/include/common/xmlutils.h
@@ -22,26 +22,56 @@
#define OAK_EDITOR_XMLUTILS_H
#include "common/error.h"
+#include "common/handle.h"
#ifdef __cplusplus
extern "C" {
#endif
-typedef struct OakCommonXmlReader OakCommonXmlReader;
-typedef struct OakCommonXmlWriter OakCommonXmlWriter;
+/**
+ * @brief Neutral by-value handle to a streaming XML reader.
+ *
+ * Ownership/count semantics follow the convention in common/handle.h:
+ * init returns a handle whose object has reference count 1,
+ * addref(ctx)/release(ctx) adjust it atomically, and release destroys
+ * the object at zero. abi_version is always OAKCOMMON_ABI_VERSION.
+ */
+typedef struct OakXmlReader {
+ 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; /**< OAKCOMMON_ABI_VERSION. */
+} OakXmlReader;
+
+/**
+ * @brief Neutral by-value handle to a streaming XML writer.
+ *
+ * Same ownership/count semantics as OakXmlReader.
+ */
+typedef struct OakXmlWriter {
+ 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; /**< OAKCOMMON_ABI_VERSION. */
+} OakXmlWriter;
/**
* @brief Create a streaming XML reader over a complete document.
*
* @param data NUL-terminated XML text. Must not be NULL.
- * @return A new reader, or NULL on failure (NULL data, out of memory).
+ * @return Handle with reference count 1; ctx is NULL on failure
+ * (NULL data, out of memory).
*/
-OakCommonXmlReader *oakcommon_xml_reader_init(const char *data);
+OakXmlReader oakcommon_xml_reader_init(const char *data);
/**
- * @brief Destroy a reader. No-op on NULL.
+ * @brief Release one reference to a reader.
+ *
+ * Convenience wrapper around handle.release(handle.ctx): decrements the
+ * atomic reference count and destroys the object when it reaches zero.
+ * No-op when reader is NULL or reader->ctx is NULL.
*/
-void oakcommon_xml_reader_free(OakCommonXmlReader *reader);
+void oakcommon_xml_reader_free(OakXmlReader *reader);
/**
* @brief Advance until the next start element, an end element, or the end
@@ -51,7 +81,7 @@ void oakcommon_xml_reader_free(OakCommonXmlReader *reader);
* @param found Out: 1 if positioned on a start element, 0 otherwise.
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_xml_reader_read_next_start_element(OakCommonXmlReader *reader,
+int oakcommon_xml_reader_read_next_start_element(OakXmlReader reader,
int *found);
/**
@@ -60,7 +90,7 @@ int oakcommon_xml_reader_read_next_start_element(OakCommonXmlReader *reader,
* @return Required buffer size in bytes (including NUL), or a negative
* OAKCOMMON_E_* error code.
*/
-int oakcommon_xml_reader_name(OakCommonXmlReader *reader, char *buf,
+int oakcommon_xml_reader_name(OakXmlReader reader, char *buf,
int buf_size);
/**
@@ -72,7 +102,7 @@ int oakcommon_xml_reader_name(OakCommonXmlReader *reader, char *buf,
* @return Required buffer size in bytes (including NUL), or a negative
* OAKCOMMON_E_* error code.
*/
-int oakcommon_xml_reader_read_element_text(OakCommonXmlReader *reader,
+int oakcommon_xml_reader_read_element_text(OakXmlReader reader,
char *buf, int buf_size);
/**
@@ -80,14 +110,14 @@ int oakcommon_xml_reader_read_element_text(OakCommonXmlReader *reader,
*
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_xml_reader_skip_current_element(OakCommonXmlReader *reader);
+int oakcommon_xml_reader_skip_current_element(OakXmlReader reader);
/**
* @brief Number of attributes on the current start element.
*
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_xml_reader_attribute_count(OakCommonXmlReader *reader,
+int oakcommon_xml_reader_attribute_count(OakXmlReader reader,
int *count);
/**
@@ -96,7 +126,7 @@ int oakcommon_xml_reader_attribute_count(OakCommonXmlReader *reader,
* @return Required buffer size in bytes (including NUL), OAKCOMMON_E_NOT_FOUND
* if @p index is out of range, or another negative OAKCOMMON_E_* code.
*/
-int oakcommon_xml_reader_attribute_name(OakCommonXmlReader *reader, int index,
+int oakcommon_xml_reader_attribute_name(OakXmlReader reader, int index,
char *buf, int buf_size);
/**
@@ -105,7 +135,7 @@ int oakcommon_xml_reader_attribute_name(OakCommonXmlReader *reader, int index,
* @return Required buffer size in bytes (including NUL), OAKCOMMON_E_NOT_FOUND
* if @p index is out of range, or another negative OAKCOMMON_E_* code.
*/
-int oakcommon_xml_reader_attribute_value(OakCommonXmlReader *reader,
+int oakcommon_xml_reader_attribute_value(OakXmlReader reader,
int index, char *buf, int buf_size);
/**
@@ -113,32 +143,36 @@ int oakcommon_xml_reader_attribute_value(OakCommonXmlReader *reader,
*
* @return OAKCOMMON_OK or a negative OAKCOMMON_E_* error code.
*/
-int oakcommon_xml_reader_has_error(OakCommonXmlReader *reader,
+int oakcommon_xml_reader_has_error(OakXmlReader reader,
int *has_error);
/**
* @brief Create a streaming XML writer.
*
- * @return A new writer, or NULL on failure.
+ * @return Handle with reference count 1; ctx is NULL on failure.
*/
-OakCommonXmlWriter *oakcommon_xml_writer_init(void);
+OakXmlWriter oakcommon_xml_writer_init(void);
/**
- * @brief Destroy a writer. No-op on NULL.
+ * @brief Release one reference to a writer.
+ *
+ * Convenience wrapper around handle.release(handle.ctx): decrements the
+ * atomic reference count and destroys the object when it reaches zero.
+ * No-op when writer is NULL or writer->ctx is NULL.
*/
-void oakcommon_xml_writer_free(OakCommonXmlWriter *writer);
+void oakcommon_xml_writer_free(OakXmlWriter *writer);
-int oakcommon_xml_writer_write_start_element(OakCommonXmlWriter *writer,
+int oakcommon_xml_writer_write_start_element(OakXmlWriter writer,
const char *name);
-int oakcommon_xml_writer_write_attribute(OakCommonXmlWriter *writer,
+int oakcommon_xml_writer_write_attribute(OakXmlWriter writer,
const char *name, const char *value);
-int oakcommon_xml_writer_write_characters(OakCommonXmlWriter *writer,
+int oakcommon_xml_writer_write_characters(OakXmlWriter writer,
const char *text);
-int oakcommon_xml_writer_write_text_element(OakCommonXmlWriter *writer,
+int oakcommon_xml_writer_write_text_element(OakXmlWriter writer,
const char *name,
const char *text);
-int oakcommon_xml_writer_write_end_element(OakCommonXmlWriter *writer);
-int oakcommon_xml_writer_write_end_document(OakCommonXmlWriter *writer);
+int oakcommon_xml_writer_write_end_element(OakXmlWriter writer);
+int oakcommon_xml_writer_write_end_document(OakXmlWriter writer);
/**
* @brief The document written so far.
@@ -146,7 +180,7 @@ int oakcommon_xml_writer_write_end_document(OakCommonXmlWriter *writer);
* @return Required buffer size in bytes (including NUL), or a negative
* OAKCOMMON_E_* error code.
*/
-int oakcommon_xml_writer_output(OakCommonXmlWriter *writer, char *buf,
+int oakcommon_xml_writer_output(OakXmlWriter writer, char *buf,
int buf_size);
#ifdef __cplusplus
diff --git a/include/node/block.h b/include/node/block.h
index 55dc5fa78..1ffabfbd7 100644
--- a/include/node/block.h
+++ b/include/node/block.h
@@ -189,7 +189,7 @@ int oaknode_clip_get_maintain_audio_pitch(OakNodeBlock *clip, int *maintain);
int oaknode_clip_set_maintain_audio_pitch(OakNodeBlock *clip, int maintain);
/**
- * @brief Loop mode, one of the OakCommonLoopMode values
+ * @brief Loop mode, one of the OakLoopMode values
* (olive::ClipBlock::loop_mode/set_loop_mode).
*/
int oaknode_clip_get_loop_mode(OakNodeBlock *clip, int *loop_mode);
diff --git a/include/node/colormanager.h b/include/node/colormanager.h
index d89d6b4f1..0ea0867bc 100644
--- a/include/node/colormanager.h
+++ b/include/node/colormanager.h
@@ -174,13 +174,13 @@ int oaknode_colormanager_get_default_luma_coefs(OakNodeColorManager *manager,
* colorspace) is clamped to what the active config offers
* (olive::ColorManager::get_compliant_color_space(ColorTransform, bool)).
*
- * `out` receives a NEW handle owned by the caller (release with
- * oakcommon_colortransform_free()). Requires a loaded config
- * (OAKNODE_E_STATE otherwise).
+ * `out` receives a NEW by-value handle owned by the caller (reference
+ * count 1, release with oakcommon_colortransform_free()). Requires a
+ * loaded config (OAKNODE_E_STATE otherwise).
*/
int oaknode_colormanager_get_compliant_color_transform(
- OakNodeColorManager *manager, const OakCommonColorTransform *transform,
- int force_display, OakCommonColorTransform **out);
+ OakNodeColorManager *manager, OakColorTransform transform,
+ int force_display, OakColorTransform *out);
#ifdef __cplusplus
}
diff --git a/include/node/sequence.h b/include/node/sequence.h
index 512fe6e6c..4fa8ba738 100644
--- a/include/node/sequence.h
+++ b/include/node/sequence.h
@@ -128,20 +128,24 @@ int oaknode_sequence_get_audio_stream_count(OakNodeSequence *sequence,
int *count);
/**
- * @brief Video parameters at `index` as a NEW handle owned by the caller
- * (release with oakcommon_videoparams_free()).
+ * @brief Video parameters at `index` as a NEW by-value handle owned by
+ * the caller (reference count 1, release with
+ * oakcommon_videoparams_free()).
*
* @return OAKNODE_OK, OAKNODE_E_INVALID, OAKNODE_E_NOT_FOUND or
* OAKNODE_E_NOMEM.
*/
int oaknode_sequence_get_video_params(OakNodeSequence *sequence, int index,
- OakCommonVideoParams **out);
+ OakVideoParams *out);
/**
* @brief Replace the video parameters at `index` with a copy of `params`.
+ *
+ * @return OAKNODE_E_INVALID if sequence is NULL, params.ctx is NULL, or
+ * index is negative.
*/
int oaknode_sequence_set_video_params(OakNodeSequence *sequence, int index,
- const OakCommonVideoParams *params);
+ OakVideoParams params);
/**
* @brief Audio parameters at `index` as a NEW handle owned by the caller
diff --git a/include/render/cancelatom.h b/include/render/cancelatom.h
new file mode 100644
index 000000000..bc1dc72df
--- /dev/null
+++ b/include/render/cancelatom.h
@@ -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 .
+
+***/
+
+#ifndef OAK_EDITOR_RENDER_CANCELATOM_H
+#define OAK_EDITOR_RENDER_CANCELATOM_H
+
+#include
+
+#include "error.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief Current ABI version stamped into every oakrender handle.
+ *
+ * Bump whenever a handle layout or the semantics of any exported function
+ * change incompatibly. Consumers should compare a handle's abi_version
+ * field against the value they were compiled with before dereferencing
+ * ctx.
+ */
+#define OAKRENDER_ABI_VERSION 1
+
+/**
+ * @file cancelatom.h
+ * @brief C ABI for the oakrender cancellation primitive
+ * (olive::CancelAtom), a thread-safe cancel flag shared between a
+ * render/encode caller and its worker.
+ *
+ * OakCancelAtom follows the neutral by-value handle convention (see
+ * oakcommon's common/handle.h): oakrender_cancelatom_init() returns a
+ * handle whose underlying object has reference count 1, the addref and
+ * release function pointers adjust that count atomically (release
+ * destroys the object at zero), and abi_version is always
+ * OAKRENDER_ABI_VERSION. Copying the struct copies the pointer, not the
+ * count: call addref for every additional long-lived copy and release (or
+ * oakrender_cancelatom_free()) when done with each copy. Functions that
+ * only use a handle take it BY VALUE; an empty handle (ctx == NULL) is
+ * reported as OAKRENDER_E_INVALID.
+ */
+typedef struct OakCancelAtom {
+ 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; /**< OAKRENDER_ABI_VERSION. */
+} OakCancelAtom;
+
+/**
+ * @brief Create a cancellation atom in the not-cancelled state.
+ *
+ * @return Handle with reference count 1; ctx is NULL on allocation
+ * failure.
+ */
+OakCancelAtom oakrender_cancelatom_init(void);
+
+/**
+ * @brief Release one reference to a cancellation atom.
+ *
+ * Convenience wrapper around atom->release(atom->ctx): decrements the
+ * atomic reference count and destroys the object when it reaches zero,
+ * then nulls atom->ctx. No-op when atom is NULL or atom->ctx is NULL.
+ */
+void oakrender_cancelatom_free(OakCancelAtom *atom);
+
+/**
+ * @brief Set the cancel flag (CancelAtom::cancel()). Thread-safe.
+ *
+ * @return OAKRENDER_OK, or OAKRENDER_E_INVALID for an empty handle.
+ */
+int oakrender_cancelatom_cancel(OakCancelAtom atom);
+
+/**
+ * @brief Read the cancel flag (CancelAtom::is_cancelled()).
+ *
+ * Reading a set flag also records that a consumer heard the
+ * cancellation; see oakrender_cancelatom_heard_cancel().
+ *
+ * @param cancelled Receives 1 when cancelled, 0 otherwise.
+ * @return OAKRENDER_OK, or OAKRENDER_E_INVALID for an empty handle or a
+ * NULL out parameter.
+ */
+int oakrender_cancelatom_is_cancelled(OakCancelAtom atom, int *cancelled);
+
+/**
+ * @brief Whether any consumer has observed the cancel flag through
+ * oakrender_cancelatom_is_cancelled() (CancelAtom::heard_cancel()).
+ *
+ * @param heard Receives 1 when the cancellation was heard, 0 otherwise.
+ * @return OAKRENDER_OK, or OAKRENDER_E_INVALID for an empty handle or a
+ * NULL out parameter.
+ */
+int oakrender_cancelatom_heard_cancel(OakCancelAtom atom, int *heard);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif //OAK_EDITOR_RENDER_CANCELATOM_H
diff --git a/include/render/color.h b/include/render/color.h
index a1721e6b7..6c9eef99f 100644
--- a/include/render/color.h
+++ b/include/render/color.h
@@ -22,6 +22,7 @@
#define OAK_EDITOR_RENDER_COLOR_H
#include "error.h"
+#include "renderer.h"
#ifdef __cplusplus
extern "C" {
@@ -93,6 +94,23 @@ int oakrender_color_processor_convert(OakColorProcessor *processor,
double ia, double *out_r, double *out_g,
double *out_b, double *out_a);
+/**
+ * @brief Convert a CPU frame's pixels through the processor, in place
+ * (olive::ColorProcessor::convert_frame()).
+ *
+ * The frame's data buffer is rewritten through an OCIO PackedImageDesc
+ * view; nothing is allocated and the frame handle stays owned by the
+ * caller. A processor whose underlying OCIO processor is null
+ * (oakrender_color_processor_create() treats lookup failure as
+ * non-fatal) is a pass-through and returns OAKRENDER_OK, mirroring the
+ * C++ API.
+ *
+ * @return OAKRENDER_OK, OAKRENDER_E_INVALID for NULL/uninitialized
+ * arguments, or OAKRENDER_E_FAILED on an internal exception.
+ */
+int oakrender_color_processor_convert_frame(OakColorProcessor *processor,
+ OakCodecFrame *frame);
+
/* ---- ColorManager statics ------------------------------------------------- */
/**
diff --git a/shared/include/oakutil/oakvideo.h b/shared/include/oakutil/oakvideo.h
index 1f9c4fb0a..5918b6d4c 100644
--- a/shared/include/oakutil/oakvideo.h
+++ b/shared/include/oakutil/oakvideo.h
@@ -21,7 +21,7 @@
#ifndef OAKUTIL_OAKVIDEO_H
#define OAKUTIL_OAKVIDEO_H
-#include
+#include
#include
@@ -167,30 +167,30 @@ class ColorTransform
public:
ColorTransform() = default;
- ColorTransform(const QString &output) : output_(output) {}
+ ColorTransform(const std::string &output) : output_(output) {}
- ColorTransform(const QString &display, const QString &view,
- const QString &look)
+ ColorTransform(const std::string &display, const std::string &view,
+ const std::string &look)
: output_(display), is_display_(true), view_(view), look_(look)
{
}
bool is_display() const { return is_display_; }
- const QString &display() const { return output_; }
+ const std::string &display() const { return output_; }
- const QString &output() const { return output_; }
+ const std::string &output() const { return output_; }
- const QString &view() const { return view_; }
+ const std::string &view() const { return view_; }
- const QString &look() const { return look_; }
+ const std::string &look() const { return look_; }
private:
- QString output_;
+ std::string output_;
bool is_display_ = false;
- QString view_;
- QString look_;
+ std::string view_;
+ std::string look_;
};
} // namespace oak
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index df31c126e..7b86873b7 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,3 +1,5 @@
add_subdirectory(common)
-add_subdirectory(undo)add_subdirectory(node)
+add_subdirectory(undo)
+add_subdirectory(node)
add_subdirectory(render)
+add_subdirectory(codec)
diff --git a/src/codec/CMakeLists.txt b/src/codec/CMakeLists.txt
new file mode 100644
index 000000000..f41570121
--- /dev/null
+++ b/src/codec/CMakeLists.txt
@@ -0,0 +1,6 @@
+add_subdirectory(src)
+add_subdirectory(c_api)
+
+if(BUILD_TESTS)
+ add_subdirectory(tests)
+endif()
diff --git a/src/codec/NOTES.md b/src/codec/NOTES.md
new file mode 100644
index 000000000..90e837d02
--- /dev/null
+++ b/src/codec/NOTES.md
@@ -0,0 +1,71 @@
+# oakcodec ไธญ้ดๆไธ่กไธบๅๅๅคๅฟ๏ผM5๏ผ
+
+## ไธญ้ดๆ๏ผ็ญๅพ
ๅ็ปญ้็จ็ขๆถๅฃ๏ผ
+
+1. **Task ๅ่ฐๆณจๅ**๏ผM8 ๆถๅฃ๏ผ๏ผconform/proxy ็ๅๅฐไปปๅก็ป
+ `include/codec/task.h` ็ๅ
จๅฑๆไบคๅ่ฐ๏ผ`oakcodec_set_task_submit_cb`๏ผใ
+ ๆชๆณจๅๆถ๏ผconform ๆฅ่ฏข่ฟๅ `k_conform_unavailable`๏ผproxy ไฟๆ
+ `k_proxy_missing`๏ผไธๅดฉๆบไธ้ปๅกใๆณจๅ่ฏญไนไธบๅๆญฅๆไบค๏ผๅ่ฐๅ
ๅฎๆๆ
+ ๆ้ๅ็ซๅณ่ฟๅ๏ผ๏ผ`SubmitTask` ๆ้่ฐๅ่ฐ๏ผๅ่ฐๅ
ไธๅฏ้ๅ
ฅๆณจๅๅฝๆฐใ
+ conform/proxy ไปปๅก็ workingโfinished ๆนๅ็ๅฝๅจๆๆดไฝ็งปไบค M8 oaktaskใ
+2. **Config**๏ผconfig ้็จ็ขๆถๅฃ๏ผ๏ผ`ProxyManager::proxy_params_from_config()`
+ ่ฟๅ็ผ่ฏๆ้ป่ฎคๅผ๏ผ1280x720/div1/mp4/crf23/veryfast/ๅซ้ณ้ข๏ผ๏ผๆชๅผๅ
ฅ
+ ๅ
ๅญๆ stub๏ผffmpegencoder ๅฝๅ็ๆฌๅทฒไธ่ฏป Config๏ผใ
+3. **็บน็่ทฏๅพๅ่ฝๅ้**๏ผoakrender ๅข่กฅ shader-blit C API ๅๅฏๆขๅค๏ผ๏ผ
+ oakrender C API ๆ ้็จ shader-blit๏ผFFmpegDecoder ็ yuv2rgb GLSL ่ทฏๅพไธ
+ ๅป้่ก shader ่ทฏๅพๅทฒๅ ้ค๏ผYUV ๅธงๆนๅจ CPU ไธ swscale ่ฝฌ RGBA ๅ
+ `oakrender_display_texture_upload`๏ผๅ่ฝไฟ็ไฝๆดๆ
ข๏ผๅป้่กๅจ็บน็่ทฏๅพ
+ ไธขๅคฑ๏ผCPU ๅธง่ทฏๅพๆฌๅฐฑไธๅๅป้่ก๏ผใTexture ้ถๆท่ดๆๆ hw frame ไธๅนถๅ ้คใ
+4. **FootageDescription ไธบ codec ๅ
้จ็ปๆ**๏ผsrc/codec/src/footagedescription.h๏ผ๏ผ
+ oaknode C API ๆ ๅฏนๅบ็ฉ๏ผๆชๅฎ็ฐๆข้็ผๅญ XML load/save ไธ
+ `get_type_of_stream()`๏ผoaknode `Track::Type` ๆ ๅฐ๏ผ๏ผoaknode footage
+ ไพง้่ฆๆถๅ่กฅใ
+5. **RenderMode**๏ผoakrender C API ๆ ๅฏนๅบ็ฉ๏ผcodec ๆฌๅฐ enum
+ ๏ผdecoder.h๏ผk_offline=0/k_online=1๏ผๅผๅฏน้ฝ engine/render/rendermodes.h๏ผใ
+6. **ๆ adapter ๅฑ**๏ผ2026-08 ็ฌฌไบ่ฝฎๆๆฟ๏ผ๏ผcodec ๅ
้จ่ทจๆจกๅ่ฐ็จๅ
จ้จ็ด่ฐ
+ `oakcommon_*` / `oakrender_*` C ๅฝๆฐ๏ผๅฅๆ๏ผOakVideoParams/
+ OakColorTransform/OakCancelAtom/OakSubtitleParams๏ผๅฐฑๅฐๆๅผ็ฎก็่ฎกๆฐ๏ผ
+ ๅชๆ็ๆญฃๅคๅค้ๅค็่ฝฌๆขไฟ็ๆไปถๅ
static ๅฐๅฝๆฐ๏ผๅฆ
+ fill_render_paramsใcancel_atom_is_cancelled๏ผใๆฉๆ็ไธ็
+ src/codec/src/adapter/ ๅ
่ฃ
็ฑปๅทฒๅ ้คใ
+7. **XmlStreamWriter/Reader**๏ผ็
ง DEQT.md ็จ oakcommon ็ C++ ็ฑป
+ ๏ผsrc/common/src/xmlutils.h๏ผไธ oaknode/oakrender ็ๅฎ่ทตไธ่ด๏ผ๏ผๆช่ตฐ
+ C API โโ ๅณ็ญ 7 ็ๅฏไธไพๅค๏ผ่ฎฐๅฝๅจๆกใ
+
+## ่กไธบๅๅ๏ผ็ธๅฏน Qt ็๏ผ
+
+- Decoder ็ `index_progress` ไฟกๅท โ `std::function`
+ ๅ่ฐ๏ผ`set_index_progress_callback`๏ผ๏ผconform_ready/proxy_ready/
+ proxy_finished ไฟกๅทๅ ้ค๏ผ้็ฅๅฝ facade/task ็ณป็ป๏ผใ
+- ConformManager ๆ ็ถๆๅ๏ผ`conforming_` ๅ่กจไธๅฎๆ slot ๅ ้ค๏ผ
+ `get_conform_state` ๅปๆ `decoder_id` ๅๆฐ๏ผ็ญๅพ
่ฏญไนๆนไธบๅๆญฅๆไบคๅ
+ ้ๆฅๆไปถ็ณป็ปใ
+- `Encoder::write_subtitle(const SubtitleBlock*)` โ
+ `write_subtitle(const char *text, double in_seconds, double out_seconds)`ใ
+ ๆณจๆๅๅฎ็ฐไผ ็ๆฏ `sub_block->length()`๏ผๆถ้ฟ๏ผ๏ผๆฐ่ฐ็จๆนไผ out=in+lengthใ
+- `EncodingParams::generate_matrix` ่ฟๅ `std::array`๏ผ่กไธปๅบ๏ผ๏ผ
+ ๅ QMatrix4x4๏ผ`load/save` ็ QIODevice ็ๆฌๅ
+ `load(const std::string&)`/`save_to_string()`๏ผ้ข่ฎพ XML ไธๅๅซๅฃฐๆไธ
+ ็ผฉ่ฟ๏ผ็ดงๅ XML๏ผๅ
็ด /ๅฑๆงๅไธ้กบๅบไธๅ๏ผ๏ผ`video_opts_` ็ XML ้กบๅบ
+ ็ฑ QHash ๆ ๅบๅไธบๅญๅ
ธๅบใไฟ็ไบ load_v1 ไธ่ต custom_range_ ็ๅ bugใ
+- `PlanarFileDevice::open` ็จ `std::vector` + ็ฑปๅ
+ `OpenMode` ๆไธพ๏ผk_read_only/k_write_only๏ผ๏ผFILE* ๅฎ็ฐใ
+- FFmpegDecoder ๆ ๅๅฐ QThread๏ผ็ฐ engine ็ๆฌๅทฒๆฏๅๆญฅ retrieve ๅพช็ฏ๏ผใ
+- ้ณ้ข decode๏ผC API๏ผ๏ผ้่ฆ conform ็ๅชไฝๅจๆ task ๆณจๅๆนๆถ่ฟๅ
+ `OAKCODEC_E_STATE`๏ผไธไบง็ๅๅฐ conform๏ผใ
+- `oakcodec_audio_stream_info.duration_ts` ๆ 0๏ผAudioParams ไธๅธฆๆถ้ฟ๏ผใ
+
+## ็ฌฆๅทๅฏ่งๆง
+
+oakcodec ไปฅ `-fvisibility=hidden` ็ผ่ฏ๏ผไป
ๅฏผๅบ `OAKCODEC_API` ๆ ่ฎฐ็
+C ๅฝๆฐ๏ผinclude/codec/error.h ๅฎไนๅฎ๏ผใๅฟ
้กปๅฆๆญค๏ผcodec ๅ
้จ adapter
+็ฑป๏ผolive::VideoParams ็ญ๏ผไธ oakcommon/oakrender ๅ
ๅๅๅผฑ็ฌฆๅทไผ
+interpose๏ผๆพๅจ oakcommon_videoparams_init_with_time_base ๅ
้จๆ
+VideoParams::width() ็ป่ฟ liboakcodec ๅฏผ่ดๅดฉๆบ๏ผใ
+
+## oakcommon ไพงไฟฎๅค๏ผ้ M5 ่ฝๅฐ๏ผ
+
+- `frame_to_buffer`/`buffer_to_frame` ็งปๅ
ฅ codec๏ผoiioframebridge.h๏ผ
+ ๅ
้จ C++ ๅฝๆฐ๏ผ๏ผoakcommon ็ OIIO ๆ ๅฐๅฝๆฐไฟ็ใ
+- ไฟฎๅค `src/common/c_api/videoparams.cpp` ็ `convert_to_olive_format`
+ switch ็ผบ break ็ฉฟ้ bug๏ผU8 ็ฉฟ้ๅฐ f32๏ผbytes_per_pixel ่ฟๅ 16๏ผใ
diff --git a/src/codec/c_api/CMakeLists.txt b/src/codec/c_api/CMakeLists.txt
new file mode 100644
index 000000000..8a8e0e272
--- /dev/null
+++ b/src/codec/c_api/CMakeLists.txt
@@ -0,0 +1,7 @@
+target_sources(oakcodec PRIVATE
+ conform.cpp
+ decoder.cpp
+ encoder.cpp
+ frame.cpp
+ proxy.cpp
+)
diff --git a/src/codec/c_api/conform.cpp b/src/codec/c_api/conform.cpp
new file mode 100644
index 000000000..9dbd1d968
--- /dev/null
+++ b/src/codec/c_api/conform.cpp
@@ -0,0 +1,141 @@
+/***
+
+ 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 .
+
+***/
+
+#include "codec/conform.h"
+
+#include
+#include
+#include
+#include
+
+#include "conformmanager.h"
+#include "decoder.h"
+
+namespace
+{
+
+int string_out(const std::string &s, char *buf, int buf_size)
+{
+ int need = static_cast(s.size()) + 1;
+ if (buf && buf_size > 0) {
+ int n = std::min(static_cast(s.size()), buf_size - 1);
+ memcpy(buf, s.data(), n);
+ buf[n] = '\0';
+ }
+ return need;
+}
+
+olive::core::AudioParams to_native_params(int sample_rate,
+ uint64_t channel_layout,
+ int sample_format)
+{
+ return olive::core::AudioParams(
+ sample_rate, channel_layout,
+ static_cast(sample_format));
+}
+
+olive::Decoder::CodecStream to_native_stream(const char *source_filename,
+ int stream_index)
+{
+ return olive::Decoder::CodecStream(
+ source_filename ? source_filename : "", stream_index, nullptr);
+}
+
+bool conform_args_valid(const char *cache_path, const char *source_filename)
+{
+ return cache_path && *cache_path && source_filename && *source_filename;
+}
+
+} // namespace
+
+int oakcodec_conform_create_instance(void)
+{
+ olive::ConformManager::create_instance();
+ return OAKCODEC_OK;
+}
+
+int oakcodec_conform_destroy_instance(void)
+{
+ olive::ConformManager::destroy_instance();
+ return OAKCODEC_OK;
+}
+
+int oakcodec_conform_get_state(const char *cache_path,
+ const char *source_filename, int stream_index,
+ int sample_rate, uint64_t channel_layout,
+ int sample_format, int wait)
+{
+ if (!conform_args_valid(cache_path, source_filename))
+ return OAKCODEC_E_INVALID;
+ if (!olive::ConformManager::instance())
+ return OAKCODEC_E_STATE;
+
+ olive::ConformManager::Conform c =
+ olive::ConformManager::instance()->get_conform_state(
+ cache_path, to_native_stream(source_filename, stream_index),
+ to_native_params(sample_rate, channel_layout, sample_format),
+ wait != 0);
+
+ switch (c.state) {
+ case olive::ConformManager::k_conform_exists:
+ return OAKCODEC_CONFORM_EXISTS;
+ case olive::ConformManager::k_conform_generating:
+ return OAKCODEC_CONFORM_GENERATING;
+ case olive::ConformManager::k_conform_unavailable:
+ default:
+ return OAKCODEC_CONFORM_UNAVAILABLE;
+ }
+}
+
+int oakcodec_conform_filename_count(const char *cache_path,
+ const char *source_filename,
+ int stream_index, int sample_rate,
+ uint64_t channel_layout, int sample_format)
+{
+ if (!conform_args_valid(cache_path, source_filename))
+ return 0;
+
+ // Pure path computation: never submits work.
+ return static_cast(olive::ConformManager::get_conformed_filename(
+ cache_path,
+ to_native_stream(source_filename, stream_index),
+ to_native_params(sample_rate, channel_layout,
+ sample_format))
+ .size());
+}
+
+int oakcodec_conform_filename_at(const char *cache_path,
+ const char *source_filename,
+ int stream_index, int sample_rate,
+ uint64_t channel_layout, int sample_format,
+ int index, char *buf, int buf_size)
+{
+ if (!conform_args_valid(cache_path, source_filename))
+ return OAKCODEC_E_INVALID;
+
+ std::vector filenames =
+ olive::ConformManager::get_conformed_filename(
+ cache_path, to_native_stream(source_filename, stream_index),
+ to_native_params(sample_rate, channel_layout, sample_format));
+
+ if (index < 0 || index >= static_cast(filenames.size()))
+ return OAKCODEC_E_NOT_FOUND;
+ return string_out(filenames[index], buf, buf_size);
+}
diff --git a/src/codec/c_api/decoder.cpp b/src/codec/c_api/decoder.cpp
new file mode 100644
index 000000000..a4c1064c6
--- /dev/null
+++ b/src/codec/c_api/decoder.cpp
@@ -0,0 +1,388 @@
+/***
+
+ 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 .
+
+***/
+
+#include "codec/decoder.h"
+
+#include
+#include
+#include
+
+#include
+
+#include "common/loopmode.h"
+#include "decoder.h"
+#include "footagedescription.h"
+#include "frame.h"
+#include "refcounted.h"
+
+namespace
+{
+
+struct ProbeBox {
+ std::string decoder_name;
+ olive::FootageDescription desc;
+};
+
+struct DecoderBox {
+ olive::DecoderPtr decoder;
+ std::string last_error;
+ std::string open_filename;
+ int open_stream = -1;
+ bool open = false;
+};
+
+ProbeBox *probe_box(void *ctx)
+{
+ return oakcodec::handle_impl(ctx);
+}
+
+DecoderBox *decoder_box(void *ctx)
+{
+ return oakcodec::handle_impl(ctx);
+}
+
+thread_local std::string g_probe_error;
+
+int string_out(const std::string &s, char *buf, int buf_size)
+{
+ int need = static_cast(s.size()) + 1;
+ if (buf && buf_size > 0) {
+ int n = std::min(static_cast(s.size()), buf_size - 1);
+ memcpy(buf, s.data(), n);
+ buf[n] = '\0';
+ }
+ return need;
+}
+
+bool file_exists(const char *filename)
+{
+ struct stat st;
+ return filename && stat(filename, &st) == 0;
+}
+
+/**
+ * @brief Probe with every available decoder, returning the first valid
+ * description (and filling `name`).
+ */
+bool probe_with_any_decoder(const char *filename, std::string *name,
+ olive::FootageDescription *out)
+{
+ for (const olive::DecoderPtr &d :
+ olive::Decoder::receive_list_of_all_decoders()) {
+ olive::FootageDescription desc = d->probe(filename, nullptr);
+ if (desc.is_valid()) {
+ *name = desc.decoder();
+ *out = desc;
+ return true;
+ }
+ }
+ return false;
+}
+
+void fill_video_info(const OakVideoParams &vp,
+ oakcodec_video_stream_info *out)
+{
+ *out = {};
+
+ oakcommon_videoparams_get_stream_index(vp, &out->stream_index);
+ oakcommon_videoparams_get_width(vp, &out->width);
+ oakcommon_videoparams_get_height(vp, &out->height);
+
+ int fr_num = 0, fr_den = 0;
+ oakcommon_videoparams_get_frame_rate(vp, &fr_num, &fr_den);
+ out->frame_rate_num = fr_num;
+ out->frame_rate_den = fr_den;
+
+ int tb_num = 0, tb_den = 0;
+ oakcommon_videoparams_get_time_base(vp, &tb_num, &tb_den);
+ out->time_base_num = tb_num;
+ out->time_base_den = tb_den;
+
+ oakcommon_videoparams_get_duration(vp, &out->duration_ts);
+ oakcommon_videoparams_get_format(vp, &out->format);
+ oakcommon_videoparams_get_channel_count(vp, &out->channel_count);
+ oakcommon_videoparams_get_color_primaries(vp, &out->color_primaries);
+ oakcommon_videoparams_get_color_transfer(vp, &out->color_trc);
+
+ int interlacing = OAKCOMMON_VIDEO_INTERLACE_NONE;
+ oakcommon_videoparams_get_interlacing(vp, &interlacing);
+ out->interlaced = interlacing != OAKCOMMON_VIDEO_INTERLACE_NONE;
+}
+
+void fill_audio_info(const olive::AudioParams &ap,
+ oakcodec_audio_stream_info *out)
+{
+ *out = {};
+ out->stream_index = ap.stream_index();
+ out->sample_rate = ap.sample_rate();
+ out->channel_layout = ap.channel_layout();
+ out->channel_count = ap.channel_count();
+
+ olive::Rational tb = ap.time_base();
+ out->time_base_num = tb.numerator();
+ out->time_base_den = tb.denominator();
+ // AudioParams carries no duration; duration_ts stays 0 (unknown).
+}
+
+} // namespace
+
+/* ---- Probe ---------------------------------------------------------------- */
+
+OakDecoder oakcodec_decoder_probe(const char *filename)
+{
+ if (!filename || !*filename) {
+ g_probe_error = "no filename given";
+ return OakDecoder{};
+ }
+ if (!file_exists(filename)) {
+ g_probe_error = std::string("file not found: ") + filename;
+ return OakDecoder{};
+ }
+
+ OakDecoder h = oakcodec::make_handle_in_place();
+ ProbeBox *b = probe_box(h.ctx);
+ if (!b) {
+ g_probe_error = "out of memory";
+ return OakDecoder{};
+ }
+
+ if (!probe_with_any_decoder(filename, &b->decoder_name, &b->desc)) {
+ g_probe_error =
+ std::string("no decoder recognizes this file: ") + filename;
+ oakcodec_decoder_free(&h);
+ return OakDecoder{};
+ }
+
+ g_probe_error.clear();
+ return h;
+}
+
+int oakcodec_probe_last_error(char *buf, int buf_size)
+{
+ return string_out(g_probe_error, buf, buf_size);
+}
+
+int oakcodec_decoder_probe_decoder_name(OakDecoder probe, char *buf,
+ int buf_size)
+{
+ ProbeBox *b = probe_box(probe.ctx);
+ if (!b)
+ return OAKCODEC_E_INVALID;
+ return string_out(b->decoder_name, buf, buf_size);
+}
+
+int oakcodec_decoder_probe_video_stream_count(OakDecoder probe)
+{
+ ProbeBox *b = probe_box(probe.ctx);
+ if (!b)
+ return 0;
+ return static_cast(b->desc.get_video_streams().size());
+}
+
+int oakcodec_decoder_probe_audio_stream_count(OakDecoder probe)
+{
+ ProbeBox *b = probe_box(probe.ctx);
+ if (!b)
+ return 0;
+ return static_cast(b->desc.get_audio_streams().size());
+}
+
+int oakcodec_decoder_probe_subtitle_stream_count(OakDecoder probe)
+{
+ ProbeBox *b = probe_box(probe.ctx);
+ if (!b)
+ return 0;
+ return static_cast(b->desc.get_subtitle_streams().size());
+}
+
+int oakcodec_decoder_probe_get_video_stream(OakDecoder probe, int index,
+ oakcodec_video_stream_info *out)
+{
+ ProbeBox *b = probe_box(probe.ctx);
+ if (!b || !out)
+ return OAKCODEC_E_INVALID;
+ const auto &streams = b->desc.get_video_streams();
+ if (index < 0 || index >= static_cast(streams.size()))
+ return OAKCODEC_E_NOT_FOUND;
+ fill_video_info(streams[static_cast(index)], out);
+ return OAKCODEC_OK;
+}
+
+int oakcodec_decoder_probe_get_audio_stream(OakDecoder probe, int index,
+ oakcodec_audio_stream_info *out)
+{
+ ProbeBox *b = probe_box(probe.ctx);
+ if (!b || !out)
+ return OAKCODEC_E_INVALID;
+ const auto &streams = b->desc.get_audio_streams();
+ if (index < 0 || index >= static_cast(streams.size()))
+ return OAKCODEC_E_NOT_FOUND;
+ fill_audio_info(streams[static_cast(index)], out);
+ return OAKCODEC_OK;
+}
+
+/* ---- Decode session -------------------------------------------------------- */
+
+OakDecoder oakcodec_decoder_init(void)
+{
+ return oakcodec::make_handle_in_place();
+}
+
+void oakcodec_decoder_free(OakDecoder *decoder)
+{
+ oakcodec::free_handle(decoder);
+}
+
+int oakcodec_decoder_open(OakDecoder decoder, const char *filename,
+ int stream_index)
+{
+ DecoderBox *b = decoder_box(decoder.ctx);
+ if (!b || !filename || stream_index < 0)
+ return OAKCODEC_E_INVALID;
+
+ if (b->open && b->decoder) {
+ if (b->open_filename == filename && b->open_stream == stream_index)
+ return OAKCODEC_OK; // already open on this stream
+ b->decoder->close();
+ b->open = false;
+ }
+
+ if (!file_exists(filename)) {
+ b->last_error = std::string("file not found: ") + filename;
+ return OAKCODEC_E_NOT_FOUND;
+ }
+
+ std::string decoder_name;
+ olive::FootageDescription desc;
+ if (!probe_with_any_decoder(filename, &decoder_name, &desc)) {
+ b->last_error =
+ std::string("no decoder recognizes this file: ") + filename;
+ return OAKCODEC_E_FAILED;
+ }
+
+ b->decoder = olive::Decoder::create_from_id(decoder_name);
+ if (!b->decoder) {
+ b->last_error = std::string("failed to create decoder: ") + decoder_name;
+ return OAKCODEC_E_FAILED;
+ }
+
+ if (!b->decoder->open(
+ olive::Decoder::CodecStream(filename, stream_index, nullptr))) {
+ b->last_error = "failed to open stream";
+ b->decoder.reset();
+ return OAKCODEC_E_FAILED;
+ }
+
+ b->last_error.clear();
+ b->open_filename = filename;
+ b->open_stream = stream_index;
+ b->open = true;
+ return OAKCODEC_OK;
+}
+
+int oakcodec_decoder_close(OakDecoder decoder)
+{
+ DecoderBox *b = decoder_box(decoder.ctx);
+ if (!b)
+ return OAKCODEC_E_INVALID;
+ if (b->open && b->decoder) {
+ b->decoder->close();
+ }
+ b->open = false;
+ return OAKCODEC_OK;
+}
+
+int oakcodec_decoder_is_open(OakDecoder decoder)
+{
+ DecoderBox *b = decoder_box(decoder.ctx);
+ return (b && b->open) ? 1 : 0;
+}
+
+OakFrame oakcodec_decoder_decode_video(OakDecoder decoder, int numerator,
+ int denominator)
+{
+ DecoderBox *b = decoder_box(decoder.ctx);
+ if (!b || !b->open || !b->decoder)
+ return OakFrame{};
+
+ olive::Decoder::RetrieveVideoParams p;
+ p.time = olive::Rational(numerator, denominator);
+
+ olive::FramePtr frame = b->decoder->retrieve_video_frame(p);
+ if (!frame) {
+ b->last_error = "failed to decode video frame";
+ return OakFrame{};
+ }
+
+ return oakcodec::make_handle(std::move(frame));
+}
+
+int oakcodec_decoder_decode_audio(OakDecoder decoder, int in_num, int in_den,
+ int out_num, int out_den, int sample_rate,
+ uint64_t channel_layout, float *buf,
+ int buf_frames)
+{
+ DecoderBox *b = decoder_box(decoder.ctx);
+ if (!b || (!buf && buf_frames > 0) || buf_frames < 0)
+ return OAKCODEC_E_INVALID;
+ if (!b->open || !b->decoder)
+ return OAKCODEC_E_STATE;
+
+ olive::AudioParams params(sample_rate, channel_layout,
+ olive::core::SampleFormat::f32);
+ olive::TimeRange range(olive::Rational(in_num, in_den),
+ olive::Rational(out_num, out_den));
+
+ olive::SampleBuffer samples;
+ olive::Decoder::RetrieveAudioStatus status = b->decoder->retrieve_audio(
+ samples, range, params, std::string(), OAKCOMMON_LOOP_MODE_OFF,
+ olive::RenderMode::k_offline);
+
+ if (status == olive::Decoder::k_waiting_for_conform) {
+ // Interim state (pre-M8): conform tasks require a task registrar.
+ b->last_error =
+ "audio requires a conform, but no task submit callback is "
+ "registered (see oakcodec_set_task_submit_cb)";
+ return OAKCODEC_E_STATE;
+ }
+ if (status != olive::Decoder::k_ok || !samples.is_allocated()) {
+ b->last_error = "failed to decode audio";
+ return OAKCODEC_E_FAILED;
+ }
+
+ int channels = samples.channel_count();
+ int available = static_cast(samples.sample_count());
+ int frames = std::min(available, buf_frames);
+ for (int c = 0; c < channels; c++) {
+ const float *src = samples.data(c);
+ for (int i = 0; i < frames; i++) {
+ buf[static_cast(i) * channels + c] = src[i];
+ }
+ }
+ return frames;
+}
+
+int oakcodec_decoder_last_error(OakDecoder decoder, char *buf, int buf_size)
+{
+ DecoderBox *b = decoder_box(decoder.ctx);
+ if (!b)
+ return string_out("", buf, buf_size);
+ return string_out(b->last_error, buf, buf_size);
+}
diff --git a/src/codec/c_api/encoder.cpp b/src/codec/c_api/encoder.cpp
new file mode 100644
index 000000000..e81a708f6
--- /dev/null
+++ b/src/codec/c_api/encoder.cpp
@@ -0,0 +1,272 @@
+/***
+
+ 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 .
+
+***/
+
+#include "codec/encoder.h"
+
+#include
+#include
+#include
+#include
+
+#include "common/colortransform.h"
+#include "common/videoparams.h"
+#include "encoder.h"
+#include "frame.h"
+#include "refcounted.h"
+
+namespace
+{
+
+constexpr int k_rgba_channel_count = 4;
+
+struct EncoderBox {
+ std::unique_ptr encoder;
+ olive::EncodingParams params;
+ bool open = false;
+ bool flushed = false;
+};
+
+EncoderBox *box(void *ctx)
+{
+ return oakcodec::handle_impl(ctx);
+}
+
+int string_out(const std::string &s, char *buf, int buf_size)
+{
+ int need = static_cast(s.size()) + 1;
+ if (buf && buf_size > 0) {
+ int n = std::min(static_cast(s.size()), buf_size - 1);
+ memcpy(buf, s.data(), n);
+ buf[n] = '\0';
+ }
+ return need;
+}
+
+olive::EncodingParams to_native(const oakcodec_encoding_params *p)
+{
+ using namespace olive;
+
+ EncodingParams n;
+ n.set_filename(p->filename);
+ n.set_format(static_cast(p->format));
+
+ if (p->video_enabled) {
+ OakVideoParams vp = oakcommon_videoparams_init_with_time_base(
+ p->video_width, p->video_height, p->video_time_base_num,
+ p->video_time_base_den, p->video_pixel_format,
+ k_rgba_channel_count, p->video_pixel_aspect_num,
+ p->video_pixel_aspect_den, p->video_interlacing, 1);
+ n.enable_video(vp, static_cast(p->video_codec));
+ oakcommon_videoparams_free(&vp);
+ n.set_video_bit_rate(p->video_bit_rate);
+ n.set_video_min_bit_rate(p->video_min_bit_rate);
+ n.set_video_max_bit_rate(p->video_max_bit_rate);
+ n.set_video_buffer_size(p->video_buffer_size);
+ n.set_video_threads(p->video_threads);
+ n.set_video_pix_fmt(p->video_pix_fmt);
+ n.set_video_is_image_sequence(p->video_is_image_sequence != 0);
+ n.set_video_scaling_method(
+ static_cast(
+ p->video_scaling_method));
+ }
+
+ if (p->audio_enabled) {
+ AudioParams ap(p->audio_sample_rate, p->audio_channel_layout,
+ static_cast(
+ p->audio_sample_format));
+ n.enable_audio(ap, static_cast(p->audio_codec));
+ n.set_audio_bit_rate(p->audio_bit_rate);
+ }
+
+ if (p->subtitles_enabled) {
+ if (p->subtitles_are_sidecar) {
+ n.enable_sidecar_subtitles(
+ static_cast(
+ p->subtitles_sidecar_format),
+ static_cast(p->subtitles_codec));
+ } else {
+ n.enable_subtitles(
+ static_cast(p->subtitles_codec));
+ }
+ }
+
+ if (p->color_transform_output[0] != '\0') {
+ OakColorTransform ct =
+ oakcommon_colortransform_init_output(p->color_transform_output);
+ n.set_color_transform(ct);
+ oakcommon_colortransform_free(&ct);
+ }
+
+ if (p->export_length_den != 0) {
+ n.set_export_length(
+ Rational(p->export_length_num, p->export_length_den));
+ }
+
+ return n;
+}
+
+} // namespace
+
+OakEncoder oakcodec_encoder_init(const oakcodec_encoding_params *params)
+{
+ if (!params)
+ return OakEncoder{};
+
+ OakEncoder h = oakcodec::make_handle_in_place();
+ EncoderBox *b = box(h.ctx);
+ if (!b)
+ return OakEncoder{};
+
+ try {
+ b->params = to_native(params);
+ } catch (...) {
+ oakcodec_encoder_free(&h);
+ return OakEncoder{};
+ }
+
+ if (!b->params.is_valid()) {
+ oakcodec_encoder_free(&h);
+ return OakEncoder{};
+ }
+
+ return h;
+}
+
+void oakcodec_encoder_free(OakEncoder *encoder)
+{
+ oakcodec::free_handle(encoder);
+}
+
+int oakcodec_encoder_set_video_option(OakEncoder encoder, const char *key,
+ const char *value)
+{
+ EncoderBox *b = box(encoder.ctx);
+ if (!b || !key)
+ return OAKCODEC_E_INVALID;
+ if (b->open)
+ return OAKCODEC_E_STATE;
+ b->params.set_video_option(key, value ? value : "");
+ return OAKCODEC_OK;
+}
+
+int oakcodec_encoder_open(OakEncoder encoder)
+{
+ EncoderBox *b = box(encoder.ctx);
+ if (!b)
+ return OAKCODEC_E_INVALID;
+ if (b->open)
+ return OAKCODEC_E_STATE;
+
+ b->encoder.reset(olive::Encoder::create_from_params(b->params));
+ if (!b->encoder)
+ return OAKCODEC_E_FAILED;
+
+ if (!b->encoder->open()) {
+ return OAKCODEC_E_FAILED;
+ }
+
+ b->open = true;
+ return OAKCODEC_OK;
+}
+
+int oakcodec_encoder_write_video(OakEncoder encoder, OakFrame frame)
+{
+ EncoderBox *b = box(encoder.ctx);
+ if (!b || !frame.ctx)
+ return OAKCODEC_E_INVALID;
+ if (!b->open || b->flushed || !b->encoder)
+ return OAKCODEC_E_STATE;
+
+ // OakFrame boxes hold an olive::FramePtr (see c_api/frame.cpp).
+ auto *fp = oakcodec::handle_impl(frame.ctx);
+ if (!fp || !*fp)
+ return OAKCODEC_E_INVALID;
+ olive::Frame *f = fp->get();
+
+ return b->encoder->write_frame(*fp, f->timestamp()) ? OAKCODEC_OK
+ : OAKCODEC_E_FAILED;
+}
+
+int oakcodec_encoder_write_audio(OakEncoder encoder, const float *samples,
+ int frame_count)
+{
+ EncoderBox *b = box(encoder.ctx);
+ if (!b || (!samples && frame_count > 0) || frame_count < 0)
+ return OAKCODEC_E_INVALID;
+ if (!b->open || b->flushed || !b->encoder)
+ return OAKCODEC_E_STATE;
+
+ const olive::AudioParams &ap = b->params.audio_params();
+ int channels = ap.channel_count();
+ if (channels <= 0)
+ return OAKCODEC_E_STATE;
+
+ // Deinterleave into a planar SampleBuffer.
+ olive::SampleBuffer buf(ap, static_cast(frame_count));
+ buf.allocate();
+ std::vector channel_data(static_cast(frame_count));
+ for (int c = 0; c < channels; c++) {
+ for (int i = 0; i < frame_count; i++) {
+ channel_data[i] = samples[static_cast(i) * channels + c];
+ }
+ buf.set(c, channel_data.data(),
+ static_cast(frame_count));
+ }
+
+ return b->encoder->write_audio(buf) ? OAKCODEC_OK : OAKCODEC_E_FAILED;
+}
+
+int oakcodec_encoder_write_subtitle(OakEncoder encoder, const char *text,
+ double in_seconds, double out_seconds)
+{
+ EncoderBox *b = box(encoder.ctx);
+ if (!b || !text)
+ return OAKCODEC_E_INVALID;
+ if (!b->open || b->flushed || !b->encoder)
+ return OAKCODEC_E_STATE;
+
+ return b->encoder->write_subtitle(text, in_seconds, out_seconds)
+ ? OAKCODEC_OK
+ : OAKCODEC_E_FAILED;
+}
+
+int oakcodec_encoder_flush(OakEncoder encoder)
+{
+ EncoderBox *b = box(encoder.ctx);
+ if (!b)
+ return OAKCODEC_E_INVALID;
+ if (!b->open)
+ return OAKCODEC_E_STATE;
+ if (b->flushed)
+ return OAKCODEC_OK;
+
+ b->encoder->close();
+ b->flushed = true;
+ return OAKCODEC_OK;
+}
+
+int oakcodec_encoder_last_error(OakEncoder encoder, char *buf, int buf_size)
+{
+ EncoderBox *b = box(encoder.ctx);
+ if (!b)
+ return string_out("", buf, buf_size);
+ return string_out(b->encoder ? b->encoder->get_error() : std::string(),
+ buf, buf_size);
+}
diff --git a/src/codec/c_api/frame.cpp b/src/codec/c_api/frame.cpp
new file mode 100644
index 000000000..2fd27c18f
--- /dev/null
+++ b/src/codec/c_api/frame.cpp
@@ -0,0 +1,198 @@
+/***
+
+ 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 .
+
+***/
+
+#include "codec/frame.h"
+
+#include
+
+#include "frame.h"
+#include "refcounted.h"
+
+namespace
+{
+
+// Every OakFrame box holds an olive::FramePtr: frames created here own a
+// fresh olive::Frame, decoder-produced frames alias the decoder's
+// shared_ptr. Unifying the box type keeps the addref/release thunks and
+// the impl recovery symmetric across all OakFrame handles.
+olive::Frame *impl(void *ctx)
+{
+ auto *p = oakcodec::handle_impl(ctx);
+ return p ? p->get() : nullptr;
+}
+
+} // namespace
+
+namespace oakcodec
+{
+
+std::atomic g_alive_count{0};
+
+void alive_inc()
+{
+ g_alive_count.fetch_add(1, std::memory_order_relaxed);
+}
+
+void alive_dec()
+{
+ g_alive_count.fetch_sub(1, std::memory_order_relaxed);
+}
+
+} // namespace oakcodec
+
+int oakcodec_debug_alive_count(void)
+{
+ return oakcodec::g_alive_count.load(std::memory_order_relaxed);
+}
+
+OakFrame oakcodec_frame_init(void)
+{
+ return oakcodec::make_handle(olive::Frame::create());
+}
+
+OakFrame oakcodec_frame_init_with_params(OakVideoParams params)
+{
+ OakFrame h = oakcodec_frame_init();
+ if (h.ctx) {
+ impl(h.ctx)->set_video_params(params);
+ }
+ return h;
+}
+
+void oakcodec_frame_free(OakFrame *frame)
+{
+ oakcodec::free_handle(frame);
+}
+
+int oakcodec_frame_get_params(OakFrame frame, OakVideoParams *out)
+{
+ if (!frame.ctx || !out)
+ return OAKCODEC_E_INVALID;
+ *out = impl(frame.ctx)->video_params();
+ return OAKCODEC_OK;
+}
+
+int oakcodec_frame_set_params(OakFrame frame, OakVideoParams params)
+{
+ if (!frame.ctx)
+ return OAKCODEC_E_INVALID;
+ impl(frame.ctx)->set_video_params(params);
+ return OAKCODEC_OK;
+}
+
+int oakcodec_frame_allocate(OakFrame frame)
+{
+ if (!frame.ctx)
+ return OAKCODEC_E_INVALID;
+ if (!impl(frame.ctx)->allocate())
+ return OAKCODEC_E_STATE;
+ return OAKCODEC_OK;
+}
+
+int oakcodec_frame_is_allocated(OakFrame frame)
+{
+ if (!frame.ctx)
+ return 0;
+ return impl(frame.ctx)->is_allocated() ? 1 : 0;
+}
+
+void *oakcodec_frame_data(OakFrame frame)
+{
+ if (!frame.ctx)
+ return nullptr;
+ return impl(frame.ctx)->data();
+}
+
+const void *oakcodec_frame_const_data(OakFrame frame)
+{
+ if (!frame.ctx)
+ return nullptr;
+ return impl(frame.ctx)->const_data();
+}
+
+int oakcodec_frame_allocated_size(OakFrame frame)
+{
+ if (!frame.ctx)
+ return 0;
+ return impl(frame.ctx)->allocated_size();
+}
+
+int oakcodec_frame_linesize_bytes(OakFrame frame)
+{
+ if (!frame.ctx)
+ return 0;
+ return impl(frame.ctx)->linesize_bytes();
+}
+
+int oakcodec_frame_linesize_pixels(OakFrame frame)
+{
+ if (!frame.ctx)
+ return 0;
+ return impl(frame.ctx)->linesize_pixels();
+}
+
+int oakcodec_frame_width(OakFrame frame)
+{
+ if (!frame.ctx)
+ return 0;
+ return impl(frame.ctx)->width();
+}
+
+int oakcodec_frame_height(OakFrame frame)
+{
+ if (!frame.ctx)
+ return 0;
+ return impl(frame.ctx)->height();
+}
+
+int oakcodec_frame_format(OakFrame frame)
+{
+ if (!frame.ctx)
+ return OAKCOMMON_PIXEL_FORMAT_INVALID;
+ return impl(frame.ctx)->format();
+}
+
+int oakcodec_frame_channel_count(OakFrame frame)
+{
+ if (!frame.ctx)
+ return 0;
+ return impl(frame.ctx)->channel_count();
+}
+
+int oakcodec_frame_get_timestamp(OakFrame frame, int *numerator,
+ int *denominator)
+{
+ if (!frame.ctx || !numerator || !denominator)
+ return OAKCODEC_E_INVALID;
+ const olive::core::Rational &ts = impl(frame.ctx)->timestamp();
+ *numerator = ts.numerator();
+ *denominator = ts.denominator();
+ return OAKCODEC_OK;
+}
+
+int oakcodec_frame_set_timestamp(OakFrame frame, int numerator,
+ int denominator)
+{
+ if (!frame.ctx)
+ return OAKCODEC_E_INVALID;
+ impl(frame.ctx)->set_timestamp(
+ olive::core::Rational(numerator, denominator));
+ return OAKCODEC_OK;
+}
diff --git a/src/codec/c_api/proxy.cpp b/src/codec/c_api/proxy.cpp
new file mode 100644
index 000000000..5c1b3a393
--- /dev/null
+++ b/src/codec/c_api/proxy.cpp
@@ -0,0 +1,170 @@
+/***
+
+ 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 .
+
+***/
+
+#include "codec/proxy.h"
+
+#include
+#include
+#include
+#include
+
+#include "proxymanager.h"
+
+namespace
+{
+
+int string_out(const std::string &s, char *buf, int buf_size)
+{
+ int need = static_cast(s.size()) + 1;
+ if (buf && buf_size > 0) {
+ int n = std::min(static_cast(s.size()), buf_size - 1);
+ memcpy(buf, s.data(), n);
+ buf[n] = '\0';
+ }
+ return need;
+}
+
+olive::ProxyManager::ProxyParams to_native(const oakcodec_proxy_params *p)
+{
+ olive::ProxyManager::ProxyParams n;
+ if (p) {
+ n.width = p->width;
+ n.height = p->height;
+ n.divider = p->divider;
+ n.version = p->version;
+ n.crf = p->crf;
+ n.include_audio = p->include_audio != 0;
+ n.extension = p->extension;
+ n.preset = p->preset;
+ }
+ return n;
+}
+
+} // namespace
+
+int oakcodec_proxy_create_instance(void)
+{
+ olive::ProxyManager::create_instance();
+ return OAKCODEC_OK;
+}
+
+int oakcodec_proxy_destroy_instance(void)
+{
+ olive::ProxyManager::destroy_instance();
+ return OAKCODEC_OK;
+}
+
+int oakcodec_proxy_params_default(oakcodec_proxy_params *out)
+{
+ if (!out)
+ return OAKCODEC_E_INVALID;
+ olive::ProxyManager::ProxyParams n =
+ olive::ProxyManager::proxy_params_from_config();
+ *out = {};
+ 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 ? 1 : 0;
+ snprintf(out->extension, sizeof(out->extension), "%s",
+ n.extension.c_str());
+ snprintf(out->preset, sizeof(out->preset), "%s", n.preset.c_str());
+ return OAKCODEC_OK;
+}
+
+int oakcodec_proxy_get_state(const char *proxy_filename)
+{
+ if (!proxy_filename || !*proxy_filename)
+ return OAKCODEC_PROXY_STATE_MISSING;
+ return static_cast(
+ olive::ProxyManager::get_proxy_state(proxy_filename));
+}
+
+int oakcodec_proxy_state_to_string(int state, char *buf, int buf_size)
+{
+ if (state < OAKCODEC_PROXY_STATE_MISSING ||
+ state > OAKCODEC_PROXY_STATE_FAILED)
+ return OAKCODEC_E_INVALID;
+ return string_out(olive::ProxyManager::proxy_state_to_string(
+ static_cast(state)),
+ buf, buf_size);
+}
+
+int oakcodec_proxy_get_proxy_directory(const char *cache_path, char *buf,
+ int buf_size)
+{
+ if (!cache_path)
+ return OAKCODEC_E_INVALID;
+ return string_out(olive::ProxyManager::get_proxy_directory(cache_path),
+ buf, buf_size);
+}
+
+int oakcodec_proxy_get_proxy_filename(const char *cache_path,
+ const char *source_filename,
+ int stream_index,
+ const oakcodec_proxy_params *params,
+ char *buf, int buf_size)
+{
+ if (!cache_path || !source_filename)
+ return OAKCODEC_E_INVALID;
+ return string_out(
+ olive::ProxyManager::get_proxy_filename(
+ cache_path, source_filename, stream_index, to_native(params)),
+ buf, buf_size);
+}
+
+int oakcodec_proxy_get_working_filename(const char *proxy_filename,
+ char *buf, int buf_size)
+{
+ if (!proxy_filename)
+ return OAKCODEC_E_INVALID;
+ return string_out(
+ olive::ProxyManager::get_working_proxy_filename(proxy_filename),
+ buf, buf_size);
+}
+
+int oakcodec_proxy_get_or_start(const char *cache_path,
+ const char *source_filename, int stream_index,
+ const oakcodec_proxy_params *params,
+ oakcodec_proxy_result *out)
+{
+ if (!cache_path || !source_filename || !out)
+ return OAKCODEC_E_INVALID;
+ if (!olive::ProxyManager::instance())
+ return OAKCODEC_E_STATE;
+
+ olive::ProxyManager::Proxy p =
+ olive::ProxyManager::instance()->get_or_start_proxy(
+ cache_path, source_filename, stream_index, to_native(params));
+
+ out->state = static_cast(p.state);
+ snprintf(out->filename, sizeof(out->filename), "%s",
+ p.filename.c_str());
+ return OAKCODEC_OK;
+}
+
+int oakcodec_proxy_find_ffmpeg(const char *configured_path, char *buf,
+ int buf_size)
+{
+ return string_out(olive::ProxyManager::find_f_fmpeg_executable(
+ configured_path ? configured_path : ""),
+ buf, buf_size);
+}
diff --git a/src/codec/c_api/refcounted.h b/src/codec/c_api/refcounted.h
new file mode 100644
index 000000000..815aa547a
--- /dev/null
+++ b/src/codec/c_api/refcounted.h
@@ -0,0 +1,124 @@
+/***
+
+ 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 .
+
+***/
+
+#ifndef OAKCODEC_C_API_REFCOUNTED_H
+#define OAKCODEC_C_API_REFCOUNTED_H
+
+#include
+#include
+#include
+#include
+
+#include "codec/error.h"
+
+namespace oakcodec
+{
+
+/**
+ * @brief Heap box behind every handle's ctx pointer.
+ *
+ * Same pattern as oakcommon's c_api/refcounted.h: holds the wrapped
+ * object plus its atomic reference count. addref and release are emitted
+ * per boxed type so that the function pointers stored in a handle always
+ * run code from the DLL that created the object. Every box also
+ * participates in the oakcodec_debug_alive_count() ledger.
+ */
+template struct RefCounted {
+ T impl;
+ std::atomic refs;
+
+ template
+ explicit RefCounted(Args &&...args)
+ : impl(std::forward(args)...)
+ , refs(1)
+ {
+ }
+};
+
+template void ref_counted_addref(void *ctx)
+{
+ auto *box = static_cast *>(ctx);
+ if (box)
+ box->refs.fetch_add(1, std::memory_order_relaxed);
+}
+
+void alive_inc();
+void alive_dec();
+
+template void ref_counted_release(void *ctx)
+{
+ auto *box = static_cast *>(ctx);
+ if (box && box->refs.fetch_sub(1, std::memory_order_acq_rel) == 1) {
+ delete box;
+ alive_dec();
+ }
+}
+
+/**
+ * @brief Build a by-value handle owning a freshly boxed object (count 1).
+ *
+ * On allocation failure the returned handle has ctx == NULL (all C API
+ * functions treat that as OAKCODEC_E_INVALID and free() as a no-op).
+ */
+template
+Handle make_handle_in_place(Args &&...args)
+{
+ Handle h = {};
+ try {
+ h.ctx = new RefCounted(std::forward(args)...);
+ alive_inc();
+ } catch (...) {
+ h.ctx = nullptr;
+ }
+ h.addref = &ref_counted_addref;
+ h.release = &ref_counted_release;
+ h.abi_version = OAKCODEC_ABI_VERSION;
+ return h;
+}
+
+template Handle make_handle(T &&value)
+{
+ return make_handle_in_place::type>(
+ std::forward(value));
+}
+
+/**
+ * @brief Recover the boxed object from a handle ctx (NULL-safe).
+ */
+template T *handle_impl(void *ctx)
+{
+ auto *box = static_cast *>(ctx);
+ return box ? &box->impl : nullptr;
+}
+
+/**
+ * @brief Shared free() body: release the ctx, no-op on NULL/empty handle.
+ */
+template void free_handle(Handle *h)
+{
+ if (!h || !h->ctx || !h->release)
+ return;
+ h->release(h->ctx);
+ h->ctx = nullptr;
+}
+
+} // namespace oakcodec
+
+#endif // OAKCODEC_C_API_REFCOUNTED_H
diff --git a/src/codec/src/CMakeLists.txt b/src/codec/src/CMakeLists.txt
new file mode 100644
index 000000000..b3a29687f
--- /dev/null
+++ b/src/codec/src/CMakeLists.txt
@@ -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 .
+
+add_library(oakcodec SHARED
+ conformmanager.cpp
+ conformmanager.h
+ decoder.cpp
+ decoder.h
+ encoder.cpp
+ encoder.h
+ exportcodec.cpp
+ exportcodec.h
+ exportformat.cpp
+ exportformat.h
+ footagedescription.h
+ frame.cpp
+ frame.h
+ framemanager.cpp
+ framemanager.h
+ oiioframebridge.cpp
+ oiioframebridge.h
+ planarfiledevice.cpp
+ planarfiledevice.h
+ proxymanager.cpp
+ proxymanager.h
+ taskcallbacks.cpp
+ taskcallbacks.h
+ timecodemetadata.cpp
+ timecodemetadata.h
+)
+add_subdirectory(ffmpeg)
+add_subdirectory(oiio)
+
+# In a full-tree build the repo root is CMAKE_SOURCE_DIR; a standalone
+# build (see src/codec/standalone) sets OAK_REPO_ROOT explicitly.
+if(NOT DEFINED OAK_REPO_ROOT)
+ set(OAK_REPO_ROOT ${CMAKE_SOURCE_DIR})
+endif()
+
+target_include_directories(oakcodec PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${OAK_REPO_ROOT}/include
+ ${OAK_REPO_ROOT}/core/include
+ ${OAK_REPO_ROOT}/ffmpeg_bridge/include
+ ${OAK_REPO_ROOT}/third_party/openfx/include
+ ${OIIO_INCLUDE_DIRS}
+ ${OCIO_INCLUDE_DIRS}
+)
+
+# 01 ยง1 rule 5: only the OAKCODEC_API-marked C functions are exported;
+# codec-internal C++ classes (olive::Frame, FootageDescription, ...) must not
+# leak into the global symbol namespace where they would interpose on
+# same-named weak symbols inside oakcommon/oakrender.
+target_compile_options(oakcodec PRIVATE
+ -fvisibility=hidden
+ -fvisibility-inlines-hidden
+)
+
+# oakcommon's C API implementation links these PUBLICly; oakcodec consumes
+# the oakcommon C ABI (and olivecore's C++ wrappers) only.
+target_link_libraries(oakcodec PUBLIC
+ oakcommon
+ oakrender
+ olivecore
+ ffmpeg_bridge
+ ${OCIO_LIBRARIES}
+ ${OIIO_LIBRARIES}
+)
diff --git a/src/codec/src/conformmanager.cpp b/src/codec/src/conformmanager.cpp
new file mode 100644
index 000000000..8f4585f4a
--- /dev/null
+++ b/src/codec/src/conformmanager.cpp
@@ -0,0 +1,144 @@
+/*
+ * Oak Video Editor - Non-Linear Video Editor
+ * Copyright (C) 2025 Olive CE 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 .
+ */
+
+#include "conformmanager.h"
+
+#include
+
+#include "common/filefunctions.h"
+
+#include "taskcallbacks.h"
+
+namespace olive
+{
+
+ConformManager *ConformManager::instance_ = nullptr;
+
+namespace
+{
+
+/**
+ * @brief oakcommon C API wrapper for FileFunctions::get_unique_file_identifier
+ */
+std::string unique_file_identifier(const std::string &filename)
+{
+ OakFileFunctions ff = oakcommon_filefunctions_init();
+ if (!ff.ctx) {
+ return std::string();
+ }
+
+ std::string result;
+ int size = oakcommon_filefunctions_get_unique_file_identifier(
+ ff, filename.c_str(), nullptr, 0);
+ if (size > 0) {
+ result.resize(size_t(size) - 1); // size includes the NUL
+ oakcommon_filefunctions_get_unique_file_identifier(
+ ff, filename.c_str(), result.data(), size);
+ }
+
+ oakcommon_filefunctions_free(&ff);
+ return result;
+}
+
+} // namespace
+
+ConformManager::Conform ConformManager::get_conform_state(
+ const std::string &cache_path, const Decoder::CodecStream &stream,
+ const core::AudioParams ¶ms, bool wait)
+{
+ // Return existing conform if exists
+ std::vector filenames =
+ get_conformed_filename(cache_path, stream, params);
+ if (all_conforms_exist(filenames)) {
+ return { k_conform_exists, filenames };
+ }
+
+ if (!oakcodec_task_submit_is_registered()) {
+ // Interim state (pre-M8): no task system, conform cannot be generated
+ return { k_conform_unavailable, std::vector() };
+ }
+
+ // The task owns the ".working" temporary names and the rename to the
+ // final per-channel filenames on success (previously done in
+ // conform_task_finished); output_filename carries the first channel's
+ // final path and the task derives the siblings.
+ OakCodecTaskRequest req = {};
+ req.kind = OAKCODEC_TASK_CONFORM;
+ req.input_filename = stream.filename().c_str();
+ req.output_filename =
+ filenames.empty() ? nullptr : filenames.front().c_str();
+ req.stream_index = stream.stream();
+ req.sample_rate = params.sample_rate();
+ req.channel_layout = params.channel_layout();
+ req.sample_format = int(params.format());
+
+ // Interim simplification: submission is synchronous - we always wait
+ // for SubmitTask to return, regardless of `wait`.
+ int result = SubmitTask(req);
+ if (result < 0) {
+ return { k_conform_unavailable, std::vector() };
+ }
+
+ if (all_conforms_exist(filenames)) {
+ return { k_conform_exists, filenames };
+ }
+
+ if (wait) {
+ // Synchronous wait already happened and the conform still does not
+ // exist: report the wait as failed.
+ return { k_conform_unavailable, std::vector() };
+ }
+
+ return { k_conform_generating, std::vector() };
+}
+
+std::vector
+ConformManager::get_conformed_filename(const std::string &cache_path,
+ const Decoder::CodecStream &stream,
+ const core::AudioParams ¶ms)
+{
+ std::vector filenames(size_t(params.channel_count()));
+
+ const std::string base = unique_file_identifier(stream.filename()) + "-" +
+ std::to_string(stream.stream()) + "." +
+ std::to_string(params.sample_rate()) + "." +
+ std::to_string(int(params.format())) + "." +
+ std::to_string(params.channel_layout());
+
+ for (size_t i = 0; i < filenames.size(); i++) {
+ filenames[i] = (std::filesystem::path(cache_path) /
+ (base + "." + std::to_string(i) + ".pcm"))
+ .string();
+ }
+
+ return filenames;
+}
+
+bool ConformManager::all_conforms_exist(const std::vector &filenames)
+{
+ std::error_code ec;
+ for (const std::string &fn : filenames) {
+ if (!std::filesystem::exists(fn, ec)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+} // namespace olive
diff --git a/src/codec/src/conformmanager.h b/src/codec/src/conformmanager.h
new file mode 100644
index 000000000..4ab3e77b1
--- /dev/null
+++ b/src/codec/src/conformmanager.h
@@ -0,0 +1,111 @@
+/*
+ * Oak Video Editor - Non-Linear Video Editor
+ * Copyright (C) 2025 Olive CE 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 .
+ */
+
+#ifndef OAK_CONFORMMANAGER_H
+#define OAK_CONFORMMANAGER_H
+
+#include
+#include
+
+#include "decoder.h"
+#include "olive/core/render/audioparams.h"
+
+namespace olive
+{
+
+/**
+ * @brief Manages audio conform (pcm cache) generation
+ *
+ * Qt-free interim state: actual conform work is delegated to the global
+ * task submit callback (include/codec/task.h). While no callback is
+ * registered (pre-M8), requests report k_conform_unavailable instead of
+ * starting background work.
+ *
+ * Behavior changes vs. the Qt version:
+ * - The `conform_ready` signal is gone; completion notification is the
+ * task system's / facade's business.
+ * - Submission is synchronous: get_conform_state() calls the submit
+ * callback inline and re-checks the filesystem afterwards. `wait`
+ * only controls whether a post-submit miss is reported as
+ * k_conform_unavailable (wait) or k_conform_generating (queued).
+ */
+class ConformManager {
+public:
+ static void create_instance()
+ {
+ if (!instance_) {
+ instance_ = new ConformManager();
+ }
+ }
+
+ static void destroy_instance()
+ {
+ delete instance_;
+ instance_ = nullptr;
+ }
+
+ static ConformManager *instance()
+ {
+ return instance_;
+ }
+
+ enum ConformState {
+ k_conform_exists,
+ k_conform_generating,
+ k_conform_unavailable /**< No task callback registered / submit failed. */
+ };
+
+ struct Conform {
+ ConformState state;
+ std::vector filenames;
+ };
+
+ /**
+ * @brief Get conform state, and start conforming if no conform exists
+ *
+ * Stateless and thread-safe. The decoder_id parameter of the Qt
+ * version was dropped: the task request addresses the source by
+ * filename/stream only.
+ */
+ Conform get_conform_state(const std::string &cache_path,
+ const Decoder::CodecStream &stream,
+ const core::AudioParams ¶ms, bool wait);
+
+ /**
+ * @brief Get the destination filenames of an audio stream conformed to
+ * a set of parameters (one per channel)
+ *
+ * Pure path computation: never touches the filesystem and never
+ * submits work.
+ */
+ static std::vector
+ get_conformed_filename(const std::string &cache_path,
+ const Decoder::CodecStream &stream,
+ const core::AudioParams ¶ms);
+
+private:
+ ConformManager() = default;
+
+ static ConformManager *instance_;
+
+ static bool all_conforms_exist(const std::vector &filenames);
+};
+
+} // namespace olive
+
+#endif // OAK_CONFORMMANAGER_H
diff --git a/src/codec/src/decoder.cpp b/src/codec/src/decoder.cpp
new file mode 100644
index 000000000..300ae5cad
--- /dev/null
+++ b/src/codec/src/decoder.cpp
@@ -0,0 +1,458 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+ Modifications Copyright (C) 2025 mikesolar
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#include "decoder.h"
+
+#include
+#include
+#include
+
+#include "conformmanager.h"
+#include "ffmpeg/ffmpegdecoder.h"
+#include "oiio/oiiodecoder.h"
+#include "planarfiledevice.h"
+
+namespace olive
+{
+
+namespace
+{
+
+/**
+ * @brief NULL/empty-handle-safe check of an oakrender cancel atom
+ * (borrowed pointer, used at several retrieval entry points)
+ */
+bool cancel_atom_is_cancelled(const OakCancelAtom *cancelled)
+{
+ if (!cancelled || !cancelled->ctx) {
+ return false;
+ }
+ int c = 0;
+ oakrender_cancelatom_is_cancelled(*cancelled, &c);
+ return c != 0;
+}
+
+} // namespace
+
+const Rational Decoder::k_any_timecode = RATIONAL_MIN;
+
+Decoder::Decoder()
+ : cached_texture_(nullptr)
+{
+ update_last_accessed();
+}
+
+Decoder::~Decoder()
+{
+ oakrender_display_texture_free(cached_texture_);
+}
+
+void Decoder::increment_access_time(int64_t t)
+{
+ last_accessed_ += t;
+}
+
+bool Decoder::open(const CodecStream &stream)
+{
+ std::lock_guard locker(mutex_);
+
+ update_last_accessed();
+
+ if (stream_.is_valid()) {
+ // Decoder is already open. Return TRUE if the stream is the stream we have, or FALSE if not.
+ if (stream_ == stream) {
+ return true;
+ } else {
+ fprintf(stderr, "Tried to open a decoder that was already open with another stream\n");
+ return false;
+ }
+ } else {
+ // Stream was not open, try opening it now
+ if (!stream.is_valid()) {
+ // Cannot open null stream
+ fprintf(stderr, "Decoder attempted to open null stream\n");
+ return false;
+ }
+
+ if (!stream.exists()) {
+ // Cannot open file that doesn't exist
+ fprintf(stderr, "Decoder attempted to open file that doesn't exist\n");
+ return false;
+ }
+
+ // Set stream
+ stream_ = stream;
+
+ // Try open internal
+ if (open_internal()) {
+ return true;
+ } else {
+ // Unset stream
+ fprintf(stderr, "Failed to open %s stream %d\n",
+ stream_.filename().c_str(), stream_.stream());
+ close_internal();
+ stream_.reset();
+ return false;
+ }
+ }
+}
+
+OakRenderTexture *Decoder::retrieve_video(const RetrieveVideoParams &p)
+{
+ std::lock_guard locker(mutex_);
+
+ update_last_accessed();
+
+ if (!stream_.is_valid()) {
+ fprintf(stderr, "Can't retrieve video on a closed decoder\n");
+ return nullptr;
+ }
+
+ if (!supports_video()) {
+ fprintf(stderr, "Decoder doesn't support video\n");
+ return nullptr;
+ }
+
+ if (cancel_atom_is_cancelled(p.cancelled)) {
+ return nullptr;
+ }
+
+ if (cached_texture_ && cached_time_ == p.time &&
+ cached_divider_ == p.divider) {
+ // Hand the caller its own reference; the cache keeps its own
+ return oakrender_display_texture_retain(cached_texture_);
+ }
+
+ OakRenderTexture *texture = retrieve_video_internal(p);
+ oakrender_display_texture_free(cached_texture_);
+ cached_texture_ = texture ? oakrender_display_texture_retain(texture) :
+ nullptr;
+ cached_time_ = p.time;
+ cached_divider_ = p.divider;
+
+ return texture;
+}
+
+FramePtr Decoder::retrieve_video_frame(const RetrieveVideoParams &p)
+{
+ std::lock_guard locker(mutex_);
+
+ update_last_accessed();
+
+ if (!stream_.is_valid()) {
+ fprintf(stderr, "Can't retrieve video frame on a closed decoder\n");
+ return nullptr;
+ }
+
+ if (!supports_video()) {
+ fprintf(stderr, "Decoder doesn't support video\n");
+ return nullptr;
+ }
+
+ if (cancel_atom_is_cancelled(p.cancelled)) {
+ return nullptr;
+ }
+
+ return retrieve_video_frame_internal(p);
+}
+
+Decoder::RetrieveAudioStatus
+Decoder::retrieve_audio(SampleBuffer &dest, const TimeRange &range,
+ const AudioParams ¶ms,
+ const std::string &cache_path, OakLoopMode loop_mode,
+ RenderMode::Mode mode)
+{
+ std::lock_guard locker(mutex_);
+
+ update_last_accessed();
+
+ if (!stream_.is_valid()) {
+ fprintf(stderr, "Can't retrieve audio on a closed decoder\n");
+ return k_invalid;
+ }
+
+ if (!supports_audio()) {
+ fprintf(stderr, "Decoder doesn't support audio\n");
+ return k_invalid;
+ }
+
+ if (params.sample_rate() <= 0 || params.channel_count() <= 0) {
+ fprintf(stderr, "Invalid audio parameters, skipping audio retrieve\n");
+ return k_invalid;
+ }
+
+ // Get conform state from ConformManager
+ ConformManager::Conform conform =
+ ConformManager::instance()->get_conform_state(
+ cache_path, stream_, params, (mode == RenderMode::k_online));
+ if (conform.state == ConformManager::k_conform_generating) {
+ return k_waiting_for_conform;
+ }
+
+ // See if we got the conform
+ if (retrieve_audio_from_conform(dest, conform.filenames, range, loop_mode,
+ params)) {
+ return k_ok;
+ } else {
+ return k_unknown_error;
+ }
+}
+
+int64_t Decoder::get_last_accessed_time()
+{
+ return last_accessed_;
+}
+
+void Decoder::close()
+{
+ std::lock_guard locker(mutex_);
+
+ update_last_accessed();
+
+ oakrender_display_texture_free(cached_texture_);
+ cached_texture_ = nullptr;
+
+ if (stream_.is_valid()) {
+ close_internal();
+ stream_.reset();
+ } else {
+ fprintf(stderr, "Tried to close a decoder that wasn't open\n");
+ }
+}
+
+bool Decoder::conform_audio(const std::vector &output_filenames,
+ const AudioParams ¶ms, OakCancelAtom *cancelled)
+{
+ return conform_audio_internal(output_filenames, params, cancelled);
+}
+
+/*
+ * DECODER STATIC PUBLIC MEMBERS
+ */
+
+std::vector Decoder::receive_list_of_all_decoders()
+{
+ std::vector decoders;
+
+ // The order in which these decoders are added is their priority when probing. Hence FFmpeg should usually be last,
+ // since it supports so many formats and we presumably want to override those formats with a more specific decoder.
+ decoders.push_back(std::make_shared());
+ decoders.push_back(std::make_shared());
+
+ return decoders;
+}
+
+DecoderPtr Decoder::create_from_id(const std::string &id)
+{
+ if (id.empty()) {
+ return nullptr;
+ }
+
+ // Create list to iterate through
+ std::vector decoder_list = receive_list_of_all_decoders();
+
+ for (DecoderPtr d : decoder_list) {
+ if (d->id() == id) {
+ return d;
+ }
+ }
+
+ return nullptr;
+}
+
+void Decoder::signal_processing_progress(int64_t ts, int64_t duration)
+{
+ if (duration != FB_NOPTS_VALUE && duration != 0) {
+ if (index_progress_callback_) {
+ index_progress_callback_(static_cast(ts) /
+ static_cast(duration));
+ }
+ }
+}
+
+std::string
+Decoder::transform_image_sequence_file_name(const std::string &filename,
+ const int64_t &number)
+{
+ int digit_count = get_image_sequence_digit_count(filename);
+
+ std::filesystem::path file_path(filename);
+
+ // QFileInfo::completeBaseName(): filename up to the first '.'
+ std::string original_basename = file_path.filename().string();
+ std::string::size_type dot = original_basename.find('.');
+ if (dot != std::string::npos) {
+ original_basename.erase(dot);
+ }
+
+ std::string new_basename =
+ original_basename.substr(0, original_basename.size() - digit_count);
+
+ char number_buf[32];
+ snprintf(number_buf, sizeof(number_buf), "%0*lld", digit_count,
+ static_cast(number));
+ new_basename += number_buf;
+
+ std::string new_filename = file_path.filename().string();
+ std::string::size_type pos = 0;
+ while ((pos = new_filename.find(original_basename, pos)) !=
+ std::string::npos) {
+ new_filename.replace(pos, original_basename.size(), new_basename);
+ pos += new_basename.size();
+ }
+
+ return (file_path.parent_path() / new_filename).string();
+}
+
+int Decoder::get_image_sequence_digit_count(const std::string &filename)
+{
+ // QFileInfo::completeBaseName(): filename up to the first '.'
+ std::string basename =
+ std::filesystem::path(filename).filename().string();
+ std::string::size_type dot = basename.find('.');
+ if (dot != std::string::npos) {
+ basename.erase(dot);
+ }
+
+ // See if basename contains a number at the end
+ int digit_count = 0;
+
+ for (int i = int(basename.size()) - 1; i >= 0; i--) {
+ if (basename[size_t(i)] >= '0' && basename[size_t(i)] <= '9') {
+ digit_count++;
+ } else {
+ break;
+ }
+ }
+
+ return digit_count;
+}
+
+int64_t Decoder::get_image_sequence_index(const std::string &filename)
+{
+ int digit_count = get_image_sequence_digit_count(filename);
+
+ std::string original_basename =
+ std::filesystem::path(filename).filename().string();
+ std::string::size_type dot = original_basename.find('.');
+ if (dot != std::string::npos) {
+ original_basename.erase(dot);
+ }
+
+ std::string number_only =
+ original_basename.substr(original_basename.size() - digit_count);
+
+ return strtoll(number_only.c_str(), nullptr, 10);
+}
+
+OakRenderTexture *Decoder::retrieve_video_internal(const RetrieveVideoParams &p)
+{
+ (void) p;
+ return nullptr;
+}
+
+FramePtr Decoder::retrieve_video_frame_internal(const RetrieveVideoParams &p)
+{
+ (void) p;
+ return nullptr;
+}
+
+bool Decoder::conform_audio_internal(
+ const std::vector &filenames, const AudioParams ¶ms,
+ OakCancelAtom *cancelled)
+{
+ (void) filenames;
+ (void) cancelled;
+ (void) params;
+ return false;
+}
+
+bool Decoder::retrieve_audio_from_conform(
+ SampleBuffer &sample_buffer,
+ const std::vector &conform_filenames, TimeRange range,
+ OakLoopMode loop_mode, const AudioParams &input_params)
+{
+ PlanarFileDevice input;
+ if (input.open(conform_filenames, PlanarFileDevice::k_read_only)) {
+ // Offset range by audio start offset
+ range -= get_audio_start_offset();
+
+ int64_t read_index = input_params.time_to_bytes(range.in()) /
+ input_params.channel_count();
+ int64_t write_index = 0;
+
+ const int64_t buffer_length_in_bytes =
+ sample_buffer.sample_count() *
+ input_params.bytes_per_sample_per_channel();
+
+ while (write_index < buffer_length_in_bytes) {
+ if (loop_mode == OAKCOMMON_LOOP_MODE_LOOP) {
+ while (read_index >= input.size()) {
+ read_index -= input.size();
+ }
+
+ while (read_index < 0) {
+ read_index += input.size();
+ }
+ }
+
+ int64_t write_count = 0;
+
+ if (read_index < 0) {
+ // Reading before 0, write silence here until audio data would actually start
+ write_count = std::min(-read_index, buffer_length_in_bytes);
+ sample_buffer.silence_bytes(write_index,
+ write_index + write_count);
+ } else if (read_index >= input.size()) {
+ // Reading after data length, write silence until the end of the buffer
+ write_count = buffer_length_in_bytes - write_index;
+ sample_buffer.silence_bytes(write_index,
+ write_index + write_count);
+ } else {
+ write_count = std::min(input.size() - read_index,
+ buffer_length_in_bytes - write_index);
+ input.seek(read_index);
+ input.read(reinterpret_cast(
+ sample_buffer.to_raw_ptrs().data()),
+ write_count, write_index);
+ }
+
+ read_index += write_count;
+ write_index += write_count;
+ }
+
+ input.close();
+
+ return true;
+ }
+
+ return false;
+}
+
+void Decoder::update_last_accessed()
+{
+ last_accessed_ =
+ std::chrono::duration_cast(
+ std::chrono::system_clock::now().time_since_epoch())
+ .count();
+}
+
+}
diff --git a/src/codec/src/decoder.h b/src/codec/src/decoder.h
new file mode 100644
index 000000000..a3de800f9
--- /dev/null
+++ b/src/codec/src/decoder.h
@@ -0,0 +1,399 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+ Modifications Copyright (C) 2025 mikesolar
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#ifndef OAK_DECODER_H
+#define OAK_DECODER_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "common/loopmode.h"
+#include "common/videoparams.h"
+#include "footagedescription.h"
+#include "frame.h"
+#include "node/block.h"
+#include "olive/core/render/audioparams.h"
+#include "olive/core/render/pixelformat.h"
+#include "olive/core/render/samplebuffer.h"
+#include "olive/core/util/rational.h"
+#include "olive/core/util/timerange.h"
+#include "render/cancelatom.h"
+#include "render/renderer.h"
+
+namespace olive
+{
+
+using core::AudioParams;
+using core::PixelFormat;
+using core::Rational;
+using core::SampleBuffer;
+using core::TimeRange;
+
+/**
+ * @brief Local replacement for render/rendermodes.h
+ *
+ * oakrender's C API has no render-mode counterpart. Values mirror
+ * engine/render/rendermodes.h (k_offline = 0, k_online = 1).
+ */
+class RenderMode {
+public:
+ enum Mode { k_offline, k_online };
+};
+
+/**
+ * @brief "Don't force a color range" sentinel for
+ * Decoder::RetrieveVideoParams::force_range (the actual ranges are
+ * the OAKCOMMON_COLOR_RANGE_* values).
+ */
+inline constexpr int k_color_range_default = -1;
+
+class Decoder;
+using DecoderPtr = std::shared_ptr;
+
+#define DECODER_DEFAULT_DESTRUCTOR(x) \
+ virtual ~x() override \
+ { \
+ close_internal(); \
+ }
+
+/**
+ * @brief A decoder's is the main class for bringing external media into Olive
+ *
+ * Its responsibilities are to serve as
+ * abstraction from codecs/decoders and provide complete frames. These frames can be video or audio data and are
+ * provided as Frame objects in shared pointers to alleviate the responsibility of memory handling.
+ *
+ * The main function in a decoder is Retrieve() which should return complete image/audio data. A decoder should
+ * alleviate all the complexities of codec compression from the rest of the application (i.e. a decoder should never
+ * return a partial frame or require other parts of the system to interface directly with the codec). Often this will
+ * necessitate pre-emptively caching, indexing, or even fully transcoding media before using it which can be implemented
+ * through the Analyze() function.
+ *
+ * A decoder does NOT perform any pixel/sample format conversion. Frames should pass through the PixelService
+ * to be utilized in the rest of the rendering pipeline.
+ */
+class Decoder {
+public:
+ enum RetrieveState { k_ready, k_failed_to_open, k_index_unavailable };
+
+ Decoder();
+
+ virtual ~Decoder();
+
+ /**
+ * @brief Unique decoder ID
+ */
+ virtual std::string id() const = 0;
+
+ virtual bool supports_video()
+ {
+ return false;
+ }
+ virtual bool supports_audio()
+ {
+ return false;
+ }
+
+ void increment_access_time(int64_t t);
+
+ class CodecStream {
+ public:
+ CodecStream()
+ : stream_(-1)
+ , block_(nullptr)
+ {
+ }
+
+ CodecStream(const std::string &filename, int stream,
+ const OakNodeBlock *block)
+ : filename_(filename)
+ , stream_(stream)
+ , block_(block)
+ {
+ }
+
+ bool is_valid() const
+ {
+ return !filename_.empty() && stream_ >= 0;
+ }
+
+ bool exists() const
+ {
+ std::error_code ec;
+ return std::filesystem::exists(filename_, ec);
+ }
+
+ void reset()
+ {
+ *this = CodecStream();
+ }
+
+ bool operator==(const CodecStream &rhs) const
+ {
+ return filename_ == rhs.filename_ && stream_ == rhs.stream_;
+ }
+
+ const std::string &filename() const
+ {
+ return filename_;
+ }
+
+ int stream() const
+ {
+ return stream_;
+ }
+
+ /**
+ * @brief Associated timeline block (opaque oaknode handle)
+ *
+ * Borrowed pointer: codec only stores/compares it, never
+ * dereferences, retains, or frees it.
+ */
+ const OakNodeBlock *block() const
+ {
+ return block_;
+ }
+
+ private:
+ std::string filename_;
+
+ int stream_;
+
+ const OakNodeBlock *block_;
+ };
+
+ /**
+ * @brief Open stream for decoding
+ *
+ * This function is thread safe.
+ *
+ * Returns TRUE if stream could be opened successfully. Also returns TRUE if the decoder is
+ * already open and the stream == the stream provided. Returns FALSE if the stream couldn't
+ * be opened OR if already open and the stream is NOT the same.
+ */
+ bool open(const CodecStream &stream);
+
+ static const Rational k_any_timecode;
+
+ struct RetrieveVideoParams {
+ OakRenderRenderer *renderer = nullptr;
+ Rational time;
+ int divider = 1;
+ PixelFormat maximum_format = PixelFormat::invalid;
+ OakCancelAtom *cancelled = nullptr;
+ int force_range = k_color_range_default;
+ int src_interlacing = OAKCOMMON_VIDEO_INTERLACE_NONE;
+ };
+
+ /**
+ * @brief Retrieves a video frame from footage
+ *
+ * This function will always return a valid frame unless a fatal error occurs (in such case,
+ * nullptr will return). If the timecode is before the start of the footage, this function should
+ * return the first frame. Likewise, if it is after the timecode, this function should return the
+ * last frame.
+ *
+ * This function is thread safe and can only run while the decoder is open. \see Open()
+ *
+ * The returned texture handle is owned by the caller and must be
+ * released with oakrender_display_texture_free().
+ */
+ OakRenderTexture *retrieve_video(const RetrieveVideoParams &p);
+
+ /**
+ * @brief Retrieves a decoded video frame in CPU memory.
+ *
+ * Used by render-process isolation to decode media in the main process and pass packed pixel
+ * data to workers through shared memory.
+ */
+ FramePtr retrieve_video_frame(const RetrieveVideoParams &p);
+
+ enum RetrieveAudioStatus {
+ k_invalid = -1,
+ k_ok,
+ k_waiting_for_conform,
+ k_unknown_error
+ };
+
+ /**
+ * @brief Retrieve audio data from footage
+ *
+ * This function will always return a sample buffer unless a fatal error occurs (in such case,
+ * nullptr will return). The SampleBuffer should always have enough audio for the range provided.
+ *
+ * This function is thread safe and can only run while the decoder is open. \see Open()
+ */
+ RetrieveAudioStatus retrieve_audio(SampleBuffer &dest, const TimeRange &range,
+ const AudioParams ¶ms,
+ const std::string &cache_path,
+ OakLoopMode loop_mode,
+ RenderMode::Mode mode);
+
+ /**
+ * @brief Determine the last time this decoder instance was used in any way
+ */
+ int64_t get_last_accessed_time();
+
+ /**
+ * @brief Generate a Footage object from a file
+ *
+ * If this decoder is able to parse this file, it will return a valid FootagePtr. Otherwise, it
+ * will return nullptr.
+ *
+ * For sub-classes, this function should be effectively static. We can't do virtual static
+ * functions in C++, but it should hold and access no state during its run.
+ *
+ * This function is re-entrant.
+ */
+ virtual FootageDescription probe(const std::string &filename,
+ OakCancelAtom *cancelled) const = 0;
+
+ /**
+ * @brief Closes media/deallocates memory
+ *
+ * This function is thread safe and can only run while the decoder is open. \see Open()
+ */
+ void close();
+
+ /**
+ * @brief Conform audio stream
+ */
+ bool conform_audio(const std::vector &output_filenames,
+ const AudioParams ¶ms,
+ OakCancelAtom *cancelled = nullptr);
+
+ /**
+ * @brief Create a Decoder instance using a Decoder ID
+ *
+ * @return
+ *
+ * A Decoder instance or nullptr if a Decoder with this ID does not exist
+ */
+ static DecoderPtr create_from_id(const std::string &id);
+
+ static std::string
+ transform_image_sequence_file_name(const std::string &filename,
+ const int64_t &number);
+
+ static int get_image_sequence_digit_count(const std::string &filename);
+
+ static int64_t get_image_sequence_index(const std::string &filename);
+
+ static std::vector receive_list_of_all_decoders();
+
+ /**
+ * @brief Set a callback receiving indexing progress (0-1)
+ *
+ * Replaces the former index_progress Qt signal.
+ */
+ void set_index_progress_callback(std::function callback)
+ {
+ index_progress_callback_ = std::move(callback);
+ }
+
+protected:
+ /**
+ * @brief Internal open function
+ *
+ * Sub-classes must override this function. Function will already be mutexed, so there is no need
+ * to worry about thread safety. Also many other sanity checks will be done before this, so
+ * sub-classes only need to worry about their own opening functions. It is guaranteed that the
+ * decoder is not open yet and that the footage stream was from that sub-classes probe function.
+ *
+ * Return TRUE if everything opened successfully and the decoder is ready to work. Otherwise,
+ * return FALSE. If this function returns false, Decoder will call close_internal to clean any
+ * memory allocated during OpenInternal.
+ */
+ virtual bool open_internal() = 0;
+
+ /**
+ * @brief Internal close function
+ *
+ * Sub-classes must override this function. Function should be able to safely clear all allocated
+ * memory. It may be called even if Open() didn't complete or RetrieveVideo() was never called.
+ */
+ virtual void close_internal() = 0;
+
+ /**
+ * @brief Internal frame retrieval function
+ *
+ * Sub-classes must override this function IF they support video. Function is already mutexed
+ * so sub-classes don't need to worry about thread safety.
+ *
+ * The returned texture handle is owned by the caller and must be
+ * released with oakrender_display_texture_free().
+ */
+ virtual OakRenderTexture *
+ retrieve_video_internal(const RetrieveVideoParams &p);
+
+ virtual FramePtr retrieve_video_frame_internal(const RetrieveVideoParams &p);
+
+ virtual bool
+ conform_audio_internal(const std::vector &filenames,
+ const AudioParams ¶ms, OakCancelAtom *cancelled);
+
+ void signal_processing_progress(int64_t ts, int64_t duration);
+
+ /**
+ * @brief Return currently open stream
+ *
+ * This function is NOT thread safe and should therefore only be called by thread safe functions.
+ */
+ const CodecStream &stream() const
+ {
+ return stream_;
+ }
+
+ virtual Rational get_audio_start_offset() const
+ {
+ return 0;
+ }
+
+private:
+ void update_last_accessed();
+
+ bool retrieve_audio_from_conform(
+ SampleBuffer &sample_buffer,
+ const std::vector &conform_filenames, TimeRange range,
+ OakLoopMode loop_mode, const AudioParams ¶ms);
+
+ CodecStream stream_;
+
+ std::mutex mutex_;
+
+ std::atomic_int64_t last_accessed_;
+
+ OakRenderTexture *cached_texture_;
+ Rational cached_time_;
+ int cached_divider_ = 0;
+
+ std::function index_progress_callback_;
+};
+
+}
+
+#endif // OAK_DECODER_H
diff --git a/src/codec/src/encoder.cpp b/src/codec/src/encoder.cpp
new file mode 100644
index 000000000..abf22d87d
--- /dev/null
+++ b/src/codec/src/encoder.cpp
@@ -0,0 +1,778 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+ Modifications Copyright (C) 2025 mikesolar
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#include "encoder.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "common/filefunctions.h"
+#include "olive/core/util/timecodefunctions.h"
+
+#include "ffmpeg/ffmpegencoder.h"
+#include "oiio/oiioencoder.h"
+
+namespace olive
+{
+
+const std::regex Encoder::k_image_sequence_contains_digits("\\[[#]+\\]");
+const std::regex Encoder::k_image_sequence_remove_digits(
+ "[\\-\\.\\ \\_]?\\[[#]+\\]");
+
+namespace
+{
+
+int str_to_int(const std::string &s)
+{
+ return int(std::strtol(s.c_str(), nullptr, 10));
+}
+
+int64_t str_to_int64(const std::string &s)
+{
+ return std::strtoll(s.c_str(), nullptr, 10);
+}
+
+Rational video_params_pixel_aspect_ratio(OakVideoParams vp)
+{
+ int n = 0, d = 1;
+ oakcommon_videoparams_get_pixel_aspect_ratio(vp, &n, &d);
+ return Rational(n, d);
+}
+
+Rational video_params_frame_rate_as_time_base(OakVideoParams vp)
+{
+ int n = 0, d = 1;
+ oakcommon_videoparams_frame_rate_as_time_base(vp, &n, &d);
+ return Rational(n, d);
+}
+
+std::string filefunctions_get_configuration_location()
+{
+ OakFileFunctions ff = oakcommon_filefunctions_init();
+ std::string result;
+ if (ff.ctx) {
+ int size =
+ oakcommon_filefunctions_get_configuration_location(ff, nullptr, 0);
+ if (size > 0) {
+ result.assign(size_t(size) - 1, '\0');
+ oakcommon_filefunctions_get_configuration_location(ff, result.data(),
+ size);
+ }
+ }
+ oakcommon_filefunctions_free(&ff);
+ return result;
+}
+
+} // namespace
+
+Encoder::Encoder(const EncodingParams ¶ms) : params_(params) {}
+
+const EncodingParams &Encoder::params() const
+{
+ return params_;
+}
+
+std::string Encoder::get_filename_for_frame(const Rational &frame)
+{
+ if (params().video_is_image_sequence()) {
+ // Transform!
+ int64_t frame_index = core::Timecode::time_to_timestamp(
+ frame, video_params_frame_rate_as_time_base(
+ params().video_params()));
+ int digits =
+ get_image_sequence_placeholder_digit_count(params().filename());
+
+ char frame_index_str[32];
+ snprintf(frame_index_str, sizeof(frame_index_str), "%0*lld", digits,
+ static_cast(frame_index));
+
+ return std::regex_replace(params_.filename(),
+ k_image_sequence_contains_digits,
+ frame_index_str);
+ } else {
+ // Keep filename
+ return params_.filename();
+ }
+}
+
+int Encoder::get_image_sequence_placeholder_digit_count(
+ const std::string &filename)
+{
+ std::smatch match;
+ int digit_count = 0;
+ if (std::regex_search(filename, match, k_image_sequence_contains_digits)) {
+ size_t start = size_t(match.position(0));
+ for (size_t i = start + 1; i < filename.size(); i++) {
+ if (filename.at(i) == '#') {
+ digit_count++;
+ } else {
+ break;
+ }
+ }
+ }
+ return digit_count;
+}
+
+bool Encoder::filename_contains_digit_placeholder(const std::string &filename)
+{
+ return std::regex_search(filename, k_image_sequence_contains_digits);
+}
+
+std::string Encoder::filename_remove_digit_placeholder(std::string filename)
+{
+ return std::regex_replace(filename, k_image_sequence_remove_digits, "");
+}
+
+EncodingParams::EncodingParams()
+ : video_enabled_(false)
+ , video_params_(oakcommon_videoparams_init())
+ , video_bit_rate_(0)
+ , video_min_bit_rate_(0)
+ , video_max_bit_rate_(0)
+ , video_buffer_size_(0)
+ , video_threads_(0)
+ , video_is_image_sequence_(false)
+ , color_transform_(oakcommon_colortransform_init_output(""))
+ , audio_enabled_(false)
+ , audio_bit_rate_(0)
+ , subtitles_enabled_(false)
+ , subtitles_are_sidecar_(false)
+ , video_scaling_method_(k_stretch)
+ , has_custom_range_(false)
+{
+}
+
+EncodingParams::EncodingParams(const EncodingParams &other)
+ : filename_(other.filename_)
+ , format_(other.format_)
+ , video_enabled_(other.video_enabled_)
+ , video_codec_(other.video_codec_)
+ , video_params_(other.video_params_)
+ , video_opts_(other.video_opts_)
+ , video_bit_rate_(other.video_bit_rate_)
+ , video_min_bit_rate_(other.video_min_bit_rate_)
+ , video_max_bit_rate_(other.video_max_bit_rate_)
+ , video_buffer_size_(other.video_buffer_size_)
+ , video_threads_(other.video_threads_)
+ , video_pix_fmt_(other.video_pix_fmt_)
+ , video_is_image_sequence_(other.video_is_image_sequence_)
+ , color_transform_(other.color_transform_)
+ , audio_enabled_(other.audio_enabled_)
+ , audio_codec_(other.audio_codec_)
+ , audio_params_(other.audio_params_)
+ , audio_bit_rate_(other.audio_bit_rate_)
+ , subtitles_enabled_(other.subtitles_enabled_)
+ , subtitles_are_sidecar_(other.subtitles_are_sidecar_)
+ , subtitle_sidecar_fmt_(other.subtitle_sidecar_fmt_)
+ , subtitles_codec_(other.subtitles_codec_)
+ , export_length_(other.export_length_)
+ , video_scaling_method_(other.video_scaling_method_)
+ , has_custom_range_(other.has_custom_range_)
+ , custom_range_(other.custom_range_)
+{
+ if (video_params_.ctx && video_params_.addref) {
+ video_params_.addref(video_params_.ctx);
+ }
+ if (color_transform_.ctx && color_transform_.addref) {
+ color_transform_.addref(color_transform_.ctx);
+ }
+}
+
+EncodingParams &EncodingParams::operator=(const EncodingParams &other)
+{
+ if (this != &other) {
+ // addref the incoming handles before releasing ours so that
+ // self-shared handles survive the release below
+ if (other.video_params_.ctx && other.video_params_.addref) {
+ other.video_params_.addref(other.video_params_.ctx);
+ }
+ if (other.color_transform_.ctx && other.color_transform_.addref) {
+ other.color_transform_.addref(other.color_transform_.ctx);
+ }
+ oakcommon_videoparams_free(&video_params_);
+ oakcommon_colortransform_free(&color_transform_);
+
+ filename_ = other.filename_;
+ format_ = other.format_;
+ video_enabled_ = other.video_enabled_;
+ video_codec_ = other.video_codec_;
+ video_params_ = other.video_params_;
+ video_opts_ = other.video_opts_;
+ video_bit_rate_ = other.video_bit_rate_;
+ video_min_bit_rate_ = other.video_min_bit_rate_;
+ video_max_bit_rate_ = other.video_max_bit_rate_;
+ video_buffer_size_ = other.video_buffer_size_;
+ video_threads_ = other.video_threads_;
+ video_pix_fmt_ = other.video_pix_fmt_;
+ video_is_image_sequence_ = other.video_is_image_sequence_;
+ color_transform_ = other.color_transform_;
+ audio_enabled_ = other.audio_enabled_;
+ audio_codec_ = other.audio_codec_;
+ audio_params_ = other.audio_params_;
+ audio_bit_rate_ = other.audio_bit_rate_;
+ subtitles_enabled_ = other.subtitles_enabled_;
+ subtitles_are_sidecar_ = other.subtitles_are_sidecar_;
+ subtitle_sidecar_fmt_ = other.subtitle_sidecar_fmt_;
+ subtitles_codec_ = other.subtitles_codec_;
+ export_length_ = other.export_length_;
+ video_scaling_method_ = other.video_scaling_method_;
+ has_custom_range_ = other.has_custom_range_;
+ custom_range_ = other.custom_range_;
+ }
+ return *this;
+}
+
+EncodingParams::~EncodingParams()
+{
+ oakcommon_videoparams_free(&video_params_);
+ oakcommon_colortransform_free(&color_transform_);
+}
+
+std::string EncodingParams::get_preset_path()
+{
+ return (std::filesystem::path(filefunctions_get_configuration_location()) /
+ "exportpresets")
+ .string();
+}
+
+std::vector EncodingParams::get_list_of_presets()
+{
+ std::vector list;
+ std::error_code ec;
+ for (const auto &entry : std::filesystem::directory_iterator(
+ get_preset_path(), ec)) {
+ if (entry.is_regular_file()) {
+ list.push_back(entry.path().filename().string());
+ }
+ }
+ // QDir::entryList(QDir::Files) sorted by name by default
+ std::sort(list.begin(), list.end());
+ return list;
+}
+
+void EncodingParams::enable_video(const OakVideoParams &video_params,
+ const ExportCodec::Codec &vcodec)
+{
+ if (video_params.ctx && video_params.addref) {
+ video_params.addref(video_params.ctx);
+ }
+ oakcommon_videoparams_free(&video_params_);
+ video_params_ = video_params;
+
+ video_enabled_ = true;
+ video_codec_ = vcodec;
+}
+
+void EncodingParams::set_color_transform(
+ const OakColorTransform &color_transform)
+{
+ if (color_transform.ctx && color_transform.addref) {
+ color_transform.addref(color_transform.ctx);
+ }
+ oakcommon_colortransform_free(&color_transform_);
+ color_transform_ = color_transform;
+}
+
+void EncodingParams::enable_audio(const AudioParams &audio_params,
+ const ExportCodec::Codec &acodec)
+{
+ audio_enabled_ = true;
+ audio_params_ = audio_params;
+ audio_codec_ = acodec;
+}
+
+void EncodingParams::enable_subtitles(const ExportCodec::Codec &scodec)
+{
+ subtitles_enabled_ = true;
+ subtitles_codec_ = scodec;
+}
+
+void EncodingParams::enable_sidecar_subtitles(const ExportFormat::Format &sfmt,
+ const ExportCodec::Codec &scodec)
+{
+ subtitles_enabled_ = true;
+ subtitles_are_sidecar_ = true;
+ subtitle_sidecar_fmt_ = sfmt;
+ subtitles_codec_ = scodec;
+}
+
+void EncodingParams::disable_video()
+{
+ video_enabled_ = false;
+}
+
+void EncodingParams::disable_audio()
+{
+ audio_enabled_ = false;
+}
+
+void EncodingParams::disable_subtitles()
+{
+ subtitles_enabled_ = false;
+}
+
+bool EncodingParams::load(XmlStreamReader *reader)
+{
+ while (xml_read_next_start_element(reader)) {
+ if (reader->name() == "export") {
+ int version = 0;
+
+ for (const auto &attr : reader->attributes()) {
+ if (attr.name == "version") {
+ version = str_to_int(attr.value);
+ }
+ }
+
+ switch (version) {
+ case 1:
+ return load_v1(reader);
+ }
+ } else {
+ reader->skip_current_element();
+ }
+ }
+
+ return false;
+}
+
+bool EncodingParams::load(const std::string &xml)
+{
+ XmlStreamReader reader(xml);
+ return load(&reader);
+}
+
+std::string EncodingParams::save_to_string() const
+{
+ XmlStreamWriter writer;
+ save(&writer);
+ return writer.output();
+}
+
+void EncodingParams::save(XmlStreamWriter *writer) const
+{
+ writer->write_start_element("export");
+
+ writer->write_attribute("version", std::to_string(k_encoder_params_version));
+
+ writer->write_text_element("filename", filename_);
+ writer->write_text_element("format", std::to_string(format_));
+
+ writer->write_text_element("range", std::to_string(has_custom_range_));
+ writer->write_text_element("customrangein", custom_range_.in().to_string());
+ writer->write_text_element("customrangeout",
+ custom_range_.out().to_string());
+
+ writer->write_start_element("video");
+
+ writer->write_attribute("enabled", std::to_string(video_enabled_));
+
+ if (video_enabled_) {
+ int vp_width = 0, vp_height = 0, vp_format = -1, vp_divider = 1;
+ oakcommon_videoparams_get_width(video_params_, &vp_width);
+ oakcommon_videoparams_get_height(video_params_, &vp_height);
+ oakcommon_videoparams_get_format(video_params_, &vp_format);
+ oakcommon_videoparams_get_divider(video_params_, &vp_divider);
+ int vp_time_base_num = 0, vp_time_base_den = 1;
+ oakcommon_videoparams_get_time_base(video_params_, &vp_time_base_num,
+ &vp_time_base_den);
+
+ writer->write_text_element("codec", std::to_string(video_codec_));
+ writer->write_text_element("width", std::to_string(vp_width));
+ writer->write_text_element("height", std::to_string(vp_height));
+ writer->write_text_element("format", std::to_string(vp_format));
+ writer->write_text_element(
+ "pixelaspect",
+ video_params_pixel_aspect_ratio(video_params_).to_string());
+ writer->write_text_element(
+ "timebase",
+ Rational(vp_time_base_num, vp_time_base_den).to_string());
+ writer->write_text_element("divider", std::to_string(vp_divider));
+ writer->write_text_element("bitrate", std::to_string(video_bit_rate_));
+ writer->write_text_element("minbitrate",
+ std::to_string(video_min_bit_rate_));
+ writer->write_text_element("maxbitrate",
+ std::to_string(video_max_bit_rate_));
+ writer->write_text_element("bufsize",
+ std::to_string(video_buffer_size_));
+ writer->write_text_element("threads", std::to_string(video_threads_));
+ writer->write_text_element("pixfmt", video_pix_fmt_);
+ writer->write_text_element("imgseq",
+ std::to_string(video_is_image_sequence_));
+
+ std::string color_output;
+ int color_output_size = oakcommon_colortransform_get_output(
+ color_transform_, nullptr, 0);
+ if (color_output_size > 0) {
+ color_output.assign(size_t(color_output_size) - 1, '\0');
+ oakcommon_colortransform_get_output(
+ color_transform_, color_output.data(), color_output_size);
+ }
+
+ writer->write_start_element("color");
+ writer->write_text_element("output", color_output);
+ writer->write_end_element(); // colortransform
+
+ writer->write_text_element("vscale",
+ std::to_string(video_scaling_method_));
+
+ if (!video_opts_.empty()) {
+ writer->write_start_element("opts");
+
+ for (const auto &entry : video_opts_) {
+ writer->write_start_element("entry");
+
+ writer->write_text_element("key", entry.first);
+ writer->write_text_element("value", entry.second);
+
+ writer->write_end_element(); // entry
+ }
+
+ writer->write_end_element(); // opts
+ }
+ }
+
+ writer->write_end_element(); // video
+
+ writer->write_start_element("audio");
+
+ writer->write_attribute("enabled", std::to_string(audio_enabled_));
+
+ if (audio_enabled_) {
+ writer->write_text_element("codec", std::to_string(audio_codec_));
+ writer->write_text_element(
+ "samplerate", std::to_string(audio_params_.sample_rate()));
+
+ writer->write_text_element(
+ "channellayout", std::to_string(audio_params().channel_layout()));
+ writer->write_text_element("format",
+ audio_params_.format().to_string());
+ writer->write_text_element("bitrate", std::to_string(audio_bit_rate_));
+ }
+
+ writer->write_start_element("subtitles");
+
+ writer->write_attribute("enabled", std::to_string(subtitles_enabled_));
+
+ if (subtitles_enabled_) {
+ writer->write_text_element("sidecar",
+ std::to_string(subtitles_are_sidecar_));
+ writer->write_text_element("sidecarformat",
+ std::to_string(subtitle_sidecar_fmt_));
+
+ writer->write_text_element("codec", std::to_string(subtitles_codec_));
+ }
+
+ writer->write_end_element(); // subtitles
+
+ writer->write_end_element(); // audio
+
+ writer->write_end_element(); // export
+
+ writer->write_end_document();
+}
+
+Encoder *Encoder::create_from_id(Type id, const EncodingParams ¶ms)
+{
+ switch (id) {
+ case k_encoder_type_none:
+ break;
+ case k_encoder_type_f_fmpeg:
+ return new FFmpegEncoder(params);
+ case k_encoder_type_oiio:
+ return new OIIOEncoder(params);
+ }
+
+ return nullptr;
+}
+
+Encoder::Type Encoder::get_type_from_format(ExportFormat::Format f)
+{
+ switch (f) {
+ case ExportFormat::k_format_d_nx_hd:
+ case ExportFormat::k_format_matroska:
+ case ExportFormat::k_format_quick_time:
+ case ExportFormat::k_format_mpe_g4_video:
+ case ExportFormat::k_format_mpe_g4_audio:
+ case ExportFormat::k_format_wav:
+ case ExportFormat::k_format_aiff:
+ case ExportFormat::k_format_m_p3:
+ case ExportFormat::k_format_flac:
+ case ExportFormat::k_format_ogg:
+ case ExportFormat::k_format_web_m:
+ case ExportFormat::k_format_srt:
+ return k_encoder_type_f_fmpeg;
+ case ExportFormat::k_format_open_exr:
+ case ExportFormat::k_format_png:
+ case ExportFormat::k_format_tiff:
+ return k_encoder_type_oiio;
+ case ExportFormat::k_format_count:
+ break;
+ }
+
+ return k_encoder_type_none;
+}
+
+Encoder *Encoder::create_from_format(ExportFormat::Format f,
+ const EncodingParams ¶ms)
+{
+ return create_from_id(get_type_from_format(f), params);
+}
+
+Encoder *Encoder::create_from_params(const EncodingParams ¶ms)
+{
+ return create_from_format(params.format(), params);
+}
+
+std::vector
+Encoder::get_pixel_formats_for_codec(ExportCodec::Codec c) const
+{
+ return std::vector();
+}
+
+std::vector
+Encoder::get_sample_formats_for_codec(ExportCodec::Codec c) const
+{
+ return std::vector();
+}
+
+std::array
+EncodingParams::generate_matrix(EncodingParams::VideoScalingMethod method,
+ int source_width, int source_height,
+ int dest_width, int dest_height)
+{
+ // Identity (former default-constructed QMatrix4x4), row-major
+ std::array preview_matrix = { 1, 0, 0, 0, //
+ 0, 1, 0, 0, //
+ 0, 0, 1, 0, //
+ 0, 0, 0, 1 };
+
+ if (method == EncodingParams::k_stretch) {
+ return preview_matrix;
+ }
+
+ float export_ar =
+ static_cast(dest_width) / static_cast(dest_height);
+ float source_ar =
+ static_cast(source_width) / static_cast(source_height);
+
+ // qFuzzyCompare(export_ar, source_ar)
+ if (std::abs(export_ar - source_ar) * 100000.0f <=
+ std::min(std::abs(export_ar), std::abs(source_ar))) {
+ return preview_matrix;
+ }
+
+ if ((export_ar > source_ar) == (method == EncodingParams::k_fit)) {
+ // scale(source_ar / export_ar, 1)
+ preview_matrix[0] = source_ar / export_ar;
+ } else {
+ // scale(1, export_ar / source_ar)
+ preview_matrix[5] = export_ar / source_ar;
+ }
+
+ return preview_matrix;
+}
+
+bool EncodingParams::load_v1(XmlStreamReader *reader)
+{
+ Rational custom_range_in, custom_range_out;
+
+ while (xml_read_next_start_element(reader)) {
+ if (reader->name() == "filename") {
+ filename_ = reader->read_element_text();
+ } else if (reader->name() == "format") {
+ format_ = static_cast(
+ str_to_int(reader->read_element_text()));
+ } else if (reader->name() == "range") {
+ has_custom_range_ = str_to_int(reader->read_element_text());
+ } else if (reader->name() == "customrangein") {
+ custom_range_in =
+ Rational::from_string(reader->read_element_text());
+ } else if (reader->name() == "customrangeout") {
+ custom_range_out =
+ Rational::from_string(reader->read_element_text());
+ } else if (reader->name() == "video") {
+ for (const auto &attr : reader->attributes()) {
+ if (attr.name == "enabled") {
+ video_enabled_ = str_to_int(attr.value);
+ }
+ }
+
+ while (xml_read_next_start_element(reader)) {
+ if (reader->name() == "codec") {
+ video_codec_ = static_cast(
+ str_to_int(reader->read_element_text()));
+ } else if (reader->name() == "width") {
+ oakcommon_videoparams_set_width(
+ video_params_,
+ str_to_int(reader->read_element_text()));
+ } else if (reader->name() == "height") {
+ oakcommon_videoparams_set_height(
+ video_params_,
+ str_to_int(reader->read_element_text()));
+ } else if (reader->name() == "format") {
+ oakcommon_videoparams_set_format(
+ video_params_,
+ str_to_int(reader->read_element_text()));
+ } else if (reader->name() == "pixelaspect") {
+ Rational par =
+ Rational::from_string(reader->read_element_text());
+ oakcommon_videoparams_set_pixel_aspect_ratio(
+ video_params_, par.numerator(), par.denominator());
+ } else if (reader->name() == "timebase") {
+ Rational tb =
+ Rational::from_string(reader->read_element_text());
+ oakcommon_videoparams_set_time_base(
+ video_params_, tb.numerator(), tb.denominator());
+ } else if (reader->name() == "divider") {
+ oakcommon_videoparams_set_divider(
+ video_params_,
+ str_to_int(reader->read_element_text()));
+ } else if (reader->name() == "bitrate") {
+ video_bit_rate_ = str_to_int64(reader->read_element_text());
+ } else if (reader->name() == "minbitrate") {
+ video_min_bit_rate_ =
+ str_to_int64(reader->read_element_text());
+ } else if (reader->name() == "maxbitrate") {
+ video_max_bit_rate_ =
+ str_to_int64(reader->read_element_text());
+ } else if (reader->name() == "bufsize") {
+ video_buffer_size_ =
+ str_to_int64(reader->read_element_text());
+ } else if (reader->name() == "threads") {
+ video_threads_ = str_to_int(reader->read_element_text());
+ } else if (reader->name() == "pixfmt") {
+ video_pix_fmt_ = reader->read_element_text();
+ } else if (reader->name() == "imgseq") {
+ video_is_image_sequence_ =
+ str_to_int(reader->read_element_text());
+ } else if (reader->name() == "color") {
+ while (xml_read_next_start_element(reader)) {
+ if (reader->name() == "output") {
+ OakColorTransform ct =
+ oakcommon_colortransform_init_output(
+ reader->read_element_text().c_str());
+ oakcommon_colortransform_free(&color_transform_);
+ color_transform_ = ct;
+ } else {
+ reader->skip_current_element();
+ }
+ }
+ } else if (reader->name() == "vscale") {
+ video_scaling_method_ = static_cast(
+ str_to_int(reader->read_element_text()));
+ } else if (reader->name() == "opts") {
+ while (xml_read_next_start_element(reader)) {
+ if (reader->name() == "entry") {
+ std::string key, value;
+ while (xml_read_next_start_element(reader)) {
+ if (reader->name() == "key") {
+ key = reader->read_element_text();
+ } else if (reader->name() == "value") {
+ value = reader->read_element_text();
+ } else {
+ reader->skip_current_element();
+ }
+ }
+ set_video_option(key, value);
+ } else {
+ reader->skip_current_element();
+ }
+ }
+ } else {
+ reader->skip_current_element();
+ }
+ }
+
+ // HACK: Resolve bug where I forgot to serialize pixel aspect ratio
+ if (video_params_pixel_aspect_ratio(video_params_).isNull()) {
+ oakcommon_videoparams_set_pixel_aspect_ratio(video_params_, 1,
+ 1);
+ }
+ } else if (reader->name() == "audio") {
+ for (const auto &attr : reader->attributes()) {
+ if (attr.name == "enabled") {
+ audio_enabled_ = str_to_int(attr.value);
+ }
+ }
+
+ while (xml_read_next_start_element(reader)) {
+ if (reader->name() == "codec") {
+ audio_codec_ = static_cast(
+ str_to_int(reader->read_element_text()));
+ } else if (reader->name() == "samplerate") {
+ audio_params_.set_sample_rate(
+ str_to_int(reader->read_element_text()));
+ } else if (reader->name() == "channellayout") {
+ audio_params_.set_channel_layout(
+ uint64_t(str_to_int64(reader->read_element_text())));
+ } else if (reader->name() == "format") {
+ audio_params_.set_format(
+ SampleFormat::from_string(reader->read_element_text()));
+ } else if (reader->name() == "bitrate") {
+ audio_bit_rate_ = str_to_int64(reader->read_element_text());
+ } else {
+ reader->skip_current_element();
+ }
+ }
+
+ // HACK: Resolve bug where I forgot to serialize the audio bit rate
+ if (!audio_bit_rate_) {
+ audio_bit_rate_ = 320000;
+ }
+ } else if (reader->name() == "subtitles") {
+ for (const auto &attr : reader->attributes()) {
+ if (attr.name == "enabled") {
+ subtitles_enabled_ = str_to_int(attr.value);
+ }
+ }
+
+ while (xml_read_next_start_element(reader)) {
+ if (reader->name() == "sidecar") {
+ subtitles_are_sidecar_ =
+ str_to_int(reader->read_element_text());
+ } else if (reader->name() == "sidecarformat") {
+ subtitle_sidecar_fmt_ = static_cast(
+ str_to_int(reader->read_element_text()));
+ } else if (reader->name() == "codec") {
+ subtitles_codec_ = static_cast(
+ str_to_int(reader->read_element_text()));
+ } else {
+ reader->skip_current_element();
+ }
+ }
+ } else {
+ reader->skip_current_element();
+ }
+ }
+
+ // NOTE: custom_range_in/custom_range_out are intentionally not applied to
+ // custom_range_ โ this matches the original behavior (they were read but
+ // never assigned).
+ (void) custom_range_in;
+ (void) custom_range_out;
+
+ return true;
+}
+
+}
diff --git a/src/codec/src/encoder.h b/src/codec/src/encoder.h
new file mode 100644
index 000000000..101cb4a53
--- /dev/null
+++ b/src/codec/src/encoder.h
@@ -0,0 +1,436 @@
+/***
+
+ Olive - Non-Linear Video Editor
+ Copyright (C) 2022 Olive Team
+ Modifications Copyright (C) 2025 mikesolar
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+***/
+
+#ifndef OAK_ENCODER_H
+#define OAK_ENCODER_H
+
+#include
+#include