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 +#include +#include +#include +#include + +#include "olive/core/render/audioparams.h" +#include "olive/core/render/pixelformat.h" +#include "olive/core/render/samplebuffer.h" +#include "olive/core/render/sampleformat.h" +#include "olive/core/util/rational.h" +#include "olive/core/util/timerange.h" + +#include "common/colortransform.h" +#include "common/videoparams.h" +#include "exportcodec.h" +#include "exportformat.h" +#include "frame.h" +#include "xmlutils.h" + +namespace olive +{ + +using core::AudioParams; +using core::PixelFormat; +using core::Rational; +using core::SampleBuffer; +using core::SampleFormat; +using core::TimeRange; + +class Encoder; +using EncoderPtr = std::shared_ptr; + +/** + * @brief Parameters for an export encode + * + * Holds OakVideoParams / OakColorTransform C handles directly (no adapter + * layer). Copy constructor/assignment addref the handles, destructor + * releases them, so EncodingParams remains safely copyable by value + * (Encoder::params_ stores a copy). + */ +class EncodingParams { +public: + enum VideoScalingMethod { k_fit, k_stretch, k_crop }; + + EncodingParams(); + EncodingParams(const EncodingParams &other); + EncodingParams &operator=(const EncodingParams &other); + ~EncodingParams(); + + static std::string get_preset_path(); + static std::vector get_list_of_presets(); + + bool is_valid() const + { + return video_enabled_ || audio_enabled_ || subtitles_enabled_; + } + + void set_filename(const std::string &filename) + { + filename_ = filename; + } + + /** + * @brief Enable video with the given parameter set + * + * addrefs @p video_params; the caller keeps ownership of its own + * reference. + */ + void enable_video(const OakVideoParams &video_params, + const ExportCodec::Codec &vcodec); + void enable_audio(const AudioParams &audio_params, + const ExportCodec::Codec &acodec); + void enable_subtitles(const ExportCodec::Codec &scodec); + void enable_sidecar_subtitles(const ExportFormat::Format &sfmt, + const ExportCodec::Codec &scodec); + + void disable_video(); + void disable_audio(); + void disable_subtitles(); + + const ExportFormat::Format &format() const + { + return format_; + } + void set_format(const ExportFormat::Format &format) + { + format_ = format; + } + + void set_video_option(const std::string &key, const std::string &value) + { + video_opts_[key] = value; + } + void set_video_bit_rate(const int64_t &rate) + { + video_bit_rate_ = rate; + } + void set_video_min_bit_rate(const int64_t &rate) + { + video_min_bit_rate_ = rate; + } + void set_video_max_bit_rate(const int64_t &rate) + { + video_max_bit_rate_ = rate; + } + void set_video_buffer_size(const int64_t &sz) + { + video_buffer_size_ = sz; + } + void set_video_threads(const int &threads) + { + video_threads_ = threads; + } + void set_video_pix_fmt(const std::string &s) + { + video_pix_fmt_ = s; + } + void set_video_is_image_sequence(bool s) + { + video_is_image_sequence_ = s; + } + /** + * @brief Set the export color transform + * + * addrefs @p color_transform and releases the previously held handle. + */ + void set_color_transform(const OakColorTransform &color_transform); + + const std::string &filename() const + { + return filename_; + } + + bool video_enabled() const + { + return video_enabled_; + } + const ExportCodec::Codec &video_codec() const + { + return video_codec_; + } + /** + * @brief Borrowed video parameter handle (valid while this object lives) + */ + const OakVideoParams &video_params() const + { + return video_params_; + } + const std::map &video_opts() const + { + return video_opts_; + } + std::string video_option(const std::string &key) const + { + auto it = video_opts_.find(key); + return it != video_opts_.end() ? it->second : std::string(); + } + bool has_video_opt(const std::string &key) const + { + return video_opts_.count(key) > 0; + } + const int64_t &video_bit_rate() const + { + return video_bit_rate_; + } + const int64_t &video_min_bit_rate() const + { + return video_min_bit_rate_; + } + const int64_t &video_max_bit_rate() const + { + return video_max_bit_rate_; + } + const int64_t &video_buffer_size() const + { + return video_buffer_size_; + } + const int &video_threads() const + { + return video_threads_; + } + const std::string &video_pix_fmt() const + { + return video_pix_fmt_; + } + bool video_is_image_sequence() const + { + return video_is_image_sequence_; + } + /** + * @brief Borrowed color transform handle (valid while this object lives) + */ + const OakColorTransform &color_transform() const + { + return color_transform_; + } + + bool audio_enabled() const + { + return audio_enabled_; + } + const ExportCodec::Codec &audio_codec() const + { + return audio_codec_; + } + const AudioParams &audio_params() const + { + return audio_params_; + } + const int64_t &audio_bit_rate() const + { + return audio_bit_rate_; + } + + void set_audio_bit_rate(const int64_t &b) + { + audio_bit_rate_ = b; + } + + bool subtitles_enabled() const + { + return subtitles_enabled_; + } + bool subtitles_are_sidecar() const + { + return subtitles_are_sidecar_; + } + ExportFormat::Format subtitle_sidecar_fmt() const + { + return subtitle_sidecar_fmt_; + } + ExportCodec::Codec subtitles_codec() const + { + return subtitles_codec_; + } + + const Rational &get_export_length() const + { + return export_length_; + } + void set_export_length(const Rational &export_length) + { + export_length_ = export_length; + } + + bool load(const std::string &xml); + bool load(XmlStreamReader *reader); + + std::string save_to_string() const; + void save(XmlStreamWriter *writer) const; + + bool has_custom_range() const + { + return has_custom_range_; + } + const TimeRange &custom_range() const + { + return custom_range_; + } + void set_custom_range(const TimeRange &custom_range) + { + has_custom_range_ = true; + custom_range_ = custom_range; + } + + const VideoScalingMethod &video_scaling_method() const + { + return video_scaling_method_; + } + void + set_video_scaling_method(const VideoScalingMethod &video_scaling_method) + { + video_scaling_method_ = video_scaling_method; + } + + /** + * @brief Generate a scaling matrix for the given scaling method + * + * De-Qt note: formerly returned QMatrix4x4. Now returns 16 floats in + * row-major order (m[row * 4 + column], matching QMatrix4x4's + * operator()(row, column) layout). The result is always a diagonal + * matrix: identity for k_stretch (or aspect-equal sources), otherwise + * a uniform axis scale at (0,0) and (1,1). + */ + static std::array + generate_matrix(VideoScalingMethod method, int source_width, + int source_height, int dest_width, int dest_height); + +private: + static const int k_encoder_params_version = 1; + + bool load_v1(XmlStreamReader *reader); + + std::string filename_; + ExportFormat::Format format_ = ExportFormat::k_format_count; + + bool video_enabled_; + ExportCodec::Codec video_codec_ = ExportCodec::k_codec_count; + OakVideoParams video_params_; + std::map video_opts_; + int64_t video_bit_rate_; + int64_t video_min_bit_rate_; + int64_t video_max_bit_rate_; + int64_t video_buffer_size_; + int video_threads_; + std::string video_pix_fmt_; + bool video_is_image_sequence_; + OakColorTransform color_transform_; + + bool audio_enabled_; + ExportCodec::Codec audio_codec_ = ExportCodec::k_codec_count; + AudioParams audio_params_; + int64_t audio_bit_rate_; + + bool subtitles_enabled_; + bool subtitles_are_sidecar_; + ExportFormat::Format subtitle_sidecar_fmt_ = ExportFormat::k_format_count; + ExportCodec::Codec subtitles_codec_ = ExportCodec::k_codec_count; + + Rational export_length_; + VideoScalingMethod video_scaling_method_; + + bool has_custom_range_; + TimeRange custom_range_; +}; + +class Encoder { +public: + Encoder(const EncodingParams ¶ms); + + virtual ~Encoder() = default; + + enum Type { k_encoder_type_none = -1, k_encoder_type_f_fmpeg, k_encoder_type_oiio }; + + /** + * @brief Create a Encoder instance using a Encoder ID + * + * @return + * + * A Encoder instance or nullptr if a Decoder with this ID does not exist + */ + static Encoder *create_from_id(Type id, const EncodingParams ¶ms); + + static Type get_type_from_format(ExportFormat::Format f); + + static Encoder *create_from_format(ExportFormat::Format f, + const EncodingParams ¶ms); + + static Encoder *create_from_params(const EncodingParams ¶ms); + + virtual std::vector + get_pixel_formats_for_codec(ExportCodec::Codec c) const; + virtual std::vector + get_sample_formats_for_codec(ExportCodec::Codec c) const; + + const EncodingParams ¶ms() const; + + virtual PixelFormat get_desired_pixel_format() const + { + return PixelFormat::invalid; + } + + const std::string &get_error() const + { + return error_; + } + + std::string get_filename_for_frame(const Rational &frame); + + static int get_image_sequence_placeholder_digit_count(const std::string &filename); + + static bool filename_contains_digit_placeholder(const std::string &filename); + static std::string filename_remove_digit_placeholder(std::string filename); + + static const std::regex k_image_sequence_contains_digits; + static const std::regex k_image_sequence_remove_digits; + + virtual bool open() = 0; + + virtual bool write_frame(olive::FramePtr frame, + olive::core::Rational time) = 0; + virtual bool write_audio(const olive::SampleBuffer &audio) = 0; + + /** + * @brief Write one subtitle entry + * + * De-Qt note: formerly took a `const SubtitleBlock *` (an oaknode C++ + * type). Now takes the flattened text and in/out times in seconds; + * callers extract them from the subtitle block via the oaknode C API. + */ + virtual bool write_subtitle(const char *text, double in_seconds, + double out_seconds) = 0; + + virtual void close() = 0; + +protected: + void set_error(const std::string &err) + { + error_ = err; + } + +private: + EncodingParams params_; + + std::string error_; +}; + +} + +#endif // OAK_ENCODER_H diff --git a/src/codec/src/exportcodec.cpp b/src/codec/src/exportcodec.cpp new file mode 100644 index 000000000..4ef415092 --- /dev/null +++ b/src/codec/src/exportcodec.cpp @@ -0,0 +1,139 @@ +/*** + + 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 "exportcodec.h" + +extern "C" { +} + +namespace olive +{ + +std::string ExportCodec::get_codec_name(ExportCodec::Codec c) +{ + switch (c) { + case k_codec_d_nx_hd: + return "DNxHD"; + case k_codec_h264: + return "H.264"; + case k_codec_h264rgb: + return "H.264 RGB"; + case k_codec_h265: + return "H.265"; + case k_codec_open_exr: + return "OpenEXR"; + case k_codec_png: + return "PNG"; + case k_codec_pro_res: + return "ProRes"; + case k_codec_cineform: + return "Cineform"; + case k_codec_tiff: + return "TIFF"; + case k_codec_m_p2: + return "MP2"; + case k_codec_m_p3: + return "MP3"; + case k_codec_aac: + return "AAC"; + case k_codec_pcm: + return "PCM (Uncompressed)"; + case k_codec_flac: + return "FLAC"; + case k_codec_opus: + return "Opus"; + case k_codec_vorbis: + return "Vorbis"; + case k_codec_v_p9: + return "VP9"; + case k_codec_a_v1: + return "AV1"; + case k_codec_srt: + return "SubRip SRT"; + case k_codec_count: + break; + } + + return "Unknown"; +} + +bool ExportCodec::is_codec_a_still_image(ExportCodec::Codec c) +{ + switch (c) { + case k_codec_d_nx_hd: + case k_codec_h264: + case k_codec_h264rgb: + case k_codec_h265: + case k_codec_pro_res: + case k_codec_cineform: + case k_codec_m_p2: + case k_codec_m_p3: + case k_codec_aac: + case k_codec_pcm: + case k_codec_vorbis: + case k_codec_opus: + case k_codec_flac: + case k_codec_v_p9: + case k_codec_a_v1: + case k_codec_srt: + return false; + case k_codec_open_exr: + case k_codec_png: + case k_codec_tiff: + return true; + case k_codec_count: + break; + } + + return false; +} + +bool ExportCodec::is_codec_lossless(Codec c) +{ + switch (c) { + case k_codec_pcm: + case k_codec_flac: + return true; + case k_codec_d_nx_hd: + case k_codec_h264: + case k_codec_h264rgb: + case k_codec_h265: + case k_codec_pro_res: + case k_codec_cineform: + case k_codec_m_p2: + case k_codec_m_p3: + case k_codec_aac: + case k_codec_vorbis: + case k_codec_opus: + case k_codec_v_p9: + case k_codec_a_v1: + case k_codec_srt: + case k_codec_open_exr: + case k_codec_png: + case k_codec_tiff: + case k_codec_count: + break; + } + + return false; +} + +} diff --git a/src/codec/src/exportcodec.h b/src/codec/src/exportcodec.h new file mode 100644 index 000000000..e3f121266 --- /dev/null +++ b/src/codec/src/exportcodec.h @@ -0,0 +1,66 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EXPORTCODEC_H +#define OAK_EXPORTCODEC_H + +#include + +namespace olive +{ + +class ExportCodec { +public: + // Only append to this list (never insert) because indexes are used in serialized files + enum Codec { + k_codec_d_nx_hd, + k_codec_h264, + k_codec_h264rgb, + k_codec_h265, + k_codec_open_exr, + k_codec_png, + k_codec_pro_res, + k_codec_cineform, + k_codec_tiff, + k_codec_v_p9, + k_codec_m_p2, + k_codec_m_p3, + k_codec_aac, + k_codec_pcm, + k_codec_opus, + k_codec_vorbis, + k_codec_flac, + k_codec_srt, + k_codec_a_v1, + + k_codec_count + }; + + static std::string get_codec_name(Codec c); + + static bool is_codec_a_still_image(Codec c); + + static bool is_codec_lossless(Codec c); +}; + +} + +#endif // OAK_EXPORTCODEC_H diff --git a/src/codec/src/exportformat.cpp b/src/codec/src/exportformat.cpp new file mode 100644 index 000000000..6e957bbd7 --- /dev/null +++ b/src/codec/src/exportformat.cpp @@ -0,0 +1,249 @@ +/*** + + 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 "exportformat.h" + +#include "encoder.h" + +namespace olive +{ + +std::string ExportFormat::get_name(olive::ExportFormat::Format f) +{ + switch (f) { + case k_format_d_nx_hd: + return "DNxHD"; + case k_format_matroska: + return "Matroska Video"; + case k_format_mpe_g4_video: + return "MPEG-4 Video"; + case k_format_mpe_g4_audio: + return "MPEG-4 Audio"; + case k_format_open_exr: + return "OpenEXR"; + case k_format_png: + return "PNG"; + case k_format_tiff: + return "TIFF"; + case k_format_quick_time: + return "QuickTime"; + case k_format_wav: + return "Wave Audio"; + case k_format_aiff: + return "AIFF"; + case k_format_m_p3: + return "MP3"; + case k_format_flac: + return "FLAC"; + case k_format_ogg: + return "Ogg"; + case k_format_web_m: + return "WebM"; + case k_format_srt: + return "SubRip SRT"; + + case k_format_count: + break; + } + + return "Unknown"; +} + +std::string ExportFormat::get_extension(ExportFormat::Format f) +{ + switch (f) { + case k_format_d_nx_hd: + return "mxf"; + case k_format_matroska: + return "mkv"; + case k_format_mpe_g4_video: + return "mp4"; + case k_format_mpe_g4_audio: + return "m4a"; + case k_format_open_exr: + return "exr"; + case k_format_png: + return "png"; + case k_format_tiff: + return "tiff"; + case k_format_quick_time: + return "mov"; + case k_format_wav: + return "wav"; + case k_format_aiff: + return "aiff"; + case k_format_m_p3: + return "mp3"; + case k_format_flac: + return "flac"; + case k_format_ogg: + return "ogg"; + case k_format_web_m: + return "webm"; + case k_format_srt: + return "srt"; + case k_format_count: + break; + } + + return std::string(); +} + +std::vector ExportFormat::get_video_codecs(ExportFormat::Format f) +{ + switch (f) { + case k_format_d_nx_hd: + return { ExportCodec::k_codec_d_nx_hd }; + case k_format_matroska: + return { ExportCodec::k_codec_h264, ExportCodec::k_codec_h264rgb, + ExportCodec::k_codec_h265, ExportCodec::k_codec_v_p9 }; + case k_format_mpe_g4_video: + return { ExportCodec::k_codec_h264, ExportCodec::k_codec_h264rgb, + ExportCodec::k_codec_h265 }; + case k_format_open_exr: + return { ExportCodec::k_codec_open_exr }; + case k_format_png: + return { ExportCodec::k_codec_png }; + case k_format_tiff: + return { ExportCodec::k_codec_tiff }; + case k_format_quick_time: + return { ExportCodec::k_codec_h264, ExportCodec::k_codec_h264rgb, + ExportCodec::k_codec_h265, ExportCodec::k_codec_pro_res, + ExportCodec::k_codec_cineform }; + case k_format_web_m: + return { ExportCodec::k_codec_a_v1, ExportCodec::k_codec_v_p9 }; + case k_format_ogg: + case k_format_wav: + case k_format_mpe_g4_audio: + case k_format_aiff: + case k_format_m_p3: + case k_format_flac: + case k_format_srt: + case k_format_count: + break; + } + + return {}; +} + +std::vector ExportFormat::get_audio_codecs(ExportFormat::Format f) +{ + switch (f) { + // Video/audio formats + case k_format_d_nx_hd: + return { ExportCodec::k_codec_pcm }; + case k_format_matroska: + return { ExportCodec::k_codec_aac, ExportCodec::k_codec_m_p2, + ExportCodec::k_codec_m_p3, ExportCodec::k_codec_pcm, + ExportCodec::k_codec_vorbis, ExportCodec::k_codec_opus, + ExportCodec::k_codec_flac }; + case k_format_mpe_g4_video: + case k_format_mpe_g4_audio: + return { ExportCodec::k_codec_aac, ExportCodec::k_codec_m_p2, + ExportCodec::k_codec_m_p3 }; + case k_format_quick_time: + return { ExportCodec::k_codec_aac, ExportCodec::k_codec_m_p2, + ExportCodec::k_codec_m_p3, ExportCodec::k_codec_pcm }; + case k_format_web_m: + return { ExportCodec::k_codec_opus, ExportCodec::k_codec_aac, + ExportCodec::k_codec_m_p2, ExportCodec::k_codec_m_p3, + ExportCodec::k_codec_pcm, ExportCodec::k_codec_vorbis }; + + // Audio only formats + case k_format_wav: + return { ExportCodec::k_codec_pcm }; + case k_format_aiff: + return { ExportCodec::k_codec_pcm }; + case k_format_m_p3: + return { ExportCodec::k_codec_m_p3 }; + case k_format_flac: + return { ExportCodec::k_codec_flac }; + case k_format_ogg: + return { ExportCodec::k_codec_opus, ExportCodec::k_codec_vorbis, + ExportCodec::k_codec_pcm }; + + // Video only formats + case k_format_open_exr: + case k_format_png: + case k_format_tiff: + case k_format_srt: + case k_format_count: + break; + } + + return {}; +} + +std::vector ExportFormat::get_subtitle_codecs(Format f) +{ + switch (f) { + case k_format_d_nx_hd: + case k_format_mpe_g4_video: + case k_format_mpe_g4_audio: + case k_format_open_exr: + case k_format_quick_time: + case k_format_png: + case k_format_tiff: + case k_format_wav: + case k_format_aiff: + case k_format_m_p3: + case k_format_flac: + case k_format_ogg: + case k_format_web_m: + case k_format_count: + break; + case k_format_matroska: + case k_format_srt: + return { ExportCodec::k_codec_srt }; + } + + return {}; +} + +std::vector ExportFormat::get_pixel_formats_for_codec(ExportFormat::Format f, + ExportCodec::Codec c) +{ + Encoder *e = Encoder::create_from_format(f, EncodingParams()); + std::vector list; + + if (e) { + list = e->get_pixel_formats_for_codec(c); + delete e; + } + + return list; +} + +std::vector +ExportFormat::get_sample_formats_for_codec(Format format, ExportCodec::Codec c) +{ + std::vector f; + Encoder *e = Encoder::create_from_format(format, EncodingParams()); + + if (e) { + f = e->get_sample_formats_for_codec(c); + delete e; + } + + return f; +} + +} diff --git a/src/codec/src/exportformat.h b/src/codec/src/exportformat.h new file mode 100644 index 000000000..d0c2eb506 --- /dev/null +++ b/src/codec/src/exportformat.h @@ -0,0 +1,75 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_EXPORTFORMAT_H +#define OAK_EXPORTFORMAT_H + +#include +#include + +#include "olive/core/render/sampleformat.h" + +#include "exportcodec.h" + +namespace olive +{ + +class ExportFormat { +public: + // Only append to this list (never insert) because indexes are used in serialized files + enum Format { + k_format_d_nx_hd, + k_format_matroska, + k_format_mpe_g4_video, + k_format_open_exr, + k_format_quick_time, + k_format_png, + k_format_tiff, + k_format_wav, + k_format_aiff, + k_format_m_p3, + k_format_flac, + k_format_ogg, + k_format_web_m, + k_format_srt, + k_format_mpe_g4_audio, + + k_format_count + }; + + static std::string get_name(Format f); + static std::string get_extension(Format f); + static std::vector + get_video_codecs(ExportFormat::Format f); + static std::vector + get_audio_codecs(ExportFormat::Format f); + static std::vector + get_subtitle_codecs(ExportFormat::Format f); + + static std::vector + get_pixel_formats_for_codec(Format f, ExportCodec::Codec c); + static std::vector + get_sample_formats_for_codec(Format f, ExportCodec::Codec c); +}; + +} + +#endif // OAK_EXPORTFORMAT_H diff --git a/src/codec/src/ffmpeg/CMakeLists.txt b/src/codec/src/ffmpeg/CMakeLists.txt new file mode 100644 index 000000000..90324c48c --- /dev/null +++ b/src/codec/src/ffmpeg/CMakeLists.txt @@ -0,0 +1,21 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive 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 . + +target_sources(oakcodec PRIVATE avframeptr.h ffmpegdecoder.cpp + ffmpegdecoder.h + ffmpegencoder.cpp + ffmpegencoder.h +) diff --git a/src/codec/src/ffmpeg/avframeptr.h b/src/codec/src/ffmpeg/avframeptr.h new file mode 100644 index 000000000..9c09aa1b0 --- /dev/null +++ b/src/codec/src/ffmpeg/avframeptr.h @@ -0,0 +1,139 @@ +/*** + + 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_CODEC_FFMPEG_AVFRAMEPTR_H +#define OAK_CODEC_FFMPEG_AVFRAMEPTR_H + +#include + +#include + +#include + +namespace olive +{ + +/** + * @brief C++ adapter around the ffmpeg_bridge frame handle + * + * Mirrors the AVFrame field access the codebase used to perform directly, + * but every operation goes through the pure C bridge API so the editor + * never touches FFmpeg itself. The underlying frame object always lives + * inside the bridge library. + */ +class AVFrame { +public: + AVFrame() : + handle_(fb_frame_alloc()) + { + } + + explicit AVFrame(FBFrame *handle) : + handle_(handle) + { + } + + ~AVFrame() + { + if (handle_) { + fb_frame_free(&handle_); + } + } + + AVFrame(const AVFrame &) = delete; + AVFrame &operator=(const AVFrame &) = delete; + + FBFrame *handle() const { return handle_; } + + int width() const { return fb_frame_get_width(handle_); } + void set_width(int w) { fb_frame_set_width(handle_, w); } + int height() const { return fb_frame_get_height(handle_); } + void set_height(int h) { fb_frame_set_height(handle_, h); } + int format() const { return fb_frame_get_format(handle_); } + void set_format(int f) { fb_frame_set_format(handle_, f); } + int64_t pts() const { return fb_frame_get_pts(handle_); } + void set_pts(int64_t p) { fb_frame_set_pts(handle_, p); } + int64_t best_effort_timestamp() const + { + return fb_frame_get_best_effort_timestamp(handle_); + } + int nb_samples() const { return fb_frame_get_nb_samples(handle_); } + void set_nb_samples(int n) { fb_frame_set_nb_samples(handle_, n); } + int sample_rate() const { return fb_frame_get_sample_rate(handle_); } + void set_sample_rate(int r) { fb_frame_set_sample_rate(handle_, r); } + int color_range() const { return fb_frame_get_color_range(handle_); } + void set_color_range(int r) { fb_frame_set_color_range(handle_, r); } + int colorspace() const { return fb_frame_get_colorspace(handle_); } + void set_colorspace(int cs) { fb_frame_set_colorspace(handle_, cs); } + uint64_t channel_layout_mask() const + { + return fb_frame_get_channel_layout_mask(handle_); + } + void set_channel_layout_mask(uint64_t m) + { + fb_frame_set_channel_layout_mask(handle_, m); + } + + bool is_hw() const { return fb_frame_is_hw(handle_) != 0; } + int hw_transfer_data(const AVFrame *src) + { + return fb_frame_hw_transfer_data(handle_, src->handle_); + } + int get_buffer(int align) { return fb_frame_get_buffer(handle_, align); } + int make_writable() { return fb_frame_make_writable(handle_); } + + uint8_t *data(int plane) { return fb_frame_get_data(handle_, plane); } + const uint8_t *data(int plane) const + { + return fb_frame_get_data_const(handle_, plane); + } + void set_data(int plane, uint8_t *d) + { + fb_frame_set_data(handle_, plane, d); + } + int linesize(int plane) const + { + return fb_frame_get_linesize(handle_, plane); + } + void set_linesize(int plane, int l) + { + fb_frame_set_linesize(handle_, plane, l); + } + +private: + FBFrame *handle_; +}; + +using AVFramePtr = std::shared_ptr; + +inline AVFramePtr create_av_frame_ptr(FBFrame *f) +{ + return std::make_shared(f); +} + +inline AVFramePtr create_av_frame_ptr() +{ + return std::make_shared(); +} + +} + +#endif // OAK_CODEC_FFMPEG_AVFRAMEPTR_H diff --git a/src/codec/src/ffmpeg/ffmpegdecoder.cpp b/src/codec/src/ffmpeg/ffmpegdecoder.cpp new file mode 100644 index 000000000..8870d03b8 --- /dev/null +++ b/src/codec/src/ffmpeg/ffmpegdecoder.cpp @@ -0,0 +1,1193 @@ +/*** + + 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 "ffmpegdecoder.h" + +#include +#include +#include +#include + +#include "common/ffmpegutils.h" +#include "common/subtitleparams.h" +#include "olive/core/util/timecodefunctions.h" +#include "planarfiledevice.h" +#include "timecodemetadata.h" + +namespace olive +{ + +using core::SampleFormat; +using core::Timecode; + +namespace +{ + +/** + * @brief NULL/empty-handle-safe check of an oakrender cancel atom + * (borrowed pointer, checked at every cancellation point) + */ +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; +} + +// Thin wrappers over the oakcommon_ffmpegutils_* C API (replacing the +// former FFmpegUtils C++ class) + +int ff_get_compatible_bridge_pixel_format(int pix_fmt, + int maximum_pix_fmt = -1) +{ + int out = pix_fmt; + oakcommon_ffmpegutils_get_compatible_bridge_pixel_format( + pix_fmt, maximum_pix_fmt, &out); + return out; +} + +int ff_convert_jpeg_space_to_regular_space(int pix_fmt) +{ + int out = pix_fmt; + oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space(pix_fmt, &out); + return out; +} + +int ff_get_native_sample_format(int smp_fmt) +{ + int out = -1; + oakcommon_ffmpegutils_get_native_sample_format(smp_fmt, &out); + return out; +} + +int ff_get_f_fmpeg_sample_format(int smp_fmt) +{ + int out = fb_sample_fmt_none; + oakcommon_ffmpegutils_get_ffmpeg_sample_format(smp_fmt, &out); + return out; +} + +/** + * @brief Flatten an OakVideoParams handle into the oakrender POD + * (needed at every texture creation point) + */ +void fill_render_params(const OakVideoParams ¶ms, + oakrender_video_params *out) +{ + *out = oakrender_video_params{}; + oakcommon_videoparams_get_width(params, &out->width); + oakcommon_videoparams_get_height(params, &out->height); + oakcommon_videoparams_get_time_base(params, &out->time_base_num, + &out->time_base_den); + oakcommon_videoparams_get_format(params, &out->format); + oakcommon_videoparams_get_pixel_aspect_ratio( + params, &out->pixel_aspect_num, &out->pixel_aspect_den); + oakcommon_videoparams_get_interlacing(params, &out->interlacing); + oakcommon_videoparams_get_color_range(params, &out->color_range); + oakcommon_videoparams_get_divider(params, &out->divider); + oakcommon_videoparams_get_video_type(params, &out->video_type); + oakcommon_videoparams_get_premultiplied_alpha(params, + &out->premultiplied_alpha); +} + +} // namespace + +static FramePtr copy_packed_av_frame_to_frame(const AVFramePtr &src, + PixelFormat format, int channel_count, + const Rational ×tamp) +{ + if (!src || !src->data(0)) { + return nullptr; + } + + OakVideoParams params = oakcommon_videoparams_init_basic( + src->width(), src->height(), format, channel_count, 1, 1, + OAKCOMMON_VIDEO_INTERLACE_NONE, 1); + FramePtr frame = Frame::create(); + frame->set_video_params(params); // Frame addrefs the handle + frame->set_timestamp(timestamp); + + int effective_width = 0; + oakcommon_videoparams_get_effective_width(params, &effective_width); + oakcommon_videoparams_free(¶ms); // Release our reference + + if (!frame->allocate()) { + return nullptr; + } + + const int row_bytes = + effective_width * + oakcommon_videoparams_static_get_bytes_per_pixel( + static_cast( + static_cast(format)), + channel_count); + for (int y = 0; y < frame->height(); y++) { + memcpy(frame->data() + y * frame->linesize_bytes(), + src->data(0) + y * src->linesize(0), size_t(row_bytes)); + } + + return frame; +} + +static int f_fmpeg_field_order_to_olive(int fo) +{ + switch (fo) { + case fb_field_order_tt: + return OAKCOMMON_VIDEO_INTERLACED_TOP_FIRST; + case fb_field_order_bb: + return OAKCOMMON_VIDEO_INTERLACED_BOTTOM_FIRST; + case fb_field_order_progressive: + default: + return OAKCOMMON_VIDEO_INTERLACE_NONE; + } +} + +namespace +{ + +int cancel_thunk(void *userdata) +{ + return cancel_atom_is_cancelled(static_cast(userdata)) ? + 1 : + 0; +} + +TimecodeMetadata::SourceTime extract_source_start_time(FBProbe *probe, + int stream_index, + const Rational &timebase, + int sample_rate) +{ + char buf[1024]; + + if (fb_probe_get_metadata(probe, stream_index, "timecode", buf, + sizeof(buf)) == 1) { + TimecodeMetadata::SourceTime parsed = + TimecodeMetadata::from_timecode_string(std::string(buf), timebase); + if (parsed.valid) { + return parsed; + } + } + + if (fb_probe_get_metadata(probe, stream_index, "time_reference", buf, + sizeof(buf)) == 1) { + TimecodeMetadata::SourceTime parsed = + TimecodeMetadata::from_bwf_time_reference(std::string(buf), + sample_rate); + if (parsed.valid) { + return parsed; + } + } + + return TimecodeMetadata::SourceTime(); +} + +struct SubtitleReadContext { + OakSubtitleParams sub; + Rational time_base; +}; + +void subtitle_read_thunk(int64_t pts, int64_t duration, const char *text, + int text_size, void *userdata) +{ + SubtitleReadContext *ctx = static_cast(userdata); + + Rational in = Timecode::timestamp_to_time(pts, ctx->time_base); + Rational out = Timecode::timestamp_to_time(pts + duration, ctx->time_base); + std::string str(text, size_t(text_size)); + + oakcommon_subtitleparams_add_subtitle(ctx->sub, in.numerator(), + in.denominator(), out.numerator(), + out.denominator(), str.c_str()); +} + +} // namespace + +FFmpegDecoder::FFmpegDecoder() + : scaler_(nullptr) + , working_packet_(nullptr) + , cache_at_zero_(false) + , cache_at_eof_(false) + , instance_(nullptr) + , stream_start_time_(0) + , stream_duration_(0) + , format_start_time_(FB_NOPTS_VALUE) + , input_sample_format_(fb_sample_fmt_none) + , input_sample_rate_(0) + , input_channel_layout_mask_(0) +{ +} + +bool FFmpegDecoder::open_internal() +{ + instance_ = fb_decoder_create(); + if (!instance_) { + return false; + } + + if (fb_decoder_open(instance_, stream().filename().c_str(), + stream().stream()) == 0) { + // Cache the stream parameters the decoder logic needs; the stream + // object itself always lives inside the bridge library + FBStreamInfo info; + if (fb_decoder_get_stream_info(instance_, &info) != 0) { + fb_decoder_free(&instance_); + return false; + } + + stream_time_base_ = Rational(info.time_base_num, info.time_base_den); + stream_start_time_ = info.start_time; + stream_duration_ = info.duration; + format_start_time_ = fb_decoder_get_format_start_time(instance_); + input_sample_format_ = info.sample_format; + input_sample_rate_ = info.sample_rate; + input_channel_layout_mask_ = info.channel_layout_mask; + + // Store one second in the source's timebase + second_ts_ = int64_t(std::llround(stream_time_base_.flipped().to_double())); + + working_packet_ = fb_packet_alloc(); + return true; + } + + fb_decoder_free(&instance_); + return false; +} + +OakRenderTexture * +FFmpegDecoder::process_frame_into_texture(AVFramePtr f, + const RetrieveVideoParams &p, + const AVFramePtr original) +{ + // NOTE: The original GPU shader paths (YUV->RGB conversion and + // deinterlacing via Renderer::blit_to_texture/ShaderJob/NodeValue) have + // no oakrender C API counterpart and were removed in the de-Qt wave. + // YUV frames are now converted to RGBA on the CPU in pre_process_frame() + // (is_pixel_format_glsl_compatible() only accepts directly-uploadable + // RGBA formats), so everything arriving here is uploaded verbatim. + + // Determine native format + int ideal_fmt = ff_get_compatible_bridge_pixel_format(f->format()); + PixelFormat native_fmt = get_native_pixel_format(ideal_fmt); + int native_channels = get_native_channel_count(ideal_fmt); + + // Determine pixel aspect ratio + int sar_num, sar_den; + Rational pixel_aspect_ratio(1, 1); + if (fb_decoder_guess_sample_aspect_ratio(instance_, nullptr, &sar_num, + &sar_den) == 0 && + sar_den != 0) { + pixel_aspect_ratio = Rational(sar_num, sar_den); + } + + // Set up video params + OakVideoParams vp = oakcommon_videoparams_init_basic( + original->width(), original->height(), native_fmt, native_channels, + pixel_aspect_ratio.numerator(), pixel_aspect_ratio.denominator(), + OAKCOMMON_VIDEO_INTERLACE_NONE, p.divider); + + // Create texture and upload the (CPU-converted) frame; the bridge + // linesize is already in bytes, which is what the C API expects + oakrender_video_params rvp; + fill_render_params(vp, &rvp); + oakcommon_videoparams_free(&vp); + + OakRenderTexture *tex = + oakrender_display_texture_create(p.renderer, &rvp, nullptr, 0); + if (!tex) { + return nullptr; + } + + if (oakrender_display_texture_upload(tex, f->data(0), f->linesize(0)) != + 0) { + oakrender_display_texture_free(tex); + return nullptr; + } + + return tex; +} + +OakRenderTexture * +FFmpegDecoder::retrieve_video_internal(const RetrieveVideoParams &p) +{ + if (AVFramePtr f = retrieve_frame(p.time, p.cancelled)) { + if (cancel_atom_is_cancelled(p.cancelled)) { + return nullptr; + } + + AVFramePtr original = f; + + // Disregard "JPEG" pixel formats because we allow the user to override that + f->set_format(ff_convert_jpeg_space_to_regular_space(f->format())); + + // Force frame's color range to whatever it's set to in Olive + f->set_color_range(p.force_range == OAKCOMMON_COLOR_RANGE_FULL ? + fb_color_range_jpeg : + fb_color_range_mpeg); + + // Perform any CPU processing required + AVFramePtr ptr = pre_process_frame(f, p); + f = std::move(ptr); + if (!f) { + fprintf(stderr, "PreProcessFrame failed\n"); + return nullptr; + } + + // Finally, upload to a texture + OakRenderTexture *texture = process_frame_into_texture(f, p, original); + + if (!texture) { + fprintf(stderr, "ProcessFrameIntoTexture returned null\n"); + } + + return texture; + } + + return nullptr; +} + +FramePtr FFmpegDecoder::retrieve_video_frame_internal(const RetrieveVideoParams &p) +{ + if (AVFramePtr f = retrieve_frame(p.time, p.cancelled)) { + if (cancel_atom_is_cancelled(p.cancelled)) { + return nullptr; + } + + f->set_format(ff_convert_jpeg_space_to_regular_space(f->format())); + f->set_color_range(p.force_range == OAKCOMMON_COLOR_RANGE_FULL ? + fb_color_range_jpeg : + fb_color_range_mpeg); + + AVFramePtr dest = create_av_frame_ptr(); + dest->set_width(f->width()); + dest->set_height(f->height()); + dest->set_format(p.maximum_format == PixelFormat::u8 ? + fb_pix_fmt_rgba : + fb_pix_fmt_rgb_a64_le); + dest->set_color_range(f->color_range()); + dest->set_colorspace(f->colorspace()); + if (p.divider > 1) { + dest->set_width( + oakcommon_videoparams_get_scaled_dimension(dest->width(), + p.divider)); + dest->set_height( + oakcommon_videoparams_get_scaled_dimension(dest->height(), + p.divider)); + } + + int r = dest->get_buffer(0); + if (r < 0) { + f_fmpeg_error(r); + return nullptr; + } + + FBScaler *cpu_scaler = fb_scaler_create(f->width(), f->height(), + f->format(), dest->width(), + dest->height(), dest->format(), + FB_SCALER_POINT); + if (!cpu_scaler) { + fprintf(stderr, "Failed to create CPU frame conversion context\n"); + return nullptr; + } + + fb_scaler_set_colorspace(cpu_scaler, dest->colorspace(), + dest->color_range() == fb_color_range_jpeg); + + r = fb_scaler_scale_frame(cpu_scaler, dest->handle(), f->handle()); + fb_scaler_free(&cpu_scaler); + if (r < 0) { + f_fmpeg_error(r); + return nullptr; + } + + // sws_scale does not initialize the alpha channel when converting + // from non-alpha source formats (e.g. YUV). fb_frame_get_buffer + // zero-initializes the destination, leaving alpha at 0. The color + // management shader later multiplies RGB by alpha, producing black. + // Ensure alpha is opaque for source formats that have no alpha. + if (!fb_pix_fmt_has_alpha(f->format())) { + const int bpc = (dest->format() == fb_pix_fmt_rgba) ? 1 : 2; + const int stride = dest->linesize(0); + for (int y = 0; y < dest->height(); ++y) { + uint8_t *row = dest->data(0) + y * stride; + for (int x = 0; x < dest->width(); ++x) { + if (bpc == 1) { + row[x * 4 + 3] = 0xFF; + } else { + *reinterpret_cast(row + x * 8 + 6) = 0xFFFF; + } + } + } + } + + return copy_packed_av_frame_to_frame(dest, + dest->format() == fb_pix_fmt_rgba ? + PixelFormat::u8 : + PixelFormat::u16, + OAKCOMMON_RGBA_CHANNEL_COUNT, p.time); + } + + return nullptr; +} + +void FFmpegDecoder::close_internal() +{ + if (working_packet_) { + fb_packet_free(&working_packet_); + working_packet_ = nullptr; + } + + clear_frame_cache(); + free_scaler(); + + if (instance_) { + fb_decoder_free(&instance_); + } +} + +Rational FFmpegDecoder::get_audio_start_offset() const +{ + if (instance_) { + Rational fmt_start = Rational(format_start_time_, FB_TIME_BASE); + Rational str_start = stream_time_base_ * stream_start_time_; + return str_start - fmt_start; + } else { + return 0; + } +} + +std::string FFmpegDecoder::id() const +{ + return "ffmpeg"; +} + +FootageDescription FFmpegDecoder::probe(const std::string &filename, + OakCancelAtom *cancelled) const +{ + // Return value + FootageDescription desc(id()); + + // Variable for receiving errors from the bridge + int error_code; + + // C string for the bridge API + const char *filename_c = filename.c_str(); + + // Open file in the bridge library + FBProbe *probe = fb_probe_create(); + error_code = fb_probe_open(probe, filename_c); + + // Handle open error + if (error_code == 0) { + int64_t footage_duration = fb_probe_get_duration(probe); + TimecodeMetadata::SourceTime source_start_time = extract_source_start_time( + probe, -1, Rational(1, FB_TIME_BASE), 0); + + bool duration_guessed_from_bitrate = + fb_probe_duration_from_bitrate(probe) != 0; + if (duration_guessed_from_bitrate) { + fprintf(stderr, "Unreliable duration detected - we will manually determine it ourselves (this may take some time)\n"); + } + + // Dump it into the Footage object + int video_streams = 0, audio_streams = 0, still_streams = 0; + + int stream_count = fb_probe_get_stream_count(probe); + for (int i = 0; i < stream_count; i++) { + FBStreamInfo info; + if (fb_probe_get_stream_info(probe, i, &info) != 0) { + continue; + } + + Rational stream_tb(info.time_base_num, info.time_base_den); + + if (!source_start_time.valid) { + source_start_time = extract_source_start_time(probe, i, stream_tb, + info.sample_rate); + } + + // Only proceed if a decoder exists for this stream + if (!info.has_decoder) { + continue; + } + + if (info.codec_type == fb_media_type_video) { + // Read at least two frames to get more information about this video stream + int interlacing = OAKCOMMON_VIDEO_INTERLACE_NONE; + Rational pixel_aspect_ratio(1, 1); + Rational frame_rate(info.avg_frame_rate_num, + info.avg_frame_rate_den); + int compatible_pix_fmt = + ff_get_compatible_bridge_pixel_format(info.pixel_format); + bool image_is_still = false; + int64_t stream_duration = info.duration; + + int decode_full_duration = + (info.duration == FB_NOPTS_VALUE || + duration_guessed_from_bitrate) ? + 1 : + 0; + + FBVideoStreamDetails details; + if (fb_probe_video_stream_details(filename_c, i, &details, + decode_full_duration, + cancel_thunk, + cancelled) == 0) { + interlacing = f_fmpeg_field_order_to_olive(details.field_order); + if (details.pixel_aspect_den != 0) { + pixel_aspect_ratio = Rational(details.pixel_aspect_num, + details.pixel_aspect_den); + } + if (details.frame_rate_num != 0 && + details.frame_rate_den != 0) { + frame_rate = Rational(details.frame_rate_num, + details.frame_rate_den); + } + image_is_still = details.is_still != 0; + if (details.decoded_duration != FB_NOPTS_VALUE) { + stream_duration = details.decoded_duration; + } + } + + OakVideoParams stream = oakcommon_videoparams_init(); + oakcommon_videoparams_set_stream_index(stream, i); + oakcommon_videoparams_set_width(stream, info.width); + oakcommon_videoparams_set_height(stream, info.height); + oakcommon_videoparams_set_video_type( + stream, image_is_still ? OAKCOMMON_VIDEO_TYPE_STILL : + OAKCOMMON_VIDEO_TYPE_VIDEO); + oakcommon_videoparams_set_format( + stream, get_native_pixel_format(compatible_pix_fmt)); + oakcommon_videoparams_set_channel_count( + stream, get_native_channel_count(compatible_pix_fmt)); + oakcommon_videoparams_set_interlacing(stream, interlacing); + oakcommon_videoparams_set_pixel_aspect_ratio( + stream, pixel_aspect_ratio.numerator(), + pixel_aspect_ratio.denominator()); + oakcommon_videoparams_set_frame_rate( + stream, frame_rate.numerator(), frame_rate.denominator()); + oakcommon_videoparams_set_start_time(stream, info.start_time); + oakcommon_videoparams_set_time_base( + stream, stream_tb.numerator(), stream_tb.denominator()); + oakcommon_videoparams_set_duration(stream, stream_duration); + oakcommon_videoparams_set_color_range( + stream, info.color_range == fb_color_range_jpeg ? + OAKCOMMON_COLOR_RANGE_FULL : + OAKCOMMON_COLOR_RANGE_LIMITED); + oakcommon_videoparams_set_color_primaries(stream, + info.color_primaries); + oakcommon_videoparams_set_color_transfer(stream, + info.color_trc); + oakcommon_videoparams_set_premultiplied_alpha(stream, 0); + + desc.add_video_stream(stream); + oakcommon_videoparams_free(&stream); + image_is_still ? still_streams++ : video_streams++; + + } else if (info.codec_type == fb_media_type_audio) { + int64_t stream_duration = info.duration; + + if (stream_duration == FB_NOPTS_VALUE || + duration_guessed_from_bitrate) { + // Loop through stream until we get the whole duration + if (footage_duration == FB_NOPTS_VALUE || + duration_guessed_from_bitrate) { + int64_t decoded_duration = FB_NOPTS_VALUE; + if (fb_probe_audio_stream_duration( + filename_c, i, &decoded_duration, cancel_thunk, + cancelled) == 0) { + stream_duration = decoded_duration; + } + } else { + stream_duration = Timecode::rescale_timestamp_ceil( + footage_duration, Rational(1, FB_TIME_BASE), + stream_tb); + } + } + + AudioParams stream; + stream.set_stream_index(i); + stream.set_channel_layout(info.channel_layout_mask); + stream.set_sample_rate(info.sample_rate); + stream.set_format(static_cast( + ff_get_native_sample_format(info.sample_format))); + stream.set_time_base(stream_tb); + stream.set_duration(stream_duration); + desc.add_audio_stream(stream); + + audio_streams++; + + } else if (info.codec_type == fb_media_type_subtitle) { + // The bridge limits this to SRT, matching our historical behavior + OakSubtitleParams sub = oakcommon_subtitleparams_init(); + SubtitleReadContext ctx = { sub, stream_tb }; + + if (fb_probe_read_subtitle_stream(filename_c, i, + subtitle_read_thunk, + &ctx) == 0) { + desc.add_subtitle_stream(sub); + } + oakcommon_subtitleparams_free(&sub); + } + } + + desc.set_stream_count(stream_count); + if (source_start_time.valid) { + desc.set_source_start_time(source_start_time.time, + source_start_time.source); + } + + if (video_streams == 0 && audio_streams > 0 && still_streams > 0) { + // This footage has no video streams, but has audio and image streams. We've probably + // imported a song with embedded album art that most people don't care about. We'll keep the + // stills referenced in case users do, but we'll default them to disabled so they're + // easier to work with. + for (const OakVideoParams &vp : desc.get_video_streams()) { + oakcommon_videoparams_set_enabled(vp, 0); + } + } + } + + // Free all memory + fb_probe_free(&probe); + + return desc; +} + +std::string FFmpegDecoder::f_fmpeg_error(int error_code) +{ + char err[1024]; + fb_error_string(error_code, err, 512); + + char msg[1280]; + snprintf(msg, sizeof(msg), "%d %s", error_code, err); + return msg; +} + +bool FFmpegDecoder::conform_audio_internal( + const std::vector &filenames, const AudioParams ¶ms, + OakCancelAtom *cancelled) +{ + // Iterate through each audio frame and extract the PCM data + + // Seek to starting point + fb_decoder_seek(instance_, 0); + + // The channel layout was validated by the bridge when the stream info was read + if (!input_channel_layout_mask_) { + fprintf(stderr, "Failed to determine channel layout of audio file, could not conform\n"); + return false; + } + + // Create resampler + FBResampler *resampler = fb_resampler_create( + params.channel_layout(), + ff_get_f_fmpeg_sample_format(params.format()), + params.sample_rate(), input_channel_layout_mask_, input_sample_format_, + input_sample_rate_); + if (!resampler) { + fprintf(stderr, "Failed to create resampler, could not conform\n"); + return false; + } + + FBPacket *pkt = fb_packet_alloc(); + FBFrame *frame = fb_frame_alloc(); + int ret; + + bool success = false; + + int64_t duration = stream_duration_; + if (duration == 0 || duration == FB_NOPTS_VALUE) { + duration = fb_decoder_get_format_duration(instance_); + if (!(duration == 0 || duration == FB_NOPTS_VALUE)) { + // Rescale from format timebase to stream timebase + duration = Timecode::rescale_timestamp_ceil( + duration, Rational(1, FB_TIME_BASE), stream_time_base_); + } + } + + PlanarFileDevice wave_out; + if (wave_out.open(filenames, PlanarFileDevice::k_write_only)) { + int nb_channels = params.channel_count(); + SampleBuffer data; + data.set_audio_params(params); + + while (true) { + // Check if we have a `cancelled` ptr and its value + if (cancel_atom_is_cancelled(cancelled)) { + break; + } + + ret = fb_decoder_get_frame(instance_, pkt, frame); + + if (ret < 0) { + if (ret == FB_ERROR_EOF) { + success = true; + } else { + char err_str[512]; + fb_error_string(ret, err_str, 512); + fprintf(stderr, "Failed to conform: %d %s\n", ret, err_str); + } + break; + } + + // Allocate buffers + int nb_samples = + fb_resampler_get_out_samples(resampler, + fb_frame_get_nb_samples(frame)); + int nb_bytes_per_channel = + params.samples_to_bytes(nb_samples) / nb_channels; + data.set_sample_count(nb_bytes_per_channel); + data.allocate(); + + // Resample audio to our destination parameters + nb_samples = fb_resampler_convert_frame( + resampler, + reinterpret_cast(data.to_raw_ptrs().data()), + nb_samples, frame); + + // If no error, write to files + if (nb_samples > 0) { + // Update byte count for the number of samples we actually received + nb_bytes_per_channel = + params.samples_to_bytes(nb_samples) / nb_channels; + + // Write to files + wave_out.write( + const_cast( + reinterpret_cast(data.to_raw_ptrs().data())), + nb_bytes_per_channel); + } + + // Free buffer + data.destroy(); + + // Handle error now after freeing + if (nb_samples < 0) { + char err_str[512]; + fb_error_string(nb_samples, err_str, 512); + fprintf(stderr, "libswresample failed with error: %d %s\n", + nb_samples, err_str); + break; + } + + signal_processing_progress(fb_frame_get_best_effort_timestamp(frame), + duration); + } + + wave_out.close(); + } else { + fprintf(stderr, "Failed to open WAVE output for indexing\n"); + } + + fb_resampler_free(&resampler); + + fb_frame_free(&frame); + fb_packet_free(&pkt); + + return success; +} + +PixelFormat FFmpegDecoder::get_native_pixel_format(int pix_fmt) +{ + switch (pix_fmt) { + case fb_pix_fmt_rg_b24: + case fb_pix_fmt_rgba: + return PixelFormat::u8; + case fb_pix_fmt_rg_b48_le: + case fb_pix_fmt_rgb_a64_le: + return PixelFormat::u16; + case fb_pix_fmt_rgb_f32_le: + case fb_pix_fmt_rgba_f32_le: + return PixelFormat::f32; + default: + return PixelFormat::invalid; + } +} + +int FFmpegDecoder::get_native_channel_count(int pix_fmt) +{ + switch (pix_fmt) { + case fb_pix_fmt_rg_b24: + case fb_pix_fmt_rg_b48_le: + case fb_pix_fmt_rgb_f32_le: + return OAKCOMMON_RGB_CHANNEL_COUNT; + case fb_pix_fmt_rgba: + case fb_pix_fmt_rgb_a64_le: + case fb_pix_fmt_rgba_f32_le: + return OAKCOMMON_RGBA_CHANNEL_COUNT; + default: + return 0; + } +} + +bool FFmpegDecoder::is_pixel_format_glsl_compatible(int f) +{ + // NOTE: This used to include the YUV formats because they could be + // converted with the yuv2rgb GLSL shader. The oakrender C API has no + // generic shader-blit function, so only directly-uploadable formats are + // considered "GLSL compatible" now; everything else is converted to RGBA + // on the CPU in pre_process_frame(). + switch (f) { + case fb_pix_fmt_rgba: + case fb_pix_fmt_rgb_a64_le: + case fb_pix_fmt_rgba_f32_le: + return true; + default: + return false; + } +} + +void FFmpegDecoder::clear_frame_cache() +{ + if (!cached_frames_.empty()) { + cached_frames_.clear(); + cache_at_eof_ = false; + cache_at_zero_ = false; + } +} + +AVFramePtr FFmpegDecoder::pre_process_frame(AVFramePtr f, + const RetrieveVideoParams &p) +{ + // In pre-processing, we try to achieve the following: + // - If a divider is being used, scale down the image + // - If a pixel format is not directly uploadable to a texture, convert it to RGBA ourselves + + if (p.divider == 1 && is_pixel_format_glsl_compatible(f->format())) { + // No CPU processing required, the user wants this in full resolution and the pixel format can + // be uploaded as-is + return f; + } + + // Some scaling and/or format conversion needs to be done + AVFramePtr dest = create_av_frame_ptr(); + + dest->set_width(f->width()); + dest->set_height(f->height()); + dest->set_format(f->format()); + dest->set_color_range(f->color_range()); + dest->set_colorspace(f->colorspace()); + if (p.divider > 1) { + dest->set_width( + oakcommon_videoparams_get_scaled_dimension(dest->width(), + p.divider)); + dest->set_height( + oakcommon_videoparams_get_scaled_dimension(dest->height(), + p.divider)); + } + + if (!is_pixel_format_glsl_compatible(dest->format())) { + dest->set_format(ff_get_compatible_bridge_pixel_format( + dest->format(), p.maximum_format)); + } + + // swscale does not support RGBAF32 as output, fallback to RGBA64 + if (dest->format() == fb_pix_fmt_rgba_f32_le) { + dest->set_format(fb_pix_fmt_rgb_a64_le); + } + + int r = dest->get_buffer(0); + if (r < 0) { + f_fmpeg_error(r); + return nullptr; + } + + if (!scaler_ || scaler_src_width_ != f->width() || + scaler_src_height_ != f->height() || + scaler_src_format_ != f->format() || + scaler_dst_width_ != dest->width() || + scaler_dst_height_ != dest->height() || + scaler_dst_format_ != dest->format() || + scaler_colrange_ != dest->color_range() || + scaler_colspace_ != dest->colorspace()) { + // Scaler must be recreated, destroy current if it exists + free_scaler(); + + // Cache info + scaler_src_width_ = f->width(); + scaler_src_height_ = f->height(); + scaler_src_format_ = f->format(); + scaler_dst_width_ = dest->width(); + scaler_dst_height_ = dest->height(); + scaler_dst_format_ = dest->format(); + scaler_colrange_ = dest->color_range(); + scaler_colspace_ = dest->colorspace(); + + // Create new scaler + scaler_ = fb_scaler_create(scaler_src_width_, scaler_src_height_, + scaler_src_format_, scaler_dst_width_, + scaler_dst_height_, scaler_dst_format_, + FB_SCALER_POINT); + + // Set the scaler's colorspace details + fb_scaler_set_colorspace( + scaler_, scaler_colspace_, + scaler_colrange_ == fb_color_range_jpeg); + } + + r = fb_scaler_scale_frame(scaler_, dest->handle(), f->handle()); + + if (r < 0) { + f_fmpeg_error(r); + return nullptr; + } + + return dest; +} + +AVFramePtr FFmpegDecoder::retrieve_frame(const Rational &time, + OakCancelAtom *cancelled) +{ + int64_t target_ts = Timecode::time_to_timestamp(time, stream_time_base_); + + if (format_start_time_ != FB_NOPTS_VALUE) { + target_ts += Timecode::rescale_timestamp(format_start_time_, + Rational(1, FB_TIME_BASE), + stream_time_base_); + } + + const int64_t min_seek = 0; + int64_t seek_ts = std::max(min_seek, target_ts - maximum_queue_size()); + bool still_seeking = false; + + if (time != k_any_timecode) { + // If the frame wasn't in the frame cache, see if this frame cache is too old to use + if (cached_frames_.empty() || + (target_ts < cached_frames_.front()->pts() || + target_ts > cached_frames_.back()->pts() + 2 * second_ts_)) { + clear_frame_cache(); + + fb_decoder_seek(instance_, seek_ts); + if (seek_ts == min_seek) { + cache_at_zero_ = true; + } + + still_seeking = true; + } else { + // Search cache for frame + AVFramePtr cached_frame = get_frame_from_cache(target_ts); + if (cached_frame) { + return cached_frame; + } + } + } + + int ret; + AVFramePtr return_frame = nullptr; + AVFramePtr filtered = nullptr; + bool retried_after_eof = false; + + while (true) { + // Break out of loop if we've cancelled + if (cancel_atom_is_cancelled(cancelled)) { + break; + } + + if (!filtered) { + filtered = create_av_frame_ptr(); + } + + // Pull from the decoder + ret = fb_decoder_get_frame(instance_, working_packet_, + filtered->handle()); + + if (cancel_atom_is_cancelled(cancelled)) { + break; + } + + // Handle any errors that aren't EOF (EOF is handled later on) + if (ret < 0 && ret != FB_ERROR_EOF) { + fprintf(stderr, "Failed to retrieve frame: %d\n", ret); + break; + } + + if (still_seeking) { + // Handle a failure to seek (occurs on some media) + // We'll only be here if the frame cache was emptied earlier + if (!cache_at_zero_ && + (ret == FB_ERROR_EOF || + filtered->best_effort_timestamp() > target_ts)) { + seek_ts = std::max(min_seek, seek_ts - second_ts_); + fb_decoder_seek(instance_, seek_ts); + if (seek_ts == min_seek) { + cache_at_zero_ = true; + } + continue; + + } else { + still_seeking = false; + } + } + + if (ret == FB_ERROR_EOF) { + // Handle an "expected" EOF by using the last frame of our cache + cache_at_eof_ = true; + + if (cached_frames_.empty()) { + if (!retried_after_eof) { + retried_after_eof = true; + clear_frame_cache(); + fb_decoder_seek(instance_, min_seek); + cache_at_zero_ = true; + still_seeking = true; + continue; + } + + fprintf(stderr, "Unexpected codec EOF - unable to retrieve frame\n"); + } else { + return_frame = cached_frames_.back(); + } + + break; + + } else { + // Cut down to thread count - 1 before we acquire a new frame + if (cached_frames_.size() > size_t(maximum_queue_size())) { + remove_first_frame(); + } + + // Store frame before just in case + AVFramePtr previous; + if (cached_frames_.empty()) { + previous = nullptr; + } else { + previous = cached_frames_.back(); + } + + // Transfer hardware decoded frames to system memory before caching. + filtered = transfer_hardware_frame(filtered); + + // Append this frame and signal to other threads that a new frame has arrived + cached_frames_.push_back(filtered); + + // If this is a valid frame, see if this or the frame before it are the one we need + if (filtered->pts() == target_ts || time == k_any_timecode) { + return_frame = filtered; + break; + } else if (filtered->pts() > target_ts) { + if (!previous && cache_at_zero_) { + return_frame = filtered; + break; + } else { + return_frame = previous; + break; + } + } + } + + filtered = nullptr; + } + + fb_packet_unref(working_packet_); + + return return_frame; +} + +AVFramePtr FFmpegDecoder::transfer_hardware_frame(AVFramePtr f) +{ + if (!fb_decoder_hwaccel_enabled(instance_) || + !fb_frame_is_hw(f->handle())) { + return f; + } + + FBFrame *sw_frame = fb_frame_alloc(); + if (!sw_frame) { + fprintf(stderr, "Failed to allocate software frame for hardware transfer\n"); + return nullptr; + } + + int ret = fb_frame_hw_transfer_data(sw_frame, f->handle()); + if (ret < 0) { + fprintf(stderr, "Failed to transfer hardware frame to system memory: %s\n", + f_fmpeg_error(ret).c_str()); + fb_frame_free(&sw_frame); + return nullptr; + } + + ret = fb_frame_copy_props(sw_frame, f->handle()); + if (ret < 0) { + fprintf(stderr, "Failed to copy frame properties during hardware transfer: %s\n", + f_fmpeg_error(ret).c_str()); + } + + return create_av_frame_ptr(sw_frame); +} + +void FFmpegDecoder::free_scaler() +{ + if (scaler_) { + fb_scaler_free(&scaler_); + } +} + +AVFramePtr FFmpegDecoder::get_frame_from_cache(const int64_t &t) const +{ + if (t < cached_frames_.front()->pts()) { + if (cache_at_zero_) { + return cached_frames_.front(); + } + + } else if (t > cached_frames_.back()->pts()) { + if (cache_at_eof_) { + return cached_frames_.back(); + } + + } else { + // We already have this frame in the cache, find it + for (auto it = cached_frames_.cbegin(); it != cached_frames_.cend(); + it++) { + AVFramePtr this_frame = *it; + + auto next = it; + next++; + + if (this_frame->pts() == t // Test for an exact match + || + (next != cached_frames_.cend() && + (*next)->pts() > t)) { // Or for this frame to be the "closest" + + return this_frame; + } + } + } + + return nullptr; +} + +void FFmpegDecoder::remove_first_frame() +{ + cached_frames_.pop_front(); + cache_at_zero_ = false; +} + +int FFmpegDecoder::maximum_queue_size() +{ + // Fairly arbitrary size. This used to need to be the number of current threads to ensure any + // thread that arrived would have its frame available, but if we only have one render thread, + // that's no longer a concern. Now, this value could technically be 1, but some memory cache + // may be useful for reversing. This value may be tweaked over time. + return 2; +} + +} diff --git a/src/codec/src/ffmpeg/ffmpegdecoder.h b/src/codec/src/ffmpeg/ffmpegdecoder.h new file mode 100644 index 000000000..6ad76826a --- /dev/null +++ b/src/codec/src/ffmpeg/ffmpegdecoder.h @@ -0,0 +1,151 @@ +/*** + + 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_FFMPEGDECODER_H +#define OAK_FFMPEGDECODER_H + +#include + +#include +#include +#include + +#include + +#include "decoder.h" +#include "ffmpeg/avframeptr.h" + +namespace olive +{ + +/** + * @brief A Decoder derivative that uses the ffmpeg_bridge library as an Olive decoder + * + * All media access goes through the pure C API of the ffmpeg_bridge shared + * library; this class never sees an FFmpeg structure or function. + */ +class FFmpegDecoder : public Decoder { +public: + // Constructor + FFmpegDecoder(); + + // Destructor + DECODER_DEFAULT_DESTRUCTOR(FFmpegDecoder) + + virtual std::string id() const override; + + virtual bool supports_video() override + { + return true; + } + virtual bool supports_audio() override + { + return true; + } + + virtual FootageDescription probe(const std::string &filename, + OakCancelAtom *cancelled) const override; + +protected: + virtual bool open_internal() override; + virtual OakRenderTexture * + retrieve_video_internal(const RetrieveVideoParams &p) override; + virtual FramePtr + retrieve_video_frame_internal(const RetrieveVideoParams &p) override; + virtual bool + conform_audio_internal(const std::vector &filenames, + const AudioParams ¶ms, + OakCancelAtom *cancelled) override; + virtual void close_internal() override; + + virtual Rational get_audio_start_offset() const override; + +private: + /** + * @brief Handle a bridge error code + * + * Uses the bridge API to retrieve a descriptive string for this error code and sends it to Error(). As such, this + * function also automatically closes the Decoder. + * + * @param error_code + */ + static std::string f_fmpeg_error(int error_code); + + void free_scaler(); + + AVFramePtr transfer_hardware_frame(AVFramePtr f); + + static PixelFormat get_native_pixel_format(int pix_fmt); + static int get_native_channel_count(int pix_fmt); + + static bool is_pixel_format_glsl_compatible(int f); + + AVFramePtr get_frame_from_cache(const int64_t &t) const; + + void clear_frame_cache(); + + AVFramePtr pre_process_frame(AVFramePtr f, const RetrieveVideoParams &p); + + OakRenderTexture *process_frame_into_texture(AVFramePtr f, + const RetrieveVideoParams &p, + const AVFramePtr original); + + AVFramePtr retrieve_frame(const Rational &time, OakCancelAtom *cancelled); + + void remove_first_frame(); + + static int maximum_queue_size(); + + FBScaler *scaler_; + int scaler_src_width_; + int scaler_src_height_; + int scaler_src_format_; + int scaler_dst_width_; + int scaler_dst_height_; + int scaler_dst_format_; + int scaler_colrange_; + int scaler_colspace_; + + FBPacket *working_packet_; + + int64_t second_ts_; + + std::list cached_frames_; + + bool cache_at_zero_; + bool cache_at_eof_; + + FBDecoder *instance_; + + // Stream parameters cached on open (the stream object itself lives + // inside the bridge library) + Rational stream_time_base_; + int64_t stream_start_time_; + int64_t stream_duration_; + int64_t format_start_time_; + int input_sample_format_; + int input_sample_rate_; + uint64_t input_channel_layout_mask_; +}; + +} + +#endif // OAK_FFMPEGDECODER_H diff --git a/src/codec/src/ffmpeg/ffmpegencoder.cpp b/src/codec/src/ffmpeg/ffmpegencoder.cpp new file mode 100644 index 000000000..7a02a29a1 --- /dev/null +++ b/src/codec/src/ffmpeg/ffmpegencoder.cpp @@ -0,0 +1,547 @@ +/*** + + 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 "ffmpegencoder.h" + +#include +#include +#include +#include +#include + +#include "common/ffmpegutils.h" +#include "common/subtitleparams.h" +#include "common/videoparams.h" + +namespace olive +{ + +namespace +{ + +std::string to_lower(const std::string &s) +{ + std::string r = s; + std::transform(r.begin(), r.end(), r.begin(), + [](unsigned char c) { return char(std::tolower(c)); }); + return r; +} + +bool contains(const std::string &haystack, const std::string &needle) +{ + return haystack.find(needle) != std::string::npos; +} + +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 colortransform_get_output(OakColorTransform ct) +{ + std::string result; + int size = oakcommon_colortransform_get_output(ct, nullptr, 0); + if (size > 0) { + result.assign(size_t(size) - 1, '\0'); + oakcommon_colortransform_get_output(ct, result.data(), size); + } + return result; +} + +} // namespace + +FFmpegEncoder::FFmpegEncoder(const EncodingParams ¶ms) + : Encoder(params) + , encoder_(nullptr) + , open_(false) +{ +} + +bool FFmpegEncoder::get_color_tags_for_colorspace(const std::string &colorspace, + int *primaries, int *trc, + int *matrix) +{ + const std::string name = to_lower(colorspace); + + if (contains(name, "pq") || contains(name, "2084")) { + *primaries = fb_color_primaries_bt2020; + *trc = fb_color_trc_pq; + *matrix = fb_col_spc_b_t2020_ncl; + return true; + } + + if (contains(name, "hlg")) { + *primaries = fb_color_primaries_bt2020; + *trc = fb_color_trc_hlg; + *matrix = fb_col_spc_b_t2020_ncl; + return true; + } + + if (contains(name, "2020")) { + *primaries = fb_color_primaries_bt2020; + *trc = fb_color_trc_bt709; + *matrix = fb_col_spc_b_t2020_ncl; + return true; + } + + if (contains(name, "p3")) { + *primaries = fb_color_primaries_smpte432; + *trc = fb_color_trc_srgb; + *matrix = fb_col_spc_b_t709; + return true; + } + + if (contains(name, "srgb")) { + *primaries = fb_color_primaries_bt709; + *trc = fb_color_trc_srgb; + *matrix = fb_col_spc_b_t709; + return true; + } + + if (contains(name, "pal")) { + *primaries = fb_color_primaries_bt470bg; + *trc = fb_color_trc_gamma28; + *matrix = fb_col_spc_b_t470_bg; + return true; + } + + if (contains(name, "ntsc")) { + *primaries = fb_color_primaries_smpte170m; + *trc = fb_color_trc_smpte170m; + *matrix = fb_col_spc_smpt_e170_m; + return true; + } + + if (contains(name, "1886") || contains(name, "709")) { + *primaries = fb_color_primaries_bt709; + *trc = fb_color_trc_bt709; + *matrix = fb_col_spc_b_t709; + return true; + } + + return false; +} + +std::vector +FFmpegEncoder::get_pixel_formats_for_codec(ExportCodec::Codec c) const +{ + std::vector pix_fmts; + + int bridge_codec = export_codec_to_bridge(c); + if (bridge_codec != fb_codec_none) { + int count = + fb_encoder_codec_get_pixel_formats(bridge_codec, nullptr, 0); + if (count > 0) { + std::vector names(static_cast(count)); + fb_encoder_codec_get_pixel_formats(bridge_codec, names.data(), + count); + for (int i = 0; i < count; i++) { + pix_fmts.push_back(names[size_t(i)] ? names[size_t(i)] : ""); + } + } + } + + return pix_fmts; +} + +std::vector +FFmpegEncoder::get_sample_formats_for_codec(ExportCodec::Codec c) const +{ + std::vector f; + + if (c == ExportCodec::k_codec_pcm) { + // FFmpeg lists these as separate codecs so we need custom functionality here + // We list signed 16 first because ExportDialog will always use the first element by default + // (because first element is the "default" in FFmpeg) + f = { SampleFormat::s16, SampleFormat::u8, SampleFormat::s32, + SampleFormat::s64, SampleFormat::f32, SampleFormat::f64 }; + } else { + int bridge_codec = export_codec_to_bridge(c); + if (bridge_codec != fb_codec_none) { + int count = + fb_encoder_codec_get_sample_formats(bridge_codec, nullptr, 0); + if (count > 0) { + std::vector fmts(static_cast(count)); + fb_encoder_codec_get_sample_formats(bridge_codec, fmts.data(), + count); + for (int fmt : fmts) { + int native = -1; + oakcommon_ffmpegutils_get_native_sample_format(fmt, + &native); + if (native != SampleFormat::invalid) { + f.push_back( + SampleFormat(static_cast( + native))); + } + } + } + } + } + + return f; +} + +bool FFmpegEncoder::open() +{ + if (open_) { + return true; + } + + FBEncoderConfig config; + memset(&config, 0, sizeof(config)); + config.filename = params().filename().c_str(); + + // Storage keeping C strings alive until fb_encoder_create deep-copies them + std::string pix_fmt_str; + std::string subtitle_header; + std::vector opt_key_storage; + std::vector opt_value_storage; + std::vector opt_keys; + std::vector opt_values; + + // Set up video if it's enabled + if (params().video_enabled()) { + const OakVideoParams &vp = params().video_params(); + + config.video_enabled = 1; + config.video_codec = export_codec_to_bridge(params().video_codec()); + oakcommon_videoparams_get_width(vp, &config.video_width); + oakcommon_videoparams_get_height(vp, &config.video_height); + Rational pixel_aspect = video_params_pixel_aspect_ratio(vp); + config.video_pixel_aspect_num = pixel_aspect.numerator(); + config.video_pixel_aspect_den = pixel_aspect.denominator(); + Rational time_base = video_params_frame_rate_as_time_base(vp); + config.video_time_base_num = time_base.numerator(); + config.video_time_base_den = time_base.denominator(); + int frame_rate_num = 0, frame_rate_den = 1; + oakcommon_videoparams_get_frame_rate(vp, &frame_rate_num, + &frame_rate_den); + config.video_frame_rate_num = frame_rate_num; + config.video_frame_rate_den = frame_rate_den; + + pix_fmt_str = params().video_pix_fmt(); + config.video_pix_fmt = pix_fmt_str.c_str(); + + // This is the format we will expect frames received in Write() to be in + int native_pixel_fmt = -1; + oakcommon_videoparams_get_format(vp, &native_pixel_fmt); + + // This is the format we will need to convert the frame to for the bridge to understand it + int compatible_fmt = -1; + oakcommon_ffmpegutils_get_compatible_pixel_format(native_pixel_fmt, + &compatible_fmt); + video_conversion_fmt_ = + PixelFormat(static_cast(compatible_fmt)); + + // These are the equivalent pixel formats as bridge pixel formats + int src_alpha_pix_fmt = fb_pix_fmt_none; + int src_noalpha_pix_fmt = fb_pix_fmt_none; + oakcommon_ffmpegutils_get_ffmpeg_pixel_format( + compatible_fmt, OAKCOMMON_RGBA_CHANNEL_COUNT, &src_alpha_pix_fmt); + oakcommon_ffmpegutils_get_ffmpeg_pixel_format( + compatible_fmt, OAKCOMMON_RGB_CHANNEL_COUNT, &src_noalpha_pix_fmt); + + if (src_alpha_pix_fmt == fb_pix_fmt_none || + src_noalpha_pix_fmt == fb_pix_fmt_none) { + set_error("Failed to find suitable pixel format for this buffer"); + return false; + } + + config.video_src_pix_fmt = src_alpha_pix_fmt; + + int color_range = OAKCOMMON_COLOR_RANGE_LIMITED; + oakcommon_videoparams_get_color_range(vp, &color_range); + config.video_color_range = color_range == OAKCOMMON_COLOR_RANGE_FULL ? + fb_color_range_jpeg : + fb_color_range_mpeg; + + int interlacing = OAKCOMMON_VIDEO_INTERLACE_NONE; + oakcommon_videoparams_get_interlacing(vp, &interlacing); + switch (interlacing) { + case OAKCOMMON_VIDEO_INTERLACED_TOP_FIRST: + config.video_field_order = fb_field_order_tt; + break; + case OAKCOMMON_VIDEO_INTERLACED_BOTTOM_FIRST: + config.video_field_order = fb_field_order_bb; + break; + default: + config.video_field_order = fb_field_order_progressive; + break; + } + + config.video_bit_rate = params().video_bit_rate(); + config.video_min_bit_rate = params().video_min_bit_rate(); + config.video_max_bit_rate = params().video_max_bit_rate(); + config.video_buffer_size = params().video_buffer_size(); + config.video_threads = params().video_threads(); + + const std::string color_output = + colortransform_get_output(params().color_transform()); + config.video_color_srgb = + contains(to_lower(color_output), "srgb") ? 1 : 0; + + // Derive explicit nclc tags (HDR etc.) from the export colorspace; + // the bridge falls back to the legacy sRGB/Rec.709 logic when these + // are unspecified + int color_primaries = fb_color_primaries_unspec; + int color_trc = fb_color_trc_unspec; + int color_matrix = fb_col_spc_unspec; + get_color_tags_for_colorspace(color_output, &color_primaries, + &color_trc, &color_matrix); + config.video_color_primaries = color_primaries; + config.video_color_trc = color_trc; + config.video_colorspace = color_matrix; + + // Custom options (skip Olive-internal keys) + for (const auto &opt : params().video_opts()) { + if (opt.first.compare(0, 4, "ove_") != 0) { + opt_key_storage.push_back(opt.first); + opt_value_storage.push_back(opt.second); + } + } + for (size_t i = 0; i < opt_key_storage.size(); i++) { + opt_keys.push_back(opt_key_storage[i].c_str()); + opt_values.push_back(opt_value_storage[i].c_str()); + } + config.video_opt_keys = opt_keys.data(); + config.video_opt_values = opt_values.data(); + config.video_opt_count = int(opt_keys.size()); + } + + // Set up audio if it's enabled + if (params().audio_enabled()) { + config.audio_enabled = 1; + config.audio_codec = export_codec_to_bridge(params().audio_codec()); + config.audio_sample_rate = params().audio_params().sample_rate(); + config.audio_channel_layout_mask = + params().audio_params().channel_layout(); + int audio_sample_fmt = -1; + oakcommon_ffmpegutils_get_ffmpeg_sample_format( + static_cast(params().audio_params().format()), + &audio_sample_fmt); + config.audio_sample_format = audio_sample_fmt; + config.audio_bit_rate = params().audio_bit_rate(); + } + + // Set up subtitles if they're enabled + if (params().subtitles_enabled()) { + config.subtitles_enabled = 1; + config.subtitle_codec = export_codec_to_bridge(params().subtitles_codec()); + int header_size = + oakcommon_subtitleparams_generate_ass_header(nullptr, 0); + if (header_size > 0) { + subtitle_header.assign(size_t(header_size) - 1, '\0'); + oakcommon_subtitleparams_generate_ass_header( + subtitle_header.data(), header_size); + } + config.subtitle_header = + reinterpret_cast(subtitle_header.data()); + config.subtitle_header_size = int(subtitle_header.size()); + } + + encoder_ = fb_encoder_create(&config); + if (!encoder_) { + set_error("Failed to create encoder"); + return false; + } + + if (fb_encoder_open(encoder_) != 0) { + set_error_from_bridge(); + fb_encoder_free(&encoder_); + return false; + } + + open_ = true; + return true; +} + +bool FFmpegEncoder::write_frame(FramePtr frame, Rational time) +{ + // The render worker pool finishes tickets without a result when no + // worker is available (or the worker crashed); a null frame must fail + // the encode cleanly instead of crashing the export task. + if (!frame) { + fprintf(stderr, + "FFmpegEncoder::write_frame called with null frame\n"); + return false; + } + + // We may need to convert this frame to a frame that the bridge will understand + if (frame->format() != static_cast(video_conversion_fmt_)) { + frame = frame->convert(static_cast(video_conversion_fmt_)); + } + + int src_pix_fmt = fb_pix_fmt_none; + oakcommon_ffmpegutils_get_ffmpeg_pixel_format( + frame->format(), frame->channel_count(), &src_pix_fmt); + + int r = fb_encoder_write_video_frame( + encoder_, frame->width(), frame->height(), src_pix_fmt, + reinterpret_cast(frame->data()), + frame->linesize_bytes(), time.to_double()); + if (r != 0) { + set_error_from_bridge(); + return false; + } + + return true; +} + +bool FFmpegEncoder::write_audio(const SampleBuffer &audio) +{ + if (!audio.is_allocated()) { + return true; + } + + const AudioParams &audio_params = audio.audio_params().is_valid() ? + audio.audio_params() : + params().audio_params(); + + std::vector channel_data( + size_t(audio.audio_params().channel_count())); + for (size_t i = 0; i < channel_data.size(); i++) { + channel_data[i] = + reinterpret_cast(audio.data(int(i))); + } + + int sample_fmt = -1; + oakcommon_ffmpegutils_get_ffmpeg_sample_format( + static_cast(audio.audio_params().format()), &sample_fmt); + + int r = fb_encoder_write_audio( + encoder_, channel_data.data(), + audio.audio_params().channel_count(), sample_fmt, + audio_params.sample_rate(), int64_t(audio_params.channel_layout()), + int64_t(audio.sample_count())); + if (r != 0) { + set_error_from_bridge(); + return false; + } + + return true; +} + +bool FFmpegEncoder::write_audio_data(const AudioParams &audio_params, + const uint8_t **data, + int input_sample_count) +{ + int sample_fmt = -1; + oakcommon_ffmpegutils_get_ffmpeg_sample_format( + static_cast(audio_params.format()), &sample_fmt); + + int r = fb_encoder_write_audio( + encoder_, data, audio_params.channel_count(), sample_fmt, + audio_params.sample_rate(), int64_t(audio_params.channel_layout()), + input_sample_count); + if (r != 0) { + set_error_from_bridge(); + return false; + } + + return true; +} + +bool FFmpegEncoder::write_subtitle(const char *text, double in_seconds, + double out_seconds) +{ + int r = fb_encoder_write_subtitle(encoder_, text, in_seconds, out_seconds); + if (r != 0) { + set_error_from_bridge(); + return false; + } + + return true; +} + +void FFmpegEncoder::close() +{ + if (encoder_) { + // Flushes encoders, writes the trailer, and frees everything + fb_encoder_free(&encoder_); + } + + open_ = false; +} + +void FFmpegEncoder::set_error_from_bridge() +{ + const char *err = fb_encoder_get_error(encoder_); + set_error(err ? err : ""); +} + +int FFmpegEncoder::export_codec_to_bridge(ExportCodec::Codec c) +{ + switch (c) { + case ExportCodec::k_codec_h264: + return fb_codec_h264; + case ExportCodec::k_codec_h264rgb: + return fb_codec_h264_rgb; + case ExportCodec::k_codec_d_nx_hd: + return fb_codec_dnxhd; + case ExportCodec::k_codec_pro_res: + return fb_codec_prores; + case ExportCodec::k_codec_cineform: + return fb_codec_cineform; + case ExportCodec::k_codec_h265: + return fb_codec_h265; + case ExportCodec::k_codec_v_p9: + return fb_codec_v_p9; + case ExportCodec::k_codec_a_v1: + return fb_codec_a_v1; + case ExportCodec::k_codec_open_exr: + return fb_codec_openexr; + case ExportCodec::k_codec_png: + return fb_codec_png; + case ExportCodec::k_codec_tiff: + return fb_codec_tiff; + case ExportCodec::k_codec_m_p2: + return fb_codec_m_p2; + case ExportCodec::k_codec_m_p3: + return fb_codec_m_p3; + case ExportCodec::k_codec_aac: + return fb_codec_aac; + case ExportCodec::k_codec_pcm: + return fb_codec_pcm; + case ExportCodec::k_codec_flac: + return fb_codec_flac; + case ExportCodec::k_codec_opus: + return fb_codec_opus; + case ExportCodec::k_codec_vorbis: + return fb_codec_vorbis; + case ExportCodec::k_codec_srt: + return fb_codec_srt; + case ExportCodec::k_codec_count: + break; + } + + return fb_codec_none; +} + +} diff --git a/src/codec/src/ffmpeg/ffmpegencoder.h b/src/codec/src/ffmpeg/ffmpegencoder.h new file mode 100644 index 000000000..3aafbb8f2 --- /dev/null +++ b/src/codec/src/ffmpeg/ffmpegencoder.h @@ -0,0 +1,98 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_FFMPEGENCODER_H +#define OAK_FFMPEGENCODER_H + +#include + +#include "encoder.h" + +namespace olive +{ + +/** + * @brief An Encoder derivative that uses the ffmpeg_bridge library for encoding + * + * All encoding work happens inside the ffmpeg_bridge shared library through + * its pure C API; this class only translates EncodingParams into a bridge + * configuration and forwards calls. + */ +class FFmpegEncoder : public Encoder { +public: + FFmpegEncoder(const EncodingParams ¶ms); + + virtual std::vector + get_pixel_formats_for_codec(ExportCodec::Codec c) const override; + + virtual std::vector + get_sample_formats_for_codec(ExportCodec::Codec c) const override; + + virtual bool open() override; + + virtual bool write_frame(olive::FramePtr frame, + olive::core::Rational time) override; + + virtual bool write_audio(const olive::SampleBuffer &audio) override; + + bool write_audio_data(const AudioParams &audio_params, const uint8_t **data, + int input_sample_count); + + virtual bool write_subtitle(const char *text, double in_seconds, + double out_seconds) override; + + virtual void close() override; + + virtual PixelFormat get_desired_pixel_format() const override + { + return video_conversion_fmt_; + } + + /** + * @brief Derives nclc color tags from an output colorspace name + * + * Extracted for testability. Returns true when the name maps to + * explicit tags (PQ/HLG/BT.2020, sRGB, P3, Rec.601, Rec.709); returns + * false for unknown names, in which case the bridge's legacy + * Rec.709/sRGB inference applies. + */ + static bool get_color_tags_for_colorspace(const std::string &colorspace, + int *primaries, int *trc, + int *matrix); + +private: + /** + * @brief Copy the last error message from the bridge into the encoder error state + */ + void set_error_from_bridge(); + + static int export_codec_to_bridge(ExportCodec::Codec c); + + FBEncoder *encoder_; + + PixelFormat video_conversion_fmt_; + + bool open_; +}; + +} + +#endif // OAK_FFMPEGENCODER_H diff --git a/src/codec/src/footagedescription.h b/src/codec/src/footagedescription.h new file mode 100644 index 000000000..5a54098ca --- /dev/null +++ b/src/codec/src/footagedescription.h @@ -0,0 +1,291 @@ +/*** + + 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_CODEC_FOOTAGEDESCRIPTION_H +#define OAK_CODEC_FOOTAGEDESCRIPTION_H + +#include +#include +#include + +#include "common/subtitleparams.h" +#include "common/videoparams.h" +#include "olive/core/render/audioparams.h" +#include "olive/core/util/rational.h" + +namespace olive +{ + +using core::AudioParams; +using core::Rational; + +/** + * @brief Codec-internal replacement for the former + * node/project/footage/footagedescription.h + * + * Value type describing the streams a Decoder::probe() found in a file. + * oaknode has no C API counterpart, so codec keeps its own copy. Video and + * subtitle streams are stored as oakcommon by-value handles; the class + * addrefs on insert/copy and releases on destruction. The original's + * Track::Type mapping (get_type_of_stream) and XML load/save (probe cache) + * belonged to the oaknode-facing side and are intentionally not reproduced + * here; consumers can use stream_is_video/audio/subtitle. + */ +class FootageDescription { +public: + FootageDescription(const std::string &decoder = std::string()) + : decoder_(decoder) + , total_stream_count_(0) + , has_source_start_time_(false) + { + } + + FootageDescription(const FootageDescription &other) + : decoder_(other.decoder_) + , video_streams_(other.video_streams_) + , audio_streams_(other.audio_streams_) + , subtitle_streams_(other.subtitle_streams_) + , total_stream_count_(other.total_stream_count_) + , source_start_time_(other.source_start_time_) + , source_start_time_source_(other.source_start_time_source_) + , has_source_start_time_(other.has_source_start_time_) + { + for (const OakVideoParams &h : video_streams_) { + if (h.ctx) { + h.addref(h.ctx); + } + } + for (const OakSubtitleParams &h : subtitle_streams_) { + if (h.ctx) { + h.addref(h.ctx); + } + } + } + + FootageDescription &operator=(const FootageDescription &other) + { + if (this != &other) { + release_streams(); + decoder_ = other.decoder_; + video_streams_ = other.video_streams_; + audio_streams_ = other.audio_streams_; + subtitle_streams_ = other.subtitle_streams_; + total_stream_count_ = other.total_stream_count_; + source_start_time_ = other.source_start_time_; + source_start_time_source_ = other.source_start_time_source_; + has_source_start_time_ = other.has_source_start_time_; + for (const OakVideoParams &h : video_streams_) { + if (h.ctx) { + h.addref(h.ctx); + } + } + for (const OakSubtitleParams &h : subtitle_streams_) { + if (h.ctx) { + h.addref(h.ctx); + } + } + } + return *this; + } + + ~FootageDescription() + { + release_streams(); + } + + bool is_valid() const + { + return !decoder_.empty() && + (!video_streams_.empty() || !audio_streams_.empty() || + !subtitle_streams_.empty()); + } + + const std::string &decoder() const + { + return decoder_; + } + + void add_video_stream(const OakVideoParams &video_params) + { + assert(!has_stream_index(stream_index_of(video_params))); + + if (video_params.ctx) { + video_params.addref(video_params.ctx); + } + video_streams_.push_back(video_params); + } + + void add_audio_stream(const AudioParams &audio_params) + { + assert(!has_stream_index(audio_params.stream_index())); + + audio_streams_.push_back(audio_params); + } + + void add_subtitle_stream(const OakSubtitleParams &sub_params) + { + assert(!has_stream_index(stream_index_of(sub_params))); + + if (sub_params.ctx) { + sub_params.addref(sub_params.ctx); + } + subtitle_streams_.push_back(sub_params); + } + + bool stream_is_video(int index) const + { + for (const OakVideoParams &vp : video_streams_) { + if (stream_index_of(vp) == index) { + return true; + } + } + + return false; + } + + bool stream_is_audio(int index) const + { + for (const AudioParams &ap : audio_streams_) { + if (ap.stream_index() == index) { + return true; + } + } + + return false; + } + + bool stream_is_subtitle(int index) const + { + for (const OakSubtitleParams &sp : subtitle_streams_) { + if (stream_index_of(sp) == index) { + return true; + } + } + + return false; + } + + bool has_stream_index(int index) const + { + return stream_is_video(index) || stream_is_audio(index) || + stream_is_subtitle(index); + } + + int get_stream_count() const + { + return total_stream_count_; + } + void set_stream_count(int s) + { + total_stream_count_ = s; + } + + void set_source_start_time(const Rational &time, const std::string &source) + { + source_start_time_ = time; + source_start_time_source_ = source; + has_source_start_time_ = true; + } + + bool has_source_start_time() const + { + return has_source_start_time_; + } + + const Rational &source_start_time() const + { + return source_start_time_; + } + + const std::string &source_start_time_source() const + { + return source_start_time_source_; + } + + const std::vector &get_video_streams() const + { + return video_streams_; + } + + const std::vector &get_audio_streams() const + { + return audio_streams_; + } + std::vector &get_audio_streams() + { + return audio_streams_; + } + + const std::vector &get_subtitle_streams() const + { + return subtitle_streams_; + } + +private: + static int stream_index_of(const OakVideoParams ¶ms) + { + int index = -1; + oakcommon_videoparams_get_stream_index(params, &index); + return index; + } + + static int stream_index_of(const OakSubtitleParams ¶ms) + { + int index = -1; + oakcommon_subtitleparams_get_stream_index(params, &index); + return index; + } + + void release_streams() + { + for (OakVideoParams &h : video_streams_) { + if (h.ctx) { + h.release(h.ctx); + } + } + video_streams_.clear(); + for (OakSubtitleParams &h : subtitle_streams_) { + if (h.ctx) { + h.release(h.ctx); + } + } + subtitle_streams_.clear(); + } + + std::string decoder_; + + std::vector video_streams_; + + std::vector audio_streams_; + + std::vector subtitle_streams_; + + int total_stream_count_; + + Rational source_start_time_; + + std::string source_start_time_source_; + + bool has_source_start_time_; +}; + +} + +#endif // OAK_CODEC_FOOTAGEDESCRIPTION_H diff --git a/src/codec/src/frame.cpp b/src/codec/src/frame.cpp new file mode 100644 index 000000000..c6687474d --- /dev/null +++ b/src/codec/src/frame.cpp @@ -0,0 +1,237 @@ +/*** + + 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 "frame.h" + +#include +#include + +#include + +#include "common/oiioutils.h" +#include "framemanager.h" +#include "oiioframebridge.h" + +namespace olive +{ + +Frame::Frame() + : params_(oakcommon_videoparams_init()) + , data_(nullptr) + , data_size_(0) + , timestamp_(0) + , linesize_(0) + , linesize_pixels_(0) +{ +} + +Frame::~Frame() +{ + destroy(); + + if (params_.ctx && params_.release) { + params_.release(params_.ctx); + params_.ctx = nullptr; + } +} + +FramePtr Frame::create() +{ + return std::make_shared(); +} + +OakVideoParams Frame::video_params() const +{ + OakVideoParams copy = params_; + if (copy.ctx && copy.addref) { + copy.addref(copy.ctx); + } + return copy; +} + +void Frame::set_video_params(const OakVideoParams ¶ms) +{ + if (params_.ctx && params_.release) { + params_.release(params_.ctx); + } + + params_ = params; + + if (params_.ctx && params_.addref) { + params_.addref(params_.ctx); + } + + linesize_ = generate_linesize_bytes(width(), format(), channel_count()); + + int bpp = bytes_per_pixel(); + linesize_pixels_ = bpp > 0 ? linesize_ / bpp : 0; +} + +FramePtr Frame::interlace(FramePtr top, FramePtr bottom) +{ + OakVideoParams top_params = top->video_params(); + OakVideoParams bottom_params = bottom->video_params(); + + int equal = 0; + oakcommon_videoparams_equals(top_params, bottom_params, &equal); + + oakcommon_videoparams_free(&bottom_params); + + if (!equal) { + fprintf(stderr, + "Tried to interlace two frames that had incompatible parameters\n"); + oakcommon_videoparams_free(&top_params); + return nullptr; + } + + FramePtr interlaced = Frame::create(); + interlaced->set_video_params(top_params); + oakcommon_videoparams_free(&top_params); + interlaced->allocate(); + + int linesize = interlaced->linesize_bytes(); + + for (int i = 0; i < interlaced->height(); i++) { + FramePtr which = (i % 2 == 0) ? top : bottom; + + memcpy(interlaced->data() + i * linesize, + which->const_data() + i * linesize, linesize); + } + + return interlaced; +} + +int Frame::generate_linesize_bytes(int width, int format, int channel_count) +{ + // Align to 32 bytes (not sure if this is necessary?) + int bytes_per_pixel = oakcommon_videoparams_static_get_bytes_per_pixel( + static_cast(format), channel_count); + return bytes_per_pixel * ((width + 31) & ~31); +} + +core::Color Frame::get_pixel(int x, int y) const +{ + if (!contains_pixel(x, y)) { + return core::Color(); + } + + int byte_offset = y * linesize_bytes() + x * bytes_per_pixel(); + + return core::Color(reinterpret_cast(data_ + byte_offset), + core_format(), channel_count()); +} + +bool Frame::contains_pixel(int x, int y) const +{ + return (is_allocated() && x >= 0 && x < width() && y >= 0 && y < height()); +} + +void Frame::set_pixel(int x, int y, const core::Color &c) +{ + if (!contains_pixel(x, y)) { + return; + } + + int byte_offset = y * linesize_bytes() + x * bytes_per_pixel(); + + c.to_data(reinterpret_cast(data_ + byte_offset), core_format(), + channel_count()); +} + +bool Frame::allocate() +{ + // Assume this frame is intended to be a video frame + int is_valid = 0; + oakcommon_videoparams_get_is_valid(params_, &is_valid); + if (!is_valid) { + std::cerr << "Tried to allocate a frame with invalid parameters"; + return false; + } + + if (is_allocated()) { + // Already allocated + return true; + } + + data_size_ = linesize_ * height(); + data_ = FrameManager::allocate(data_size_); + + return true; +} + +void Frame::destroy() +{ + if (is_allocated()) { + FrameManager::deallocate(data_size_, data_); + + data_size_ = 0; + data_ = nullptr; + } +} + +/** + * @brief OIIO base type for a native pixel format, via the oakcommon C ABI + * + * The OakOIIOUtils object is stateless; a process-lifetime handle is kept + * here to avoid re-boxing it on every conversion. + */ +static OIIO::TypeDesc::BASETYPE oiio_base_type_for_format(int format) +{ + static OakOIIOUtils utils = oakcommon_oiioutils_init(); + + int base_type = 0; // OIIO::TypeDesc::UNKNOWN + oakcommon_oiioutils_get_oiio_base_type_from_format(utils, format, + &base_type); + return static_cast(base_type); +} + +FramePtr Frame::convert(int format) const +{ + // Create new params with destination format + OakVideoParams params = video_params(); + oakcommon_videoparams_set_format(params, format); + + // Create new frame + FramePtr converted = Frame::create(); + converted->set_video_params(params); + oakcommon_videoparams_free(¶ms); + converted->set_timestamp(timestamp_); + converted->allocate(); + + // Do the conversion through OIIO for convenience + OIIO::ImageBuf src(OIIO::ImageSpec(width(), height(), channel_count(), + oiio_base_type_for_format(this->format()))); + + oiio_frame_to_buffer(const_data(), linesize_bytes(), &src); + + OIIO::ImageBuf dst(OIIO::ImageSpec(converted->width(), converted->height(), + channel_count(), + oiio_base_type_for_format(format))); + + if (dst.copy_pixels(src)) { + oiio_buffer_to_frame(&dst, converted->data(), + converted->linesize_bytes()); + return converted; + } else { + return nullptr; + } +} + +} diff --git a/src/codec/src/frame.h b/src/codec/src/frame.h new file mode 100644 index 000000000..24c0f9f57 --- /dev/null +++ b/src/codec/src/frame.h @@ -0,0 +1,210 @@ +/*** + + 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_FRAME_H +#define OAK_FRAME_H + +#include + +#include +#include +#include + +#include "common/videoparams.h" + +namespace olive +{ + +class Frame; +using FramePtr = std::shared_ptr; + +/** + * @brief Video frame data or audio sample data from a Decoder + * + * Qt-free (M5). The parameter set is held as an OakVideoParams handle + * (oakcommon C ABI); every access goes through the oakcommon_videoparams_* + * functions. Pixel formats cross the boundary as OakPixelFormat int codes + * (numerically identical to olive::core::PixelFormat::Format). + */ +class Frame { +public: + Frame(); + + ~Frame(); + + Frame(const Frame &) = delete; + + static FramePtr create(); + + /** + * @brief Return a copy of the frame's parameter handle + * + * The returned handle has had its reference count incremented; the + * caller must release it (oakcommon_videoparams_free()). + */ + OakVideoParams video_params() const; + void set_video_params(const OakVideoParams ¶ms); + + static FramePtr interlace(FramePtr top, FramePtr bottom); + + static int generate_linesize_bytes(int width, int format, + int channel_count); + + int linesize_pixels() const + { + return linesize_pixels_; + } + + int linesize_bytes() const + { + return linesize_; + } + + int width() const + { + int width = 0; + oakcommon_videoparams_get_effective_width(params_, &width); + return width; + } + + int height() const + { + int height = 0; + oakcommon_videoparams_get_effective_height(params_, &height); + return height; + } + + /** + * @brief Pixel format as an OakPixelFormat value + * (== olive::core::PixelFormat::Format ordinal). + */ + int format() const + { + int format = OAKCOMMON_PIXEL_FORMAT_INVALID; + oakcommon_videoparams_get_format(params_, &format); + return format; + } + + int channel_count() const + { + int count = 0; + oakcommon_videoparams_get_channel_count(params_, &count); + return count; + } + + core::Color get_pixel(int x, int y) const; + bool contains_pixel(int x, int y) const; + void set_pixel(int x, int y, const core::Color &c); + + /** + * @brief Get frame's timestamp. + * + * This timestamp is always a Rational that will equate to the time in seconds. + */ + const core::Rational ×tamp() const + { + return timestamp_; + } + + void set_timestamp(const core::Rational ×tamp) + { + timestamp_ = timestamp; + } + + /** + * @brief Get the data buffer of this frame + */ + char *data() + { + return data_; + } + + /** + * @brief Get the const data buffer of this frame + */ + const char *const_data() const + { + return data_; + } + + /** + * @brief Allocate memory buffer to store data based on parameters + * + * For video frames, the width(), height(), and format() must be set for this function to work. + * + * If a memory buffer has been previously allocated without destroying, this function will destroy it. + */ + bool allocate(); + + /** + * @brief Return whether the frame is allocated or not + */ + bool is_allocated() const + { + return data_; + } + + /** + * @brief Destroy a memory buffer allocated with allocate() + */ + void destroy(); + + /** + * @brief Returns the size of the array returned in data() in bytes + * + * Returns 0 if nothing is allocated. + */ + int allocated_size() const + { + return data_size_; + } + + FramePtr convert(int format) const; + +private: + int bytes_per_pixel() const + { + int bytes = 0; + oakcommon_videoparams_get_bytes_per_pixel(params_, &bytes); + return bytes; + } + + core::PixelFormat core_format() const + { + return core::PixelFormat( + static_cast(format())); + } + + OakVideoParams params_; + + char *data_; + int data_size_; + + core::Rational timestamp_; + + int linesize_; + + int linesize_pixels_; +}; + +} + + +#endif // OAK_FRAME_H diff --git a/src/render/src/framemanager.cpp b/src/codec/src/framemanager.cpp similarity index 100% rename from src/render/src/framemanager.cpp rename to src/codec/src/framemanager.cpp diff --git a/src/render/src/framemanager.h b/src/codec/src/framemanager.h similarity index 100% rename from src/render/src/framemanager.h rename to src/codec/src/framemanager.h diff --git a/src/codec/src/oiio/CMakeLists.txt b/src/codec/src/oiio/CMakeLists.txt new file mode 100644 index 000000000..0c06c7dff --- /dev/null +++ b/src/codec/src/oiio/CMakeLists.txt @@ -0,0 +1,22 @@ +# Olive - Non-Linear Video Editor +# Copyright (C) 2022 Olive 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 . + +target_sources(oakcodec PRIVATE + oiiodecoder.cpp + oiiodecoder.h + oiioencoder.cpp + oiioencoder.h +) diff --git a/src/codec/src/oiio/oiiodecoder.cpp b/src/codec/src/oiio/oiiodecoder.cpp new file mode 100644 index 000000000..c1cec6427 --- /dev/null +++ b/src/codec/src/oiio/oiiodecoder.cpp @@ -0,0 +1,400 @@ +/*** + + 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 "oiiodecoder.h" + +#include +#include +#include +#include +#include + +#include "common/oiioutils.h" + +namespace olive +{ + +std::vector OIIODecoder::supported_formats; + +namespace +{ + +// Thin wrappers over the oakcommon_oiioutils_* C API (replacing the former +// OIIOUtils C++ class). The handle is stateless, so it is created and +// released per call. + +int pix_format_from_oiio_basetype(int base_type) +{ + int out = OAKCOMMON_PIXEL_FORMAT_INVALID; + OakOIIOUtils utils = oakcommon_oiioutils_init(); + oakcommon_oiioutils_get_format_from_oiio_basetype(utils, base_type, &out); + oakcommon_oiioutils_free(&utils); + return out; +} + +int oiio_base_type_from_pix_format(int pixel_format) +{ + int out = 0; // OIIO::TypeDesc::UNKNOWN + OakOIIOUtils utils = oakcommon_oiioutils_init(); + oakcommon_oiioutils_get_oiio_base_type_from_format(utils, pixel_format, + &out); + oakcommon_oiioutils_free(&utils); + return out; +} + +/** + * @brief Flatten an OakVideoParams handle into the oakrender POD + * (needed at every texture creation point) + */ +void fill_render_params(const OakVideoParams ¶ms, + oakrender_video_params *out) +{ + *out = oakrender_video_params{}; + oakcommon_videoparams_get_width(params, &out->width); + oakcommon_videoparams_get_height(params, &out->height); + oakcommon_videoparams_get_time_base(params, &out->time_base_num, + &out->time_base_den); + oakcommon_videoparams_get_format(params, &out->format); + oakcommon_videoparams_get_pixel_aspect_ratio( + params, &out->pixel_aspect_num, &out->pixel_aspect_den); + oakcommon_videoparams_get_interlacing(params, &out->interlacing); + oakcommon_videoparams_get_color_range(params, &out->color_range); + oakcommon_videoparams_get_divider(params, &out->divider); + oakcommon_videoparams_get_video_type(params, &out->video_type); + oakcommon_videoparams_get_premultiplied_alpha(params, + &out->premultiplied_alpha); +} + +std::vector split_string(const std::string &s, char delimiter) +{ + std::vector out; + std::string::size_type start = 0; + while (true) { + std::string::size_type pos = s.find(delimiter, start); + if (pos == std::string::npos) { + out.push_back(s.substr(start)); + break; + } + out.push_back(s.substr(start, pos - start)); + start = pos + 1; + } + return out; +} + +std::string to_lower(const std::string &s) +{ + std::string out = s; + std::transform(out.begin(), out.end(), out.begin(), + [](unsigned char c) { return char(std::tolower(c)); }); + return out; +} + +} // namespace + +OIIODecoder::OIIODecoder() + : image_(nullptr) +{ +} + +std::string OIIODecoder::id() const +{ + return "oiio"; +} + +FootageDescription OIIODecoder::probe(const std::string &filename, + OakCancelAtom *cancelled) const +{ + (void) cancelled; + + FootageDescription desc(id()); + + // Filter out any file extensions that aren't expected to work - sometimes OIIO will crash trying + // to open a file that it can't if it's given one + if (!file_type_is_supported(filename)) { + return desc; + } + + auto in = OIIO::ImageInput::open(filename); + + if (!in) { + return desc; + } + + // Filter out OIIO detecting an "FFmpeg movie", we have a native FFmpeg decoder that can handle + // it better + if (!strcmp(in->format_name(), "FFmpeg movie")) { + return desc; + } + + bool stream_enabled = true; + + int i; + for (i = 0; in->seek_subimage(i, 0); i++) { + OIIO::ImageSpec spec = in->spec(); + + OakVideoParams video_params = get_video_params_from_image_spec(spec); + + oakcommon_videoparams_set_stream_index(video_params, i); + + if (i > 1) { + // This is a multilayer image and this image might have an offset + OIIO::ImageSpec root_spec = in->spec(0); + + float norm_x = spec.x + float(spec.width) * 0.5f - + float(root_spec.width) * 0.5f; + float norm_y = spec.y + float(spec.height) * 0.5f - + float(root_spec.height) * 0.5f; + + oakcommon_videoparams_set_x(video_params, norm_x); + oakcommon_videoparams_set_y(video_params, norm_y); + } + + // By default, only enable the first subimage (presumably the combined image). Later we will + // ask the user if they want to enable the layers instead. + oakcommon_videoparams_set_enabled(video_params, stream_enabled ? 1 : 0); + stream_enabled = false; + + // OIIO automatically premultiplies alpha + // FIXME: We usually disassociate the alpha for the color management later, for 8-bit images this + // likely reduces the fidelity? + oakcommon_videoparams_set_premultiplied_alpha(video_params, 1); + + desc.add_video_stream(video_params); + oakcommon_videoparams_free(&video_params); + } + + desc.set_stream_count(i); + + // If we're here, we have a successful image open + in->close(); + + return desc; +} + +bool OIIODecoder::open_internal() +{ + // If we can open the filename provided, assume everything is working + return open_image_handler(stream().filename(), stream().stream()); +} + +OakRenderTexture * +OIIODecoder::retrieve_video_internal(const RetrieveVideoParams &p) +{ + FramePtr frame = retrieve_video_frame_internal(p); + if (!frame) { + return nullptr; + } + + OakVideoParams frame_params = frame->video_params(); // addref'd copy + oakrender_video_params rvp; + fill_render_params(frame_params, &rvp); + oakcommon_videoparams_free(&frame_params); + + // Frame linesize is already in bytes, which is what the C API expects + return oakrender_display_texture_create(p.renderer, &rvp, frame->data(), + frame->linesize_bytes()); +} + +FramePtr OIIODecoder::retrieve_video_frame_internal(const RetrieveVideoParams &p) +{ + OakVideoParams vp = get_video_params_from_image_spec(image_->spec()); + oakcommon_videoparams_set_divider(vp, p.divider); + + if (!buffer_.is_allocated() || last_params_.divider != p.divider) { + last_params_ = p; + + buffer_.destroy(); + buffer_.set_video_params(vp); // Frame addrefs the handle + buffer_.allocate(); + + if (p.divider == 1) { + // Just upload straight to the buffer + image_->read_image(0, 0, 0, -1, oiio_pix_fmt_, buffer_.data()); + } else { + OIIO::ImageBuf buf(image_->spec()); + image_->read_image(0, 0, 0, -1, image_->spec().format, + buf.localpixels(), buf.pixel_stride(), + buf.scanline_stride(), buf.z_stride()); + + // Roughly downsample image for divider (for some reason OIIO::ImageBufAlgo::resample failed here) + int px_sz = 0; + oakcommon_videoparams_get_bytes_per_pixel(vp, &px_sz); + for (int dst_y = 0; dst_y < buffer_.height(); dst_y++) { + int src_y = dst_y * buf.spec().height / buffer_.height(); + + for (int dst_x = 0; dst_x < buffer_.width(); dst_x++) { + int src_x = dst_x * buf.spec().width / buffer_.width(); + memcpy(buffer_.data() + buffer_.linesize_bytes() * dst_y + + px_sz * dst_x, + static_cast(buf.localpixels()) + + buf.scanline_stride() * src_y + px_sz * src_x, + px_sz); + } + } + } + } + + int format = OAKCOMMON_PIXEL_FORMAT_INVALID; + oakcommon_videoparams_get_format(vp, &format); + oakcommon_videoparams_free(&vp); + + // Force F32 output for all still images + if (format != PixelFormat::f32) { + FramePtr f32_frame = buffer_.convert(PixelFormat::f32); + if (f32_frame) { + f32_frame->set_timestamp(p.time); + return f32_frame; + } + } + + FramePtr frame = Frame::create(); + OakVideoParams buffer_params = buffer_.video_params(); // addref'd copy + frame->set_video_params(buffer_params); + oakcommon_videoparams_free(&buffer_params); + frame->set_timestamp(p.time); + if (!frame->allocate()) { + return nullptr; + } + memcpy(frame->data(), buffer_.const_data(), + size_t(buffer_.allocated_size())); + return frame; +} + +void OIIODecoder::close_internal() +{ + close_image_handle(); +} + +bool OIIODecoder::file_type_is_supported(const std::string &fn) +{ + // We prioritize OIIO over FFmpeg to pick up still images more effectively, but some OIIO decoders (notably OpenJPEG) + // will segfault entirely if given unexpected data (an MPEG-4 for instance). To workaround this issue, we use OIIO's + // "extension_list" attribute and match it with the extension of the file. + + // Check if we've created the supported formats list, create it if not + if (supported_formats.empty()) { + std::vector extension_list = + split_string(OIIO::get_string_attribute("extension_list"), ';'); + + // The format of "extension_list" is "format:ext", we want to separate it into a simple list of extensions + for (const std::string &ext : extension_list) { + std::vector format_and_ext = split_string(ext, ':'); + + if (format_and_ext.size() >= 2) { + std::vector exts = + split_string(format_and_ext.at(1), ','); + supported_formats.insert(supported_formats.end(), + exts.begin(), exts.end()); + } + } + } + + // QFileInfo::suffix(): extension after the last '.', case-insensitive match + std::string suffix = std::filesystem::path(fn).extension().string(); + if (!suffix.empty() && suffix.front() == '.') { + suffix.erase(0, 1); + } + suffix = to_lower(suffix); + + for (const std::string &supported : supported_formats) { + if (to_lower(supported) == suffix) { + return true; + } + } + + return false; +} + +bool OIIODecoder::open_image_handler(const std::string &fn, int subimage) +{ + image_ = OIIO::ImageInput::open(fn); + + if (!image_) { + return false; + } + + if (!image_->seek_subimage(subimage, 0)) { + return false; + } + + // Check if we can work with this pixel format + const OIIO::ImageSpec &spec = image_->spec(); + + // We use RGBA frames because that tends to be the native format of GPUs + pix_fmt_ = static_cast( + pix_format_from_oiio_basetype(spec.format.basetype)); + + if (pix_fmt_ == PixelFormat::invalid) { + fprintf(stderr, "Failed to convert OIIO::ImageDesc to native pixel format\n"); + return false; + } + + oiio_pix_fmt_ = + static_cast( + oiio_base_type_from_pix_format(pix_fmt_)); + + if (oiio_pix_fmt_ == OIIO::TypeDesc::UNKNOWN) { + fprintf(stderr, "Failed to determine appropriate OIIO basetype from native format\n"); + return false; + } + + return true; +} + +void OIIODecoder::close_image_handle() +{ + if (image_) { + image_->close(); + image_ = nullptr; + } + + buffer_.destroy(); +} + +OakVideoParams +OIIODecoder::get_video_params_from_image_spec(const OIIO::ImageSpec &spec) +{ + OakVideoParams video_params = oakcommon_videoparams_init(); + + oakcommon_videoparams_set_width(video_params, spec.width); + oakcommon_videoparams_set_height(video_params, spec.height); + oakcommon_videoparams_set_format( + video_params, pix_format_from_oiio_basetype(spec.format.basetype)); + oakcommon_videoparams_set_channel_count(video_params, spec.nchannels); + + int par_num = 1, par_den = 1; + { + OakOIIOUtils utils = oakcommon_oiioutils_init(); + oakcommon_oiioutils_get_pixel_aspect_ratio( + utils, spec.get_float_attribute("PixelAspectRatio", 1.0f), + &par_num, &par_den); + oakcommon_oiioutils_free(&utils); + } + oakcommon_videoparams_set_pixel_aspect_ratio(video_params, par_num, + par_den); + + oakcommon_videoparams_set_video_type(video_params, + OAKCOMMON_VIDEO_TYPE_STILL); + + return video_params; +} + +} diff --git a/src/codec/src/oiio/oiiodecoder.h b/src/codec/src/oiio/oiiodecoder.h new file mode 100644 index 000000000..b7da2ea28 --- /dev/null +++ b/src/codec/src/oiio/oiiodecoder.h @@ -0,0 +1,84 @@ +/*** + + 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_OIIODECODER_H +#define OAK_OIIODECODER_H + +#include +#include +#include + +#include +#include + +#include "decoder.h" + +namespace olive +{ + +class OIIODecoder : public Decoder { +public: + OIIODecoder(); + + DECODER_DEFAULT_DESTRUCTOR(OIIODecoder) + + virtual std::string id() const override; + + virtual bool supports_video() override + { + return true; + } + + virtual FootageDescription probe(const std::string &filename, + OakCancelAtom *cancelled) const override; + +protected: + virtual bool open_internal() override; + virtual OakRenderTexture * + retrieve_video_internal(const RetrieveVideoParams &p) override; + virtual FramePtr + retrieve_video_frame_internal(const RetrieveVideoParams &p) override; + virtual void close_internal() override; + +private: + std::unique_ptr image_; + + static bool file_type_is_supported(const std::string &fn); + + bool open_image_handler(const std::string &fn, int subimage); + + void close_image_handle(); + + static OakVideoParams + get_video_params_from_image_spec(const OIIO::ImageSpec &spec); + + PixelFormat pix_fmt_; + OIIO::TypeDesc::BASETYPE oiio_pix_fmt_; + + Frame buffer_; + RetrieveVideoParams last_params_; + + static std::vector supported_formats; +}; + +} + +#endif // OAK_OIIODECODER_H diff --git a/src/codec/src/oiio/oiioencoder.cpp b/src/codec/src/oiio/oiioencoder.cpp new file mode 100644 index 000000000..250fc6314 --- /dev/null +++ b/src/codec/src/oiio/oiioencoder.cpp @@ -0,0 +1,92 @@ +/*** + + 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 "oiioencoder.h" + +#include + +#include "common/oiioutils.h" + +OIIO_NAMESPACE_USING + +namespace olive +{ + +OIIOEncoder::OIIOEncoder(const EncodingParams ¶ms) : Encoder(params) {} + +bool OIIOEncoder::open() +{ + return true; +} + +bool OIIOEncoder::write_frame(FramePtr frame, Rational time) +{ + std::string filename = get_filename_for_frame(time); + + auto output = OIIO::ImageOutput::create(filename); + if (!output) { + return false; + } + + int base_type = 0; // OIIO::TypeDesc::UNKNOWN + OakOIIOUtils oiio_utils = oakcommon_oiioutils_init(); + oakcommon_oiioutils_get_oiio_base_type_from_format( + oiio_utils, frame->format(), &base_type); + oakcommon_oiioutils_free(&oiio_utils); + + OIIO::TypeDesc type(static_cast(base_type)); + OIIO::ImageSpec spec(frame->width(), frame->height(), + frame->channel_count(), type); + + if (!output->open(filename, spec)) { + return false; + } + + if (!output->write_image(type, frame->data(), OIIO::AutoStride, + frame->linesize_bytes())) { + return false; + } + + if (!output->close()) { + return false; + } + + return true; +} + +bool OIIOEncoder::write_audio(const SampleBuffer &audio) +{ + // Do nothing + return false; +} + +bool OIIOEncoder::write_subtitle(const char *text, double in_seconds, + double out_seconds) +{ + return false; +} + +void OIIOEncoder::close() +{ + // Do nothing +} + +} diff --git a/src/codec/src/oiio/oiioencoder.h b/src/codec/src/oiio/oiioencoder.h new file mode 100644 index 000000000..e49521d34 --- /dev/null +++ b/src/codec/src/oiio/oiioencoder.h @@ -0,0 +1,47 @@ +/*** + + Olive - Non-Linear Video Editor + Copyright (C) 2022 Olive Team + Modifications Copyright (C) 2025 mikesolar + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#ifndef OAK_OIIOENCODER_H +#define OAK_OIIOENCODER_H + +#include "encoder.h" + +namespace olive +{ + +class OIIOEncoder : public Encoder { +public: + OIIOEncoder(const EncodingParams ¶ms); + + virtual bool open() override; + + virtual bool write_frame(olive::FramePtr frame, + olive::core::Rational time) override; + virtual bool write_audio(const SampleBuffer &audio) override; + virtual bool write_subtitle(const char *text, double in_seconds, + double out_seconds) override; + + virtual void close() override; +}; + +} + +#endif // OAK_OIIOENCODER_H diff --git a/src/codec/src/oiioframebridge.cpp b/src/codec/src/oiioframebridge.cpp new file mode 100644 index 000000000..c8f21bad3 --- /dev/null +++ b/src/codec/src/oiioframebridge.cpp @@ -0,0 +1,40 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include "oiioframebridge.h" + +namespace olive +{ + +void oiio_frame_to_buffer(const void *data, int64_t linesize_bytes, + OIIO::ImageBuf *buf) +{ + buf->set_pixels(OIIO::ROI(), buf->spec().format, data, OIIO::AutoStride, + static_cast(linesize_bytes)); +} + +void oiio_buffer_to_frame(OIIO::ImageBuf *buf, void *data, + int64_t linesize_bytes) +{ + buf->get_pixels(OIIO::ROI(), buf->spec().format, data, OIIO::AutoStride, + static_cast(linesize_bytes)); +} + +} diff --git a/src/codec/src/oiioframebridge.h b/src/codec/src/oiioframebridge.h new file mode 100644 index 000000000..160eaaf09 --- /dev/null +++ b/src/codec/src/oiioframebridge.h @@ -0,0 +1,52 @@ +/*** + + 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_OIIOFRAMEBRIDGE_H +#define OAK_OIIOFRAMEBRIDGE_H + +#include + +#include + +namespace olive +{ + +/** + * @brief Copies raw pixel data into an OIIO image buffer + * + * Moved from oakcommon's OIIOUtils (M5): these two helpers are only used + * by codec (Frame::convert() and the OIIO decoder/encoder), so they live + * here as internal C++ functions and no longer cross the oakcommon + * boundary. `format`/`nb_channels` describe the raw buffer and are only + * needed by callers to have set up `buf`'s spec correctly beforehand; + * the copy itself goes by the buffer's spec. + */ +void oiio_frame_to_buffer(const void *data, int64_t linesize_bytes, + OIIO::ImageBuf *buf); + +/** + * @brief Copies an OIIO image buffer's pixels into raw memory + */ +void oiio_buffer_to_frame(OIIO::ImageBuf *buf, void *data, + int64_t linesize_bytes); + +} + +#endif // OAK_OIIOFRAMEBRIDGE_H diff --git a/src/codec/src/planarfiledevice.cpp b/src/codec/src/planarfiledevice.cpp new file mode 100644 index 000000000..e2512f65a --- /dev/null +++ b/src/codec/src/planarfiledevice.cpp @@ -0,0 +1,125 @@ +/*** + + 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 "planarfiledevice.h" + +#include + +namespace olive +{ + +PlanarFileDevice::PlanarFileDevice() = default; + +PlanarFileDevice::~PlanarFileDevice() +{ + close(); +} + +bool PlanarFileDevice::open(const std::vector &filenames, + OpenMode mode) +{ + if (isOpen()) { + // Already open + return false; + } + + const char *mode_str = (mode == k_read_only) ? "rb" : "wb"; + + files_.resize(filenames.size(), nullptr); + + for (size_t i = 0; i < files_.size(); i++) { + files_[i] = std::fopen(filenames.at(i).c_str(), mode_str); + if (!files_[i]) { + close(); + return false; + } + } + + return true; +} + +int64_t PlanarFileDevice::read(char **data, int64_t bytes_per_channel, + int64_t offset) +{ + int64_t ret = -1; + + if (isOpen()) { + for (size_t i = 0; i < files_.size(); i++) { + // Kind of clunky but should be largely fine + ret = int64_t(std::fread(data[i] + offset, 1, + size_t(bytes_per_channel), files_[i])); + } + } + + return ret; +} + +int64_t PlanarFileDevice::write(const char **data, int64_t bytes_per_channel, + int64_t offset) +{ + int64_t ret = -1; + + if (isOpen()) { + for (size_t i = 0; i < files_.size(); i++) { + // Kind of clunky but should be largely fine + ret = int64_t(std::fwrite(data[i] + offset, 1, + size_t(bytes_per_channel), files_[i])); + } + } + + return ret; +} + +int64_t PlanarFileDevice::size() const +{ + if (isOpen()) { + struct stat st; + if (fstat(fileno(files_.front()), &st) == 0) { + return int64_t(st.st_size); + } + } + + return 0; +} + +bool PlanarFileDevice::seek(int64_t pos) +{ + bool ret = true; + + for (size_t i = 0; i < files_.size(); i++) { + ret = (std::fseek(files_[i], pos, SEEK_SET) == 0) && ret; + } + + return ret; +} + +void PlanarFileDevice::close() +{ + for (size_t i = 0; i < files_.size(); i++) { + std::FILE *f = files_.at(i); + if (f) { + std::fclose(f); + } + } + files_.clear(); +} + +} diff --git a/src/codec/src/planarfiledevice.h b/src/codec/src/planarfiledevice.h new file mode 100644 index 000000000..199b436ba --- /dev/null +++ b/src/codec/src/planarfiledevice.h @@ -0,0 +1,77 @@ +/*** + + 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_PLANARFILEDEVICE_H +#define OAK_PLANARFILEDEVICE_H + +#include +#include +#include +#include + +namespace olive +{ + +/** + * @brief Reads/writes interleaved planar channel files + * + * De-Qt replacement for the QFile-based version; now a thin wrapper over + * std::FILE. Not copyable; closes all files on destruction. + */ +class PlanarFileDevice { +public: + /** + * @brief Open mode (replaces QIODevice::OpenMode) + */ + enum OpenMode { k_read_only, k_write_only }; + + PlanarFileDevice(); + + ~PlanarFileDevice(); + + PlanarFileDevice(const PlanarFileDevice &) = delete; + PlanarFileDevice &operator=(const PlanarFileDevice &) = delete; + + bool isOpen() const + { + return !files_.empty(); + } + + bool open(const std::vector &filenames, OpenMode mode); + + int64_t read(char **data, int64_t bytes_per_channel, int64_t offset = 0); + + int64_t write(const char **data, int64_t bytes_per_channel, + int64_t offset = 0); + + int64_t size() const; + + bool seek(int64_t pos); + + void close(); + +private: + std::vector files_; +}; + +} + +#endif // OAK_PLANARFILEDEVICE_H diff --git a/src/codec/src/proxymanager.cpp b/src/codec/src/proxymanager.cpp new file mode 100644 index 000000000..2d9384ab7 --- /dev/null +++ b/src/codec/src/proxymanager.cpp @@ -0,0 +1,301 @@ +/* + * 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 "proxymanager.h" + +#include +#include +#include + +#include + +#include "common/filefunctions.h" + +#include "taskcallbacks.h" + +namespace olive +{ + +ProxyManager *ProxyManager::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; +} + +/** + * @brief oakcommon C API wrapper for FileFunctions::get_application_path + */ +std::string application_path() +{ + OakFileFunctions ff = oakcommon_filefunctions_init(); + if (!ff.ctx) { + return std::string(); + } + + std::string result; + int size = oakcommon_filefunctions_get_application_path(ff, nullptr, 0); + if (size > 0) { + result.resize(size_t(size) - 1); + oakcommon_filefunctions_get_application_path(ff, result.data(), size); + } + + oakcommon_filefunctions_free(&ff); + return result; +} + +bool is_executable_file(const std::filesystem::path &p) +{ + std::error_code ec; + return std::filesystem::is_regular_file(p, ec) && + ::access(p.c_str(), X_OK) == 0; +} + +} // namespace + +std::string ProxyManager::get_proxy_directory(const std::string &cache_path) +{ + return (std::filesystem::path(cache_path) / "proxy").string(); +} + +std::string ProxyManager::get_proxy_filename(const std::string &cache_path, + const std::string &source_filename, + int stream_index, + const ProxyParams ¶ms) +{ + const std::string proxy_dir = get_proxy_directory(cache_path); + const std::string extension = + params.extension.empty() ? "mp4" : params.extension; + + // Divider mode scales relative to the source, so the tag names the + // divider rather than an absolute target size + std::string size_tag; + if (params.divider > 1) { + size_tag = "div" + std::to_string(params.divider); + } else { + size_tag = std::to_string(params.width) + "x" + + std::to_string(params.height); + } + + const std::string filename = unique_file_identifier(source_filename) + "-" + + std::to_string(stream_index) + "." + size_tag + + ".v" + std::to_string(params.version) + ".a" + + (params.include_audio ? "1" : "0") + "." + + extension; + + return (std::filesystem::path(proxy_dir) / filename).string(); +} + +std::string ProxyManager::get_working_proxy_filename(const std::string &proxy_filename) +{ + // Append a recognizable suffix while keeping a standard container extension + // so ffmpeg can infer the output format. + return proxy_filename + ".working.mp4"; +} + +ProxyManager::ProxyState +ProxyManager::get_proxy_state(const std::string &proxy_filename) +{ + std::error_code ec; + if (std::filesystem::exists(proxy_filename, ec)) { + return k_proxy_ready; + } + + if (std::filesystem::exists(get_working_proxy_filename(proxy_filename), ec)) { + return k_proxy_generating; + } + + return k_proxy_missing; +} + +std::string ProxyManager::proxy_state_to_string(ProxyState state) +{ + switch (state) { + case k_proxy_missing: + return "missing"; + case k_proxy_generating: + return "generating"; + case k_proxy_ready: + return "ready"; + case k_proxy_failed: + return "failed"; + } + + return "missing"; +} + +ProxyManager::ProxyState +ProxyManager::proxy_state_from_string(const std::string &state) +{ + if (state == "generating") { + return k_proxy_generating; + } + + if (state == "ready") { + return k_proxy_ready; + } + + if (state == "failed") { + return k_proxy_failed; + } + + return k_proxy_missing; +} + +bool ProxyManager::proxy_filename_has_audio(const std::string &proxy_filename) +{ + return std::filesystem::path(proxy_filename) + .filename() + .string() + .find(".a1.") != std::string::npos; +} + +ProxyManager::ProxyParams ProxyManager::proxy_params_from_config() +{ + // Interim state: the Qt config store (OAK_CONFIG ProxyWidth/ProxyHeight/ + // ProxyDivider/ProxyCRF/ProxyPreset/ProxyIncludeAudio) is not split yet, + // so the compiled-in defaults apply. + return ProxyParams(); +} + +std::string ProxyManager::find_f_fmpeg_executable(const std::string &configured_path) +{ + // An explicitly configured path takes precedence if it is usable + if (!configured_path.empty()) { + if (is_executable_file(configured_path)) { + return std::filesystem::absolute(configured_path).string(); + } + + fprintf(stderr, "Configured ffmpeg path is not a valid executable: %s\n", + configured_path.c_str()); + } + + // Fall back to searching the system PATH + if (const char *path_env = std::getenv("PATH")) { + std::string paths = path_env; + size_t pos = 0; + while (pos <= paths.size()) { + size_t colon = paths.find(':', pos); + std::string dir = paths.substr( + pos, colon == std::string::npos ? colon : colon - pos); + if (!dir.empty()) { + std::filesystem::path candidate = std::filesystem::path(dir) / "ffmpeg"; + if (is_executable_file(candidate)) { + return candidate.string(); + } + } + if (colon == std::string::npos) { + break; + } + pos = colon + 1; + } + } + + // Finally, try common install locations (PATH on GUI-launched apps, + // particularly on macOS, often lacks these) + std::vector candidates; + const std::string app_path = application_path(); + if (!app_path.empty()) { + candidates.push_back(app_path + "/ffmpeg"); + } +#ifdef __APPLE__ + candidates.push_back("/opt/homebrew/bin/ffmpeg"); + candidates.push_back("/usr/local/bin/ffmpeg"); +#endif + candidates.push_back("/usr/bin/ffmpeg"); + candidates.push_back("/usr/local/bin/ffmpeg"); + + for (const std::string &candidate : candidates) { + if (is_executable_file(candidate)) { + return std::filesystem::absolute(candidate).string(); + } + } + + return std::string(); +} + +ProxyManager::Proxy +ProxyManager::get_or_start_proxy(const std::string &cache_path, + const std::string &source_filename, int stream_index, + const ProxyParams ¶ms) +{ + const std::string filename = + get_proxy_filename(cache_path, source_filename, stream_index, params); + const ProxyState file_state = get_proxy_state(filename); + if (file_state == k_proxy_ready) { + return { k_proxy_ready, filename }; + } + + if (!oakcodec_task_submit_is_registered()) { + // Interim state (pre-M8): no task system, proxy cannot be generated + return { k_proxy_missing, filename }; + } + + if (file_state == k_proxy_generating) { + // Stale working file from an interrupted run + std::error_code ec; + std::filesystem::remove(get_working_proxy_filename(filename), ec); + } + + // The task owns the ".working.mp4" temporary name and the rename to the + // final filename on success (previously done in proxy_task_finished). + OakCodecTaskRequest req = {}; + req.kind = OAKCODEC_TASK_PROXY; + req.input_filename = source_filename.c_str(); + req.output_filename = filename.c_str(); + req.stream_index = stream_index; + if (params.divider <= 1) { + req.proxy_width = params.width; + req.proxy_height = params.height; + } + + // Interim simplification: submission is synchronous. + int result = SubmitTask(req); + if (result < 0) { + return { k_proxy_failed, filename }; + } + + if (get_proxy_state(filename) == k_proxy_ready) { + return { k_proxy_ready, filename }; + } + + return { k_proxy_generating, filename }; +} + +} // namespace olive diff --git a/src/codec/src/proxymanager.h b/src/codec/src/proxymanager.h new file mode 100644 index 000000000..b9e2bd9f0 --- /dev/null +++ b/src/codec/src/proxymanager.h @@ -0,0 +1,142 @@ +/* + * Oak Video Editor - Non-Linear Video Editor + * Copyright (C) 2026 Oak Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef OAK_PROXYMANAGER_H +#define OAK_PROXYMANAGER_H + +#include + +namespace olive +{ + +/** + * @brief Manages proxy (lower-res stand-in) generation + * + * Qt-free interim state: actual proxy transcodes are delegated to the + * global task submit callback (include/codec/task.h). While no callback + * is registered (pre-M8), get_or_start_proxy() reports the proxy as + * missing instead of starting background work. + * + * Behavior changes vs. the Qt version: + * - The `proxy_ready`/`proxy_finished` signals are gone; completion + * notification is the task system's / facade's business. + * - Submission is synchronous: get_or_start_proxy() calls the submit + * callback inline and re-derives the state from the filesystem. + * - proxy_params_from_config() returns compiled-in defaults until the + * config milestone wires a real store. + */ +class ProxyManager { +public: + static void create_instance() + { + if (!instance_) { + instance_ = new ProxyManager(); + } + } + + static void destroy_instance() + { + delete instance_; + instance_ = nullptr; + } + + static ProxyManager *instance() + { + return instance_; + } + + enum ProxyState { + k_proxy_missing, + k_proxy_generating, + k_proxy_ready, + k_proxy_failed + }; + + struct ProxyParams { + int width = 1280; + int height = 720; + /** + * @brief Source resolution divider (1 = use absolute width/height, + * 2/4/8 = fraction of the source resolution) + */ + int divider = 1; + int version = 1; + std::string extension = "mp4"; + int crf = 23; + std::string preset = "veryfast"; + bool include_audio = true; + }; + + struct Proxy { + ProxyState state = k_proxy_missing; + std::string filename; + }; + + static std::string get_proxy_directory(const std::string &cache_path); + + static std::string get_proxy_filename(const std::string &cache_path, + const std::string &source_filename, + int stream_index, + const ProxyParams ¶ms); + + static std::string get_working_proxy_filename(const std::string &proxy_filename); + + static ProxyState get_proxy_state(const std::string &proxy_filename); + + static std::string proxy_state_to_string(ProxyState state); + + static ProxyState proxy_state_from_string(const std::string &state); + + /** + * @brief Returns true if a proxy filename generated by GetProxyFilename() + * indicates the proxy contains audio streams + */ + static bool proxy_filename_has_audio(const std::string &proxy_filename); + + /** + * @brief Builds proxy parameters from the global application config + * + * Interim state: returns the compiled-in defaults (1280x720, divider 1, + * mp4, crf 23, veryfast, audio included); the config milestone wires + * the real store. + */ + static ProxyParams proxy_params_from_config(); + + /** + * @brief Locates an ffmpeg executable for proxy generation + * + * Resolution order: the explicitly configured path (if non-empty and an + * existing executable file), then the system PATH, then common + * platform-specific install locations. Returns an empty string if no + * executable could be found. + */ + static std::string find_f_fmpeg_executable(const std::string &configured_path); + + Proxy get_or_start_proxy(const std::string &cache_path, + const std::string &source_filename, int stream_index, + const ProxyParams ¶ms); + +private: + ProxyManager() = default; + + static ProxyManager *instance_; +}; + +} // namespace olive + +#endif // OAK_PROXYMANAGER_H diff --git a/src/codec/src/taskcallbacks.cpp b/src/codec/src/taskcallbacks.cpp new file mode 100644 index 000000000..fcb16badc --- /dev/null +++ b/src/codec/src/taskcallbacks.cpp @@ -0,0 +1,63 @@ +/* + + 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 "taskcallbacks.h" + +#include + +namespace +{ + +std::mutex g_task_cb_mutex; +oakcodec_task_submit_fn g_task_cb = nullptr; +void *g_task_cb_userdata = nullptr; + +} // namespace + +extern "C" { + +void oakcodec_set_task_submit_cb(oakcodec_task_submit_fn cb, void *userdata) +{ + std::lock_guard lock(g_task_cb_mutex); + g_task_cb = cb; + g_task_cb_userdata = userdata; +} + +int oakcodec_task_submit_is_registered(void) +{ + std::lock_guard lock(g_task_cb_mutex); + return g_task_cb != nullptr ? 1 : 0; +} + +} // extern "C" + +namespace olive +{ + +int SubmitTask(const OakCodecTaskRequest &req) +{ + std::lock_guard lock(g_task_cb_mutex); + if (!g_task_cb) { + return OAKCODEC_E_STATE; + } + return g_task_cb(&req, g_task_cb_userdata); +} + +} // namespace olive diff --git a/src/codec/src/taskcallbacks.h b/src/codec/src/taskcallbacks.h new file mode 100644 index 000000000..a48650103 --- /dev/null +++ b/src/codec/src/taskcallbacks.h @@ -0,0 +1,39 @@ +/* + + 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_CODEC_TASKCALLBACKS_H +#define OAK_CODEC_TASKCALLBACKS_H + +#include "codec/task.h" + +namespace olive +{ + +/** + * @brief C++-side convenience wrapper around the registered submit callback + * + * Returns the callback's return value, or OAKCODEC_E_STATE when no + * callback is registered (interim pre-M8 state). + */ +int SubmitTask(const OakCodecTaskRequest &req); + +} // namespace olive + +#endif // OAK_CODEC_TASKCALLBACKS_H diff --git a/src/codec/src/timecodemetadata.cpp b/src/codec/src/timecodemetadata.cpp new file mode 100644 index 000000000..2409d1197 --- /dev/null +++ b/src/codec/src/timecodemetadata.cpp @@ -0,0 +1,111 @@ +/*** + + Oak - 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 "timecodemetadata.h" + +#include +#include +#include + +#include "olive/core/util/timecodefunctions.h" + +namespace olive +{ + +namespace +{ + +std::string trimmed(const std::string &s) +{ + const char *ws = " \t\n\r\f\v"; + size_t begin = s.find_first_not_of(ws); + if (begin == std::string::npos) { + return std::string(); + } + size_t end = s.find_last_not_of(ws); + return s.substr(begin, end - begin + 1); +} + +} // namespace + +TimecodeMetadata::SourceTime +TimecodeMetadata::from_timecode_string(const std::string &timecode, + const core::Rational &timebase) +{ + SourceTime result; + const std::string trimmed_tc = trimmed(timecode); + if (trimmed_tc.empty()) { + return result; + } + + bool ok = false; + const core::Timecode::Display display = + trimmed_tc.find(';') != std::string::npos ? + core::Timecode::k_timecode_drop_frame : + core::Timecode::k_timecode_non_drop_frame; + result.time = + core::Timecode::timecode_to_time(trimmed_tc, timebase, display, &ok); + result.valid = ok; + if (ok) { + result.source = "timecode"; + } + return result; +} + +TimecodeMetadata::SourceTime +TimecodeMetadata::from_bwf_time_reference(const std::string &time_reference, + int sample_rate) +{ + SourceTime result; + if (sample_rate <= 0) { + return result; + } + + bool ok = false; + const std::string trimmed_ref = trimmed(time_reference); + char *end = nullptr; + const unsigned long long samples = + std::strtoull(trimmed_ref.c_str(), &end, 10); + ok = end != trimmed_ref.c_str() && *end == '\0'; + if (!ok) { + return result; + } + + unsigned long long numerator = samples; + unsigned long long denominator = static_cast(sample_rate); + const unsigned long long divisor = std::gcd(numerator, denominator); + numerator /= divisor; + denominator /= divisor; + + const unsigned long long rational_limit = + static_cast(std::numeric_limits::max()); + if (numerator <= rational_limit && denominator <= rational_limit) { + result.time = core::Rational(static_cast(numerator), + static_cast(denominator)); + } else { + result.time = core::Rational::from_double( + static_cast(samples) / static_cast(sample_rate)); + } + result.source = "bwf_time_reference"; + result.valid = true; + return result; +} + +} diff --git a/src/codec/src/timecodemetadata.h b/src/codec/src/timecodemetadata.h new file mode 100644 index 000000000..a7fa25252 --- /dev/null +++ b/src/codec/src/timecodemetadata.h @@ -0,0 +1,48 @@ +/*** + + Oak - 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_TIMECODEMETADATA_H +#define OAK_TIMECODEMETADATA_H + +#include + +#include "olive/core/util/rational.h" + +namespace olive +{ + +class TimecodeMetadata { +public: + struct SourceTime { + core::Rational time; + std::string source; + bool valid = false; + }; + + static SourceTime from_timecode_string(const std::string &timecode, + const core::Rational &timebase); + + static SourceTime from_bwf_time_reference(const std::string &time_reference, + int sample_rate); +}; + +} + +#endif // OAK_TIMECODEMETADATA_H diff --git a/src/codec/standalone/CMakeLists.txt b/src/codec/standalone/CMakeLists.txt new file mode 100644 index 000000000..c6097aee1 --- /dev/null +++ b/src/codec/standalone/CMakeLists.txt @@ -0,0 +1,155 @@ +# 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 . + +# Standalone build driver for the oakcodec module (M5). Mirrors +# src/render/standalone: oakcodec links oakrender (CancelAtom/texture C +# ABI), oakcommon, olivecore and ffmpeg_bridge; references into the +# not-yet-split task/config modules are interim no-ops (task submit +# callback registry, compiled-in proxy defaults), and oakrender's own +# dangling symbols resolve via -undefined dynamic_lookup (macOS). +# +# Usage (macOS/Homebrew): +# cmake -S src/codec/standalone -B build-oakcodec +# cmake --build build-oakcodec -j +# ctest --test-dir build-oakcodec + +cmake_minimum_required(VERSION 3.16 FATAL_ERROR) + +project(oakcodec-standalone LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +get_filename_component(OAK_REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../.." ABSOLUTE) + +list(APPEND CMAKE_MODULE_PATH "${OAK_REPO_ROOT}/cmake") +if(EXISTS "/opt/homebrew") + list(APPEND CMAKE_PREFIX_PATH "/opt/homebrew") +endif() + +find_package(EXPAT REQUIRED) +find_package(OpenColorIO CONFIG REQUIRED) +find_package(OpenImageIO CONFIG REQUIRED) + +set(OCIO_LIBRARIES OpenColorIO::OpenColorIO) +set(OCIO_INCLUDE_DIRS "") +set(OIIO_LIBRARIES OpenImageIO::OpenImageIO) +set(OIIO_INCLUDE_DIRS "") + +# In-repo libraries, built from source (same set src/render/standalone +# assembles, because oakcodec links oakrender): +# - olivecore (core/): oakcore_* C ABI and olive::core C++ utils +# - ffmpeg_bridge: fb_* C ABI (the only FFmpeg access codec performs) +# - oakundo / oakcommon / oaknode: oakrender's own dependencies +# - oakrender: CancelAtom + texture C ABI consumed by codec +set(OLIVECORE_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(CMAKE_DISABLE_FIND_PACKAGE_OpenTimelineIO ON) + +add_subdirectory(${OAK_REPO_ROOT}/core ${CMAKE_BINARY_DIR}/core) +target_include_directories(olivecore PUBLIC ${OAK_REPO_ROOT}/third_party/openfx/include) +add_subdirectory(${OAK_REPO_ROOT}/ffmpeg_bridge ${CMAKE_BINARY_DIR}/ffmpeg_bridge) + +set(BUILD_TESTS OFF) +add_subdirectory(${OAK_REPO_ROOT}/src/undo ${CMAKE_BINARY_DIR}/undo) +add_subdirectory(${OAK_REPO_ROOT}/src/common ${CMAKE_BINARY_DIR}/common) +add_subdirectory(${OAK_REPO_ROOT}/src/node ${CMAKE_BINARY_DIR}/node) +set(BUILD_TESTS ON) + +# oaknode needs its transition stubs when built in this tree (see +# src/render/standalone/CMakeLists.txt). +target_include_directories(oaknode BEFORE PUBLIC + ${OAK_REPO_ROOT}/src/render/transition + ${OAK_REPO_ROOT}/src/node/transition + ${OAK_REPO_ROOT}/src/render/src +) +target_include_directories(oaknode PUBLIC + ${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include + /opt/homebrew/include + /opt/homebrew/include/Imath +) +target_link_options(oaknode PRIVATE + "-undefined" "dynamic_lookup" +) + +add_subdirectory(${OAK_REPO_ROOT}/src/render/src ${CMAKE_BINARY_DIR}/render) +add_subdirectory(${OAK_REPO_ROOT}/src/render/c_api ${CMAKE_BINARY_DIR}/render_c_api) + +# Transition stub dirs must precede everything else: src/render/transition +# first, then src/node/transition (shared stubs). +target_include_directories(oakrender BEFORE PUBLIC + ${OAK_REPO_ROOT}/src/render/transition + ${OAK_REPO_ROOT}/src/node/transition +) + +target_include_directories(oakrender PUBLIC + ${OAK_REPO_ROOT}/engine/include + ${OAK_REPO_ROOT}/third_party/openfx/HostSupport/include + /opt/homebrew/include + /opt/homebrew/include/Imath + /opt/homebrew/include/OpenEXR +) + +# Vulkan headers (Homebrew keg-only vulkan-headers). +if(NOT EXISTS "/opt/homebrew/include/vulkan/vulkan.h") + execute_process(COMMAND brew --prefix vulkan-headers + OUTPUT_VARIABLE VULKAN_HEADERS_PREFIX + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + if(VULKAN_HEADERS_PREFIX AND EXISTS "${VULKAN_HEADERS_PREFIX}/include/vulkan/vulkan.h") + target_include_directories(oakrender PUBLIC "${VULKAN_HEADERS_PREFIX}/include") + endif() +endif() + +# Symbols of the not-yet-split engine modules dangle by design. The +# backend libraries resolve most symbols from liboakrender at load time +# and dangle the same way. +foreach(t oakrender oakgl oakgl2 oakvulkan) + if(TARGET ${t}) + target_link_options(${t} PRIVATE + "-undefined" "dynamic_lookup" + ) + endif() +endforeach() + +target_link_libraries(oakrender PRIVATE + oaknode + oakcommon + oakundo + olivecore + ffmpeg_bridge + ${OCIO_LIBRARIES} + ${OIIO_LIBRARIES} + "-framework OpenGL" + "-framework CoreVideo" + "-framework Metal" + "-framework QuartzCore" +) + +# oakcodec itself. Its own dangling references (none expected beyond what +# the linked libraries already dangle) resolve the same way. +add_subdirectory(${OAK_REPO_ROOT}/src/codec/src ${CMAKE_BINARY_DIR}/codec) +add_subdirectory(${OAK_REPO_ROOT}/src/codec/c_api ${CMAKE_BINARY_DIR}/codec_c_api) + +target_link_options(oakcodec PRIVATE + "-undefined" "dynamic_lookup" +) + +# Tests (oakcodec-gtest). +if(BUILD_TESTS) + enable_testing() + add_subdirectory(${OAK_REPO_ROOT}/src/codec/tests ${CMAKE_BINARY_DIR}/codec_tests) +endif() diff --git a/src/codec/tests/CMakeLists.txt b/src/codec/tests/CMakeLists.txt new file mode 100644 index 000000000..3ffea577c --- /dev/null +++ b/src/codec/tests/CMakeLists.txt @@ -0,0 +1,84 @@ +# 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 . + +find_package(GTest REQUIRED) +include(GoogleTest) + +# 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() + +add_executable(oakcodec-gtest + frame_test.cpp + decoder_test.cpp + encoder_test.cpp + task_test.cpp +) + +target_link_libraries(oakcodec-gtest PRIVATE + oakcodec + oakrender + oaknode + oakcommon + oakundo + olivecore + GTest::gtest + GTest::gtest_main +) + +# liboakrender/liboaknode dangle OFX host symbols (-undefined +# dynamic_lookup); force-load the host support archive into the test +# process so dyld finds them in the flat namespace at startup. Mirrors +# src/render/tests/CMakeLists.txt. +if(NOT DEFINED OAKRENDER_OFX_HOST_ARCHIVE) + find_library(OAKRENDER_OFX_HOST_ARCHIVE NAMES OfxHost + PATHS ${OAK_REPO_ROOT}/build/third_party/openfx/HostSupport) +endif() +if(NOT OAKRENDER_OFX_HOST_ARCHIVE) + message(FATAL_ERROR + "libOfxHost.a not found; run the full-tree build once or set " + "OAKRENDER_OFX_HOST_ARCHIVE") +endif() +target_link_options(oakcodec-gtest PRIVATE + "-Wl,-force_load,${OAKRENDER_OFX_HOST_ARCHIVE}") + +# liboakrender references the oakengine_ipc_* C ABI (worker IPC) via +# dynamic_lookup; the test binary links the inert shim from +# src/node/standalone instead. +target_sources(oakcodec-gtest PRIVATE + ${OAK_REPO_ROOT}/src/node/standalone/oakengine_ipc_shim.cpp) +target_include_directories(oakcodec-gtest PRIVATE + ${OAK_REPO_ROOT}/engine/include +) + +# include/ must win over the render/node transition dirs that leak in +# through oaknode's PUBLIC includes: they carry codec/*.h stubs that would +# otherwise shadow the real oakcodec public headers. -iquote is searched +# before every -I for quoted includes. +target_compile_options(oakcodec-gtest PRIVATE + "-iquote" "${OAK_REPO_ROOT}/include" +) + +# tests/demo.mp4 lives at the repo's shared tests directory. +target_compile_definitions(oakcodec-gtest PRIVATE + OAKCODEC_TEST_DATA_DIR="${OAK_REPO_ROOT}/tests") + +gtest_discover_tests(oakcodec-gtest + DISCOVERY_MODE PRE_TEST + PROPERTIES ENVIRONMENT + "OCIO=${OAK_REPO_ROOT}/engine/render/ocioconf/config.ocio") diff --git a/src/codec/tests/decoder_test.cpp b/src/codec/tests/decoder_test.cpp new file mode 100644 index 000000000..1f788e2cc --- /dev/null +++ b/src/codec/tests/decoder_test.cpp @@ -0,0 +1,180 @@ +/*** + + Oak Video Editor - Non-Linear Video Editor + Copyright (C) 2026 Oak Team + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +***/ + +#include + +#include +#include + +#include "codec/decoder.h" + +#ifndef OAKCODEC_TEST_DATA_DIR +#define OAKCODEC_TEST_DATA_DIR "tests" +#endif + +namespace +{ + +std::string demo_path() +{ + return std::string(OAKCODEC_TEST_DATA_DIR) + "/demo.mp4"; +} + +bool demo_exists() +{ + FILE *f = fopen(demo_path().c_str(), "rb"); + if (f) { + fclose(f); + return true; + } + return false; +} + +} // namespace + +TEST(OakCodecDecoder, ProbeDemoMp4) +{ + if (!demo_exists()) { + GTEST_SKIP() << "tests/demo.mp4 not available"; + } + + int before = oakcodec_debug_alive_count(); + + OakDecoder probe = oakcodec_decoder_probe(demo_path().c_str()); + ASSERT_NE(probe.ctx, nullptr); + + char name[64] = {}; + EXPECT_GT(oakcodec_decoder_probe_decoder_name(probe, name, sizeof(name)), + 0); + EXPECT_STRNE(name, ""); + + int video_count = oakcodec_decoder_probe_video_stream_count(probe); + EXPECT_GE(video_count, 1); + + if (video_count >= 1) { + oakcodec_video_stream_info info = {}; + ASSERT_EQ(oakcodec_decoder_probe_get_video_stream(probe, 0, &info), + OAKCODEC_OK); + EXPECT_GT(info.width, 0); + EXPECT_GT(info.height, 0); + EXPECT_GT(info.time_base_den, 0); + + // Out-of-range index + EXPECT_EQ(oakcodec_decoder_probe_get_video_stream( + probe, video_count, &info), + OAKCODEC_E_NOT_FOUND); + } + + // Audio stream enumeration must not crash (count may be 0) + int audio_count = oakcodec_decoder_probe_audio_stream_count(probe); + EXPECT_GE(audio_count, 0); + if (audio_count >= 1) { + oakcodec_audio_stream_info ainfo = {}; + ASSERT_EQ(oakcodec_decoder_probe_get_audio_stream(probe, 0, &ainfo), + OAKCODEC_OK); + EXPECT_GT(ainfo.sample_rate, 0); + } + + oakcodec_decoder_free(&probe); + EXPECT_EQ(probe.ctx, nullptr); + EXPECT_EQ(oakcodec_debug_alive_count(), before); +} + +TEST(OakCodecDecoder, ProbeMissingFile) +{ + OakDecoder probe = + oakcodec_decoder_probe("/nonexistent/path/to/file.mp4"); + EXPECT_EQ(probe.ctx, nullptr); + + char err[256] = {}; + EXPECT_GT(oakcodec_probe_last_error(err, sizeof(err)), 1); + EXPECT_STRNE(err, ""); + + oakcodec_decoder_free(&probe); // no-op +} + +TEST(OakCodecDecoder, OpenAndDecodeFirstFrame) +{ + if (!demo_exists()) { + GTEST_SKIP() << "tests/demo.mp4 not available"; + } + + // Find the first video stream index via a probe. + OakDecoder probe = oakcodec_decoder_probe(demo_path().c_str()); + ASSERT_NE(probe.ctx, nullptr); + ASSERT_GE(oakcodec_decoder_probe_video_stream_count(probe), 1); + oakcodec_video_stream_info info = {}; + ASSERT_EQ(oakcodec_decoder_probe_get_video_stream(probe, 0, &info), + OAKCODEC_OK); + int stream_index = info.stream_index; + oakcodec_decoder_free(&probe); + + int before = oakcodec_debug_alive_count(); + + OakDecoder d = oakcodec_decoder_init(); + ASSERT_NE(d.ctx, nullptr); + EXPECT_EQ(oakcodec_decoder_is_open(d), 0); + + ASSERT_EQ(oakcodec_decoder_open(d, demo_path().c_str(), stream_index), + OAKCODEC_OK); + EXPECT_EQ(oakcodec_decoder_is_open(d), 1); + + OakFrame frame = oakcodec_decoder_decode_video(d, 0, 1); + ASSERT_NE(frame.ctx, nullptr); + EXPECT_EQ(oakcodec_frame_is_allocated(frame), 1); + EXPECT_NE(oakcodec_frame_const_data(frame), nullptr); + EXPECT_GT(oakcodec_frame_width(frame), 0); + EXPECT_GT(oakcodec_frame_height(frame), 0); + + oakcodec_frame_free(&frame); + + EXPECT_EQ(oakcodec_decoder_close(d), OAKCODEC_OK); + EXPECT_EQ(oakcodec_decoder_is_open(d), 0); + + oakcodec_decoder_free(&d); + EXPECT_EQ(oakcodec_debug_alive_count(), before); +} + +TEST(OakCodecDecoder, OpenMissingFile) +{ + OakDecoder d = oakcodec_decoder_init(); + ASSERT_NE(d.ctx, nullptr); + + int rc = oakcodec_decoder_open(d, "/nonexistent/video.mp4", 0); + EXPECT_EQ(rc, OAKCODEC_E_NOT_FOUND); + EXPECT_EQ(oakcodec_decoder_is_open(d), 0); + + char err[256] = {}; + EXPECT_GT(oakcodec_decoder_last_error(d, err, sizeof(err)), 1); + EXPECT_STRNE(err, ""); + + oakcodec_decoder_free(&d); +} + +TEST(OakCodecDecoder, EmptyHandleSemantics) +{ + OakDecoder empty = {}; + EXPECT_EQ(oakcodec_decoder_is_open(empty), 0); + EXPECT_EQ(oakcodec_decoder_probe_video_stream_count(empty), 0); + OakFrame f = oakcodec_decoder_decode_video(empty, 0, 1); + EXPECT_EQ(f.ctx, nullptr); + oakcodec_decoder_free(nullptr); + oakcodec_decoder_free(&empty); +} diff --git a/src/codec/tests/encoder_test.cpp b/src/codec/tests/encoder_test.cpp new file mode 100644 index 000000000..18e8d26b4 --- /dev/null +++ b/src/codec/tests/encoder_test.cpp @@ -0,0 +1,143 @@ +/*** + + 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 + +#include +#include +#include + +#include "codec/decoder.h" +#include "codec/encoder.h" + +// Format/codec values mirror oakengine/encoding.h (olive::ExportFormat / +// olive::ExportCodec). +#define TEST_FORMAT_MPEG4 2 +#define TEST_CODEC_H264 1 + +namespace +{ + +std::string temp_mp4_path() +{ + std::string p = std::string("/tmp/oakcodec_encoder_test.mp4"); + remove(p.c_str()); + return p; +} + +} // namespace + +TEST(OakCodecEncoder, EncodeMp4RoundTrip) +{ + std::string path = temp_mp4_path(); + + int before = oakcodec_debug_alive_count(); + + oakcodec_encoding_params params = {}; + snprintf(params.filename, sizeof(params.filename), "%s", path.c_str()); + params.format = TEST_FORMAT_MPEG4; + params.video_enabled = 1; + params.video_codec = TEST_CODEC_H264; + params.video_width = 64; + params.video_height = 64; + params.video_time_base_num = 1; + params.video_time_base_den = 25; + params.video_pixel_format = OAKCOMMON_PIXEL_FORMAT_U8; + params.video_interlacing = OAKCODEC_INTERLACE_NONE; + params.video_pixel_aspect_num = 1; + params.video_pixel_aspect_den = 1; + params.video_bit_rate = 200000; + snprintf(params.video_pix_fmt, sizeof(params.video_pix_fmt), "yuv420p"); + + OakEncoder enc = oakcodec_encoder_init(¶ms); + ASSERT_NE(enc.ctx, nullptr); + + if (oakcodec_encoder_open(enc) != OAKCODEC_OK) { + char err[512] = {}; + oakcodec_encoder_last_error(enc, err, sizeof(err)); + oakcodec_encoder_free(&enc); + GTEST_SKIP() << "encoder not available in this environment: " << err; + } + + // Write 10 solid frames. + OakVideoParams vp = oakcommon_videoparams_init_with_time_base( + 64, 64, 1, 25, OAKCOMMON_PIXEL_FORMAT_U8, 4, 1, 1, + OAKCOMMON_VIDEO_INTERLACE_NONE, 1); + OakFrame frame = oakcodec_frame_init_with_params(vp); + oakcommon_videoparams_free(&vp); + ASSERT_NE(frame.ctx, nullptr); + ASSERT_EQ(oakcodec_frame_allocate(frame), OAKCODEC_OK); + + int linesize = oakcodec_frame_linesize_bytes(frame); + for (int i = 0; i < 10; i++) { + memset(oakcodec_frame_data(frame), 16 + i * 10, + static_cast(linesize) * 64); + ASSERT_EQ(oakcodec_frame_set_timestamp(frame, i, 25), OAKCODEC_OK); + ASSERT_EQ(oakcodec_encoder_write_video(enc, frame), OAKCODEC_OK); + } + + EXPECT_EQ(oakcodec_encoder_flush(enc), OAKCODEC_OK); + // Writing after flush is a state error. + EXPECT_EQ(oakcodec_encoder_write_video(enc, frame), OAKCODEC_E_STATE); + + oakcodec_frame_free(&frame); + oakcodec_encoder_free(&enc); + EXPECT_EQ(oakcodec_debug_alive_count(), before); + + // Round-trip: the decoder must open the file and decode a frame. + OakDecoder probe = oakcodec_decoder_probe(path.c_str()); + ASSERT_NE(probe.ctx, nullptr); + ASSERT_GE(oakcodec_decoder_probe_video_stream_count(probe), 1); + oakcodec_video_stream_info info = {}; + ASSERT_EQ(oakcodec_decoder_probe_get_video_stream(probe, 0, &info), + OAKCODEC_OK); + EXPECT_EQ(info.width, 64); + EXPECT_EQ(info.height, 64); + int stream_index = info.stream_index; + oakcodec_decoder_free(&probe); + + OakDecoder dec = oakcodec_decoder_init(); + ASSERT_NE(dec.ctx, nullptr); + ASSERT_EQ(oakcodec_decoder_open(dec, path.c_str(), stream_index), + OAKCODEC_OK); + OakFrame decoded = oakcodec_decoder_decode_video(dec, 0, 1); + ASSERT_NE(decoded.ctx, nullptr); + EXPECT_NE(oakcodec_frame_const_data(decoded), nullptr); + oakcodec_frame_free(&decoded); + oakcodec_decoder_free(&dec); + + remove(path.c_str()); + + EXPECT_EQ(oakcodec_debug_alive_count(), before); +} + +TEST(OakCodecEncoder, EmptyHandleSemantics) +{ + OakEncoder empty = {}; + EXPECT_EQ(oakcodec_encoder_open(empty), OAKCODEC_E_INVALID); + EXPECT_EQ(oakcodec_encoder_flush(empty), OAKCODEC_E_INVALID); + oakcodec_encoder_free(nullptr); + oakcodec_encoder_free(&empty); + + // An all-disabled params struct is invalid -> empty handle. + oakcodec_encoding_params params = {}; + OakEncoder enc = oakcodec_encoder_init(¶ms); + EXPECT_EQ(enc.ctx, nullptr); +} diff --git a/src/codec/tests/frame_test.cpp b/src/codec/tests/frame_test.cpp new file mode 100644 index 000000000..b693d048e --- /dev/null +++ b/src/codec/tests/frame_test.cpp @@ -0,0 +1,157 @@ +/*** + + 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 + +#include "codec/frame.h" + +namespace +{ + +OakVideoParams make_params(int width, int height) +{ + return oakcommon_videoparams_init_with_time_base( + width, height, 1001, 30000, OAKCOMMON_PIXEL_FORMAT_U8, 4, 1, 1, + OAKCOMMON_VIDEO_INTERLACE_NONE, 1); +} + +} // namespace + +TEST(OakCodecFrame, InitAndFree) +{ + int before = oakcodec_debug_alive_count(); + + OakFrame f = oakcodec_frame_init(); + ASSERT_NE(f.ctx, nullptr); + EXPECT_EQ(f.abi_version, OAKCODEC_ABI_VERSION); + EXPECT_EQ(oakcodec_debug_alive_count(), before + 1); + + oakcodec_frame_free(&f); + EXPECT_EQ(f.ctx, nullptr); + EXPECT_EQ(oakcodec_debug_alive_count(), before); + + // NULL / empty no-ops + oakcodec_frame_free(nullptr); + oakcodec_frame_free(&f); +} + +TEST(OakCodecFrame, ParamsRoundTrip) +{ + OakFrame f = oakcodec_frame_init(); + ASSERT_NE(f.ctx, nullptr); + + OakVideoParams p = make_params(320, 240); + ASSERT_EQ(oakcodec_frame_set_params(f, p), OAKCODEC_OK); + oakcommon_videoparams_free(&p); + + OakVideoParams out = {}; + ASSERT_EQ(oakcodec_frame_get_params(f, &out), OAKCODEC_OK); + + int w = 0, h = 0, fmt = -2, ch = 0, tb_num = 0, tb_den = 0; + oakcommon_videoparams_get_width(out, &w); + oakcommon_videoparams_get_height(out, &h); + oakcommon_videoparams_get_format(out, &fmt); + oakcommon_videoparams_get_channel_count(out, &ch); + oakcommon_videoparams_get_time_base(out, &tb_num, &tb_den); + oakcommon_videoparams_free(&out); + + EXPECT_EQ(w, 320); + EXPECT_EQ(h, 240); + EXPECT_EQ(fmt, OAKCOMMON_PIXEL_FORMAT_U8); + EXPECT_EQ(ch, 4); + EXPECT_EQ(tb_num, 1001); + EXPECT_EQ(tb_den, 30000); + + oakcodec_frame_free(&f); +} + +TEST(OakCodecFrame, AllocateDataLinesize) +{ + OakVideoParams p = make_params(64, 48); + OakFrame f = oakcodec_frame_init_with_params(p); + oakcommon_videoparams_free(&p); + ASSERT_NE(f.ctx, nullptr); + + EXPECT_EQ(oakcodec_frame_is_allocated(f), 0); + EXPECT_EQ(oakcodec_frame_data(f), nullptr); + + ASSERT_EQ(oakcodec_frame_allocate(f), OAKCODEC_OK); + EXPECT_EQ(oakcodec_frame_is_allocated(f), 1); + ASSERT_NE(oakcodec_frame_data(f), nullptr); + EXPECT_EQ(oakcodec_frame_const_data(f), oakcodec_frame_data(f)); + + // u8 rgba = 4 bytes/px, width 64 aligned to 32 -> 64 * 4 + EXPECT_EQ(oakcodec_frame_linesize_bytes(f), 64 * 4); + EXPECT_EQ(oakcodec_frame_linesize_pixels(f), 64); + EXPECT_EQ(oakcodec_frame_allocated_size(f), 64 * 4 * 48); + EXPECT_EQ(oakcodec_frame_width(f), 64); + EXPECT_EQ(oakcodec_frame_height(f), 48); + EXPECT_EQ(oakcodec_frame_format(f), OAKCOMMON_PIXEL_FORMAT_U8); + EXPECT_EQ(oakcodec_frame_channel_count(f), 4); + + // Allocating again is a successful no-op + EXPECT_EQ(oakcodec_frame_allocate(f), OAKCODEC_OK); + + oakcodec_frame_free(&f); +} + +TEST(OakCodecFrame, RefCounting) +{ + int before = oakcodec_debug_alive_count(); + + OakFrame f = oakcodec_frame_init(); + ASSERT_NE(f.ctx, nullptr); + + // Copy the struct and addref: two references, one object. + OakFrame copy = f; + copy.addref(copy.ctx); + + // Release the copy; object stays alive. + copy.release(copy.ctx); + EXPECT_EQ(oakcodec_debug_alive_count(), before + 1); + EXPECT_EQ(oakcodec_frame_width(f), 0); // still valid, default params + + oakcodec_frame_free(&f); + EXPECT_EQ(oakcodec_debug_alive_count(), before); +} + +TEST(OakCodecFrame, Timestamp) +{ + OakFrame f = oakcodec_frame_init(); + ASSERT_NE(f.ctx, nullptr); + + ASSERT_EQ(oakcodec_frame_set_timestamp(f, 1001, 30000), OAKCODEC_OK); + int num = 0, den = 0; + ASSERT_EQ(oakcodec_frame_get_timestamp(f, &num, &den), OAKCODEC_OK); + EXPECT_EQ(num, 1001); + EXPECT_EQ(den, 30000); + + oakcodec_frame_free(&f); +} + +TEST(OakCodecFrame, EmptyHandleSemantics) +{ + OakFrame empty = {}; + EXPECT_EQ(oakcodec_frame_get_params(empty, nullptr), OAKCODEC_E_INVALID); + EXPECT_EQ(oakcodec_frame_allocate(empty), OAKCODEC_E_INVALID); + EXPECT_EQ(oakcodec_frame_is_allocated(empty), 0); + EXPECT_EQ(oakcodec_frame_data(empty), nullptr); + EXPECT_EQ(oakcodec_frame_width(empty), 0); +} diff --git a/src/codec/tests/task_test.cpp b/src/codec/tests/task_test.cpp new file mode 100644 index 000000000..f242f7d1d --- /dev/null +++ b/src/codec/tests/task_test.cpp @@ -0,0 +1,184 @@ +/*** + + 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 + +#include +#include + +#include "codec/conform.h" +#include "codec/proxy.h" +#include "codec/task.h" + +namespace +{ + +struct SubmitLog { + int calls = 0; + OakCodecTaskRequest last = {}; + std::string input; + std::string output; +}; + +int recording_submit(const OakCodecTaskRequest *req, void *userdata) +{ + auto *log = static_cast(userdata); + log->calls++; + log->last = *req; + log->input = req->input_filename ? req->input_filename : ""; + log->output = req->output_filename ? req->output_filename : ""; + // Accept the task but do no work (files never appear). + return OAKCODEC_OK; +} + +struct TaskRegistrarGuard { + TaskRegistrarGuard() { oakcodec_set_task_submit_cb(nullptr, nullptr); } + ~TaskRegistrarGuard() { oakcodec_set_task_submit_cb(nullptr, nullptr); } +}; + +} // namespace + +TEST(OakCodecTask, RegistryRoundTrip) +{ + TaskRegistrarGuard guard; + + EXPECT_EQ(oakcodec_task_submit_is_registered(), 0); + + SubmitLog log; + oakcodec_set_task_submit_cb(&recording_submit, &log); + EXPECT_EQ(oakcodec_task_submit_is_registered(), 1); + + oakcodec_set_task_submit_cb(nullptr, nullptr); + EXPECT_EQ(oakcodec_task_submit_is_registered(), 0); +} + +TEST(OakCodecConform, UnregisteredReportsUnavailable) +{ + TaskRegistrarGuard guard; + + ASSERT_EQ(oakcodec_conform_create_instance(), OAKCODEC_OK); + + int state = oakcodec_conform_get_state("/tmp/oakcodec_conform_test", + "some_video.mp4", 1, 48000, 0x3, 4, + 1); + EXPECT_EQ(state, OAKCODEC_CONFORM_UNAVAILABLE); + + // Filename computation is still deterministic without a registrar. + int count = oakcodec_conform_filename_count( + "/tmp/oakcodec_conform_test", "some_video.mp4", 1, 48000, 0x3, 4); + EXPECT_GE(count, 1); // stereo layout -> 2 channels + if (count >= 1) { + char buf[1024] = {}; + EXPECT_GT(oakcodec_conform_filename_at( + "/tmp/oakcodec_conform_test", "some_video.mp4", 1, + 48000, 0x3, 4, 0, buf, sizeof(buf)), + 1); + EXPECT_STRNE(buf, ""); + EXPECT_EQ(oakcodec_conform_filename_at( + "/tmp/oakcodec_conform_test", "some_video.mp4", 1, + 48000, 0x3, 4, count, buf, sizeof(buf)), + OAKCODEC_E_NOT_FOUND); + } + + oakcodec_conform_destroy_instance(); +} + +TEST(OakCodecConform, RegisteredSubmitIsInvoked) +{ + TaskRegistrarGuard guard; + SubmitLog log; + oakcodec_set_task_submit_cb(&recording_submit, &log); + + ASSERT_EQ(oakcodec_conform_create_instance(), OAKCODEC_OK); + + // wait=0: the (no-op) task was "queued" -> GENERATING. + int state = oakcodec_conform_get_state("/tmp/oakcodec_conform_test", + "some_video.mp4", 1, 48000, 0x3, 4, + 0); + EXPECT_EQ(state, OAKCODEC_CONFORM_GENERATING); + EXPECT_EQ(log.calls, 1); + EXPECT_EQ(log.last.kind, OAKCODEC_TASK_CONFORM); + EXPECT_EQ(log.last.stream_index, 1); + EXPECT_EQ(log.last.sample_rate, 48000); + + // wait=1: post-submit miss -> UNAVAILABLE. + state = oakcodec_conform_get_state("/tmp/oakcodec_conform_test", + "some_video.mp4", 1, 48000, 0x3, 4, 1); + EXPECT_EQ(state, OAKCODEC_CONFORM_UNAVAILABLE); + + oakcodec_conform_destroy_instance(); +} + +TEST(OakCodecProxy, MissingAndStateStrings) +{ + TaskRegistrarGuard guard; + + ASSERT_EQ(oakcodec_proxy_create_instance(), OAKCODEC_OK); + + EXPECT_EQ(oakcodec_proxy_get_state(nullptr), OAKCODEC_PROXY_STATE_MISSING); + EXPECT_EQ(oakcodec_proxy_get_state("/nonexistent/proxy.mp4"), + OAKCODEC_PROXY_STATE_MISSING); + + char buf[64] = {}; + EXPECT_GT(oakcodec_proxy_state_to_string(OAKCODEC_PROXY_STATE_READY, buf, + sizeof(buf)), + 1); + EXPECT_EQ(oakcodec_proxy_state_to_string(99, buf, sizeof(buf)), + OAKCODEC_E_INVALID); + + oakcodec_proxy_params params = {}; + ASSERT_EQ(oakcodec_proxy_params_default(¶ms), OAKCODEC_OK); + EXPECT_GT(params.width, 0); + EXPECT_STRNE(params.extension, ""); + + oakcodec_proxy_result result = {}; + ASSERT_EQ(oakcodec_proxy_get_or_start("/tmp/oakcodec_proxy_test", + "some_video.mp4", 0, ¶ms, &result), + OAKCODEC_OK); + // No registrar: stays missing, filename is still computed. + EXPECT_EQ(result.state, OAKCODEC_PROXY_STATE_MISSING); + EXPECT_STRNE(result.filename, ""); + + oakcodec_proxy_destroy_instance(); +} + +TEST(OakCodecProxy, RegisteredSubmitIsInvoked) +{ + TaskRegistrarGuard guard; + SubmitLog log; + oakcodec_set_task_submit_cb(&recording_submit, &log); + + ASSERT_EQ(oakcodec_proxy_create_instance(), OAKCODEC_OK); + + oakcodec_proxy_params params = {}; + ASSERT_EQ(oakcodec_proxy_params_default(¶ms), OAKCODEC_OK); + + oakcodec_proxy_result result = {}; + ASSERT_EQ(oakcodec_proxy_get_or_start("/tmp/oakcodec_proxy_test", + "some_video.mp4", 2, ¶ms, &result), + OAKCODEC_OK); + EXPECT_EQ(log.calls, 1); + EXPECT_EQ(log.last.kind, OAKCODEC_TASK_PROXY); + EXPECT_EQ(log.last.stream_index, 2); + // Task accepted but produced nothing -> generating. + EXPECT_EQ(result.state, OAKCODEC_PROXY_STATE_GENERATING); + + oakcodec_proxy_destroy_instance(); +} diff --git a/src/common/c_api/colortransform.cpp b/src/common/c_api/colortransform.cpp index a6ebce08a..f202f6101 100644 --- a/src/common/c_api/colortransform.cpp +++ b/src/common/c_api/colortransform.cpp @@ -23,10 +23,15 @@ #include #include "../src/colortransform.h" +#include "refcounted.h" -struct OakCommonColorTransform { - olive::ColorTransform impl; -}; +/** + * @brief Recover the boxed olive::ColorTransform from a handle (NULL-safe). + */ +static olive::ColorTransform *ct(OakColorTransform transform) +{ + return oakcommon::handle_impl(transform.ctx); +} static int copy_string(const std::string &value, char *buf, int buf_size) { @@ -36,89 +41,116 @@ static int copy_string(const std::string &value, char *buf, int buf_size) return needed; } -OakCommonColorTransform *oakcommon_colortransform_init_output( +OakColorTransform oakcommon_colortransform_init_output( const char *output) { + OakColorTransform h = {}; if (!output) - return nullptr; + return h; try { - return new OakCommonColorTransform{ - olive::ColorTransform(std::string(output))}; + return oakcommon::make_handle( + olive::ColorTransform(std::string(output))); } catch (...) { - return nullptr; + OakColorTransform empty = {}; + return empty; } } -OakCommonColorTransform *oakcommon_colortransform_init_display( +OakColorTransform oakcommon_colortransform_init_display( const char *display, const char *view, const char *look) { + OakColorTransform h = {}; if (!display || !view || !look) - return nullptr; + return h; try { - return new OakCommonColorTransform{olive::ColorTransform( - std::string(display), std::string(view), std::string(look))}; + return oakcommon::make_handle( + olive::ColorTransform(std::string(display), std::string(view), + std::string(look))); } catch (...) { - return nullptr; + OakColorTransform empty = {}; + return empty; } } -void oakcommon_colortransform_free(OakCommonColorTransform *transform) +OakColorTransform oakcommon_colortransform_init_from_native( + const olive::ColorTransform *src) { - delete transform; + if (!src) { + OakColorTransform h = {}; + return h; + } + try { + return oakcommon::make_handle( + olive::ColorTransform(*src)); + } catch (...) { + OakColorTransform h = {}; + return h; + } } -int oakcommon_colortransform_is_display(OakCommonColorTransform *transform, +const olive::ColorTransform *oakcommon_colortransform_get_native( + OakColorTransform transform) +{ + return ct(transform); +} + +void oakcommon_colortransform_free(OakColorTransform *transform) +{ + oakcommon::free_handle(transform); +} + +int oakcommon_colortransform_is_display(OakColorTransform transform, int *is_display) { - if (!transform || !is_display) + if (!ct(transform) || !is_display) return OAKCOMMON_E_INVALID; - *is_display = transform->impl.is_display() ? 1 : 0; + *is_display = ct(transform)->is_display() ? 1 : 0; return OAKCOMMON_OK; } -int oakcommon_colortransform_get_display(OakCommonColorTransform *transform, +int oakcommon_colortransform_get_display(OakColorTransform transform, char *buf, int buf_size) { - if (!transform) + if (!ct(transform)) return OAKCOMMON_E_INVALID; try { - return copy_string(transform->impl.display(), buf, buf_size); + return copy_string(ct(transform)->display(), buf, buf_size); } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_colortransform_get_output(OakCommonColorTransform *transform, +int oakcommon_colortransform_get_output(OakColorTransform transform, char *buf, int buf_size) { - if (!transform) + if (!ct(transform)) return OAKCOMMON_E_INVALID; try { - return copy_string(transform->impl.output(), buf, buf_size); + return copy_string(ct(transform)->output(), buf, buf_size); } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_colortransform_get_view(OakCommonColorTransform *transform, +int oakcommon_colortransform_get_view(OakColorTransform transform, char *buf, int buf_size) { - if (!transform) + if (!ct(transform)) return OAKCOMMON_E_INVALID; try { - return copy_string(transform->impl.view(), buf, buf_size); + return copy_string(ct(transform)->view(), buf, buf_size); } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_colortransform_get_look(OakCommonColorTransform *transform, +int oakcommon_colortransform_get_look(OakColorTransform transform, char *buf, int buf_size) { - if (!transform) + if (!ct(transform)) return OAKCOMMON_E_INVALID; try { - return copy_string(transform->impl.look(), buf, buf_size); + return copy_string(ct(transform)->look(), buf, buf_size); } catch (...) { return OAKCOMMON_E_FAILED; } diff --git a/src/common/c_api/commandlineparser.cpp b/src/common/c_api/commandlineparser.cpp index 56b522646..6d255a06a 100644 --- a/src/common/c_api/commandlineparser.cpp +++ b/src/common/c_api/commandlineparser.cpp @@ -26,43 +26,79 @@ #include #include "../src/commandlineparser.h" +#include "refcounted.h" -struct OakCommonCommandLineParser { - CommandLineParser impl; -}; +namespace +{ -struct OakCommonCommandLineOption { +/** + * @brief State boxed behind an option handle's ctx pointer. + * + * The option pointer is borrowed: the option itself is owned by the + * parser, so releasing the box never destroys it. + */ +struct OptionState { CommandLineParser::Option *option; }; -struct OakCommonCommandLinePositionalArgument { +/** + * @brief State boxed behind a positional-argument handle's ctx pointer. + * + * The argument pointer is borrowed: the argument itself is owned by the + * parser, so releasing the box never destroys it. + */ +struct PositionalArgumentState { CommandLineParser::PositionalArgument *argument; }; -OakCommonCommandLineParser *oakcommon_commandlineparser_init(void) +CommandLineParser *clp(OakCommandLineParser parser) +{ + return oakcommon::handle_impl(parser.ctx); +} + +CommandLineParser::Option *clo(OakCommandLineOption option) +{ + OptionState *state = + oakcommon::handle_impl(option.ctx); + return state ? state->option : nullptr; +} + +CommandLineParser::PositionalArgument *clpa( + OakCommandLinePositionalArgument argument) +{ + PositionalArgumentState *state = + oakcommon::handle_impl(argument.ctx); + return state ? state->argument : nullptr; +} + +} // namespace + +OakCommandLineParser oakcommon_commandlineparser_init(void) { try { - return new (std::nothrow) OakCommonCommandLineParser(); + return oakcommon::make_handle_in_place(); } catch (...) { - return NULL; + OakCommandLineParser h = {}; + return h; } } -void oakcommon_commandlineparser_free(OakCommonCommandLineParser *parser) +void oakcommon_commandlineparser_free(OakCommandLineParser *parser) { - delete parser; + oakcommon::free_handle(parser); } -int oakcommon_commandlineparser_set_app_info(OakCommonCommandLineParser *parser, +int oakcommon_commandlineparser_set_app_info(OakCommandLineParser parser, const char *name, const char *version) { - if (!parser || !name) { + if (!clp(parser) || !name) { return OAKCOMMON_E_INVALID; } try { - parser->impl.set_app_info(name, version ? version : ""); + clp(parser)->set_app_info(name, version ? version : ""); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; @@ -70,11 +106,11 @@ int oakcommon_commandlineparser_set_app_info(OakCommonCommandLineParser *parser, } 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) { - if (!parser || !names || name_count <= 0) { + if (!clp(parser) || !names || name_count <= 0) { return OAKCOMMON_E_INVALID; } @@ -88,18 +124,16 @@ int oakcommon_commandlineparser_add_option( strings.emplace_back(names[i]); } - const CommandLineParser::Option *option = parser->impl.add_option( + const CommandLineParser::Option *option = clp(parser)->add_option( strings, description ? description : "", takes_arg != 0, arg_placeholder ? arg_placeholder : "", hidden != 0); if (out_option) { - auto *handle = - new (std::nothrow) OakCommonCommandLineOption(); - if (!handle) { + *out_option = oakcommon::make_handle( + OptionState{const_cast(option)}); + if (!out_option->ctx) { return OAKCOMMON_E_NOMEM; } - handle->option = const_cast(option); - *out_option = handle; } return OAKCOMMON_OK; @@ -109,28 +143,28 @@ int oakcommon_commandlineparser_add_option( } 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) { - if (!parser || !name) { + if (!clp(parser) || !name) { return OAKCOMMON_E_INVALID; } try { const CommandLineParser::PositionalArgument *argument = - parser->impl.add_positional_argument( + clp(parser)->add_positional_argument( name, description ? description : "", required != 0); if (out_argument) { - auto *handle = - new (std::nothrow) OakCommonCommandLinePositionalArgument(); - if (!handle) { + *out_argument = + oakcommon::make_handle( + PositionalArgumentState{ + const_cast( + argument)}); + if (!out_argument->ctx) { return OAKCOMMON_E_NOMEM; } - handle->argument = - const_cast(argument); - *out_argument = handle; } return OAKCOMMON_OK; @@ -139,10 +173,10 @@ int oakcommon_commandlineparser_add_positional_argument( } } -int oakcommon_commandlineparser_process(OakCommonCommandLineParser *parser, +int oakcommon_commandlineparser_process(OakCommandLineParser parser, const char *const *argv, int argc) { - if (!parser || !argv || argc < 0) { + if (!clp(parser) || !argv || argc < 0) { return OAKCOMMON_E_INVALID; } @@ -153,37 +187,37 @@ int oakcommon_commandlineparser_process(OakCommonCommandLineParser *parser, args.emplace_back(argv[i] ? argv[i] : ""); } - parser->impl.process(args); + clp(parser)->process(args); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_commandlineparser_print_help(OakCommonCommandLineParser *parser, +int oakcommon_commandlineparser_print_help(OakCommandLineParser parser, const char *filename) { - if (!parser || !filename) { + if (!clp(parser) || !filename) { return OAKCOMMON_E_INVALID; } try { - parser->impl.print_help(filename); + clp(parser)->print_help(filename); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_commandlineoption_is_set(OakCommonCommandLineOption *option, +int oakcommon_commandlineoption_is_set(OakCommandLineOption option, bool *is_set) { - if (!option || !option->option || !is_set) { + if (!clo(option) || !is_set) { return OAKCOMMON_E_INVALID; } try { - *is_set = option->option->is_set(); + *is_set = clo(option)->is_set(); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; @@ -212,29 +246,29 @@ static int copy_setting(const std::string &value, char *buf, int buf_size) return required; } -int oakcommon_commandlineoption_get_setting(OakCommonCommandLineOption *option, +int oakcommon_commandlineoption_get_setting(OakCommandLineOption option, char *buf, int buf_size) { - if (!option || !option->option) { + if (!clo(option)) { return OAKCOMMON_E_INVALID; } try { - return copy_setting(option->option->get_setting(), buf, buf_size); + return copy_setting(clo(option)->get_setting(), buf, buf_size); } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_commandlineoption_set_setting(OakCommonCommandLineOption *option, +int oakcommon_commandlineoption_set_setting(OakCommandLineOption option, const char *value) { - if (!option || !option->option || !value) { + if (!clo(option) || !value) { return OAKCOMMON_E_INVALID; } try { - option->option->set_setting(value); + clo(option)->set_setting(value); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; @@ -242,41 +276,41 @@ int oakcommon_commandlineoption_set_setting(OakCommonCommandLineOption *option, } int oakcommon_commandlinepositionalargument_get_setting( - OakCommonCommandLinePositionalArgument *argument, char *buf, int buf_size) + OakCommandLinePositionalArgument argument, char *buf, int buf_size) { - if (!argument || !argument->argument) { + if (!clpa(argument)) { return OAKCOMMON_E_INVALID; } try { - return copy_setting(argument->argument->get_setting(), buf, buf_size); + return copy_setting(clpa(argument)->get_setting(), buf, buf_size); } catch (...) { return OAKCOMMON_E_FAILED; } } int oakcommon_commandlinepositionalargument_set_setting( - OakCommonCommandLinePositionalArgument *argument, const char *value) + OakCommandLinePositionalArgument argument, const char *value) { - if (!argument || !argument->argument || !value) { + if (!clpa(argument) || !value) { return OAKCOMMON_E_INVALID; } try { - argument->argument->set_setting(value); + clpa(argument)->set_setting(value); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -void oakcommon_commandlineoption_free(OakCommonCommandLineOption *option) +void oakcommon_commandlineoption_free(OakCommandLineOption *option) { - delete option; + oakcommon::free_handle(option); } void oakcommon_commandlinepositionalargument_free( - OakCommonCommandLinePositionalArgument *argument) + OakCommandLinePositionalArgument *argument) { - delete argument; + oakcommon::free_handle(argument); } diff --git a/src/common/c_api/current.cpp b/src/common/c_api/current.cpp index deeb53ca2..080f06bfc 100644 --- a/src/common/c_api/current.cpp +++ b/src/common/c_api/current.cpp @@ -22,25 +22,47 @@ #include "../src/current.h" -struct OakCommonCurrent { - Current *current; -}; - -OakCommonCurrent *oakcommon_current_instance(void) +namespace { - static OakCommonCurrent handle = { &Current::get_instance() }; - return &handle; + +/** + * @brief No-op addref/release for the singleton: it is never destroyed. + */ +void singleton_noop(void *ctx) +{ + (void)ctx; } -void oakcommon_current_free(OakCommonCurrent *self) +/** + * @brief Recover the Current singleton from a handle (NULL-safe). + */ +Current *current_of(OakCurrent self) { - // No-op: the handle wraps a process-wide singleton. + return static_cast(self.ctx); +} + +} // namespace + +OakCurrent oakcommon_current_instance(void) +{ + OakCurrent h = {}; + h.ctx = &Current::get_instance(); + h.addref = &singleton_noop; + h.release = &singleton_noop; + h.abi_version = OAKCOMMON_ABI_VERSION; + return h; +} + +void oakcommon_current_free(OakCurrent *self) +{ + // No-op: the handle wraps a process-wide singleton whose release() + // intentionally never destroys anything. (void)self; } static int current_set(Current *current, void (Current::*set_fn)(std::shared_ptr), - void *obj, OakCommonDestroyFn destroy) + void *obj, OakDestroyFn destroy) { try { std::shared_ptr value; @@ -70,78 +92,78 @@ static int current_get(Current *current, return OAKCOMMON_OK; } -int oakcommon_current_set_video_params(OakCommonCurrent *self, void *obj, - OakCommonDestroyFn destroy) +int oakcommon_current_set_video_params(OakCurrent self, void *obj, + OakDestroyFn destroy) { - if (!self) + if (!current_of(self)) return OAKCOMMON_E_INVALID; - return current_set(self->current, &Current::set_current_video_params, + return current_set(current_of(self), &Current::set_current_video_params, obj, destroy); } -int oakcommon_current_set_audio_params(OakCommonCurrent *self, void *obj, - OakCommonDestroyFn destroy) +int oakcommon_current_set_audio_params(OakCurrent self, void *obj, + OakDestroyFn destroy) { - if (!self) + if (!current_of(self)) return OAKCOMMON_E_INVALID; - return current_set(self->current, &Current::set_current_audio_params, + return current_set(current_of(self), &Current::set_current_audio_params, obj, destroy); } -int oakcommon_current_set_plugin_host(OakCommonCurrent *self, void *obj, - OakCommonDestroyFn destroy) +int oakcommon_current_set_plugin_host(OakCurrent self, void *obj, + OakDestroyFn destroy) { - if (!self) + if (!current_of(self)) return OAKCOMMON_E_INVALID; - return current_set(self->current, &Current::set_plugin_host, obj, + return current_set(current_of(self), &Current::set_plugin_host, obj, destroy); } -int oakcommon_current_set_plugin_cache(OakCommonCurrent *self, void *obj, - OakCommonDestroyFn destroy) +int oakcommon_current_set_plugin_cache(OakCurrent self, void *obj, + OakDestroyFn destroy) { - if (!self) + if (!current_of(self)) return OAKCOMMON_E_INVALID; - return current_set(self->current, &Current::set_plugin_cache, obj, + return current_set(current_of(self), &Current::set_plugin_cache, obj, destroy); } -int oakcommon_current_get_video_params(OakCommonCurrent *self, void **out) +int oakcommon_current_get_video_params(OakCurrent self, void **out) { - if (!self || !out) + if (!current_of(self) || !out) return OAKCOMMON_E_INVALID; - return current_get(self->current, &Current::current_video_params, + return current_get(current_of(self), &Current::current_video_params, out); } -int oakcommon_current_get_audio_params(OakCommonCurrent *self, void **out) +int oakcommon_current_get_audio_params(OakCurrent self, void **out) { - if (!self || !out) + if (!current_of(self) || !out) return OAKCOMMON_E_INVALID; - return current_get(self->current, &Current::current_audio_params, + return current_get(current_of(self), &Current::current_audio_params, out); } -int oakcommon_current_get_plugin_host(OakCommonCurrent *self, void **out) +int oakcommon_current_get_plugin_host(OakCurrent self, void **out) { - if (!self || !out) + if (!current_of(self) || !out) return OAKCOMMON_E_INVALID; - return current_get(self->current, &Current::plugin_host, out); + return current_get(current_of(self), &Current::plugin_host, out); } -int oakcommon_current_get_plugin_cache(OakCommonCurrent *self, void **out) +int oakcommon_current_get_plugin_cache(OakCurrent self, void **out) { - if (!self || !out) + if (!current_of(self) || !out) return OAKCOMMON_E_INVALID; - return current_get(self->current, &Current::plugin_cache, out); + return current_get(current_of(self), &Current::plugin_cache, out); } -int oakcommon_current_is_interactive(OakCommonCurrent *self, int *out) +int oakcommon_current_is_interactive(OakCurrent self, int *out) { - if (!self || !out) + if (!current_of(self) || !out) return OAKCOMMON_E_INVALID; try { - *out = self->current->interactive() ? 1 : 0; + *out = current_of(self)->interactive() ? 1 : 0; } catch (...) { return OAKCOMMON_E_FAILED; } diff --git a/src/common/c_api/debug.cpp b/src/common/c_api/debug.cpp index a669ec195..76d758542 100644 --- a/src/common/c_api/debug.cpp +++ b/src/common/c_api/debug.cpp @@ -20,6 +20,9 @@ #include "common/debug.h" +#include +#include + #include "../src/debug.h" int oakcommon_debug_log(int level, const char *msg) @@ -43,3 +46,68 @@ int oakcommon_debug_level_name(int level, char *buf, int buf_size) return OAKCOMMON_E_FAILED; } } + +int oakcommon_log(int level, const char *fmt, ...) +{ + if (!fmt) + return OAKCOMMON_E_INVALID; + + va_list args; + va_start(args, fmt); + + va_list sizing; + va_copy(sizing, args); + int needed = vsnprintf(nullptr, 0, fmt, sizing); + va_end(sizing); + + if (needed < 0) { + va_end(args); + return OAKCOMMON_E_FAILED; + } + + std::string msg; + try { + // Dynamically sized: arbitrary message length, no truncation, + // no fixed stack buffer. + std::vector buf(static_cast(needed) + 1); + vsnprintf(buf.data(), buf.size(), fmt, args); + msg.assign(buf.data(), static_cast(needed)); + } catch (...) { + va_end(args); + return OAKCOMMON_E_FAILED; + } + va_end(args); + + try { + olive::log_message(level, msg); + } catch (...) { + return OAKCOMMON_E_FAILED; + } + return OAKCOMMON_OK; +} + +int oakcommon_log_set_level(int level) +{ + if (level < OAKCOMMON_DEBUG_DEBUG || level > OAKCOMMON_DEBUG_FATAL) + return OAKCOMMON_E_INVALID; + + try { + olive::set_log_level(static_cast(level)); + } catch (...) { + return OAKCOMMON_E_FAILED; + } + return OAKCOMMON_OK; +} + +int oakcommon_log_get_level(int *out_level) +{ + if (!out_level) + return OAKCOMMON_E_INVALID; + + try { + *out_level = static_cast(olive::get_log_level()); + } catch (...) { + return OAKCOMMON_E_FAILED; + } + return OAKCOMMON_OK; +} diff --git a/src/common/c_api/filefunctions.cpp b/src/common/c_api/filefunctions.cpp index a4a888ee3..942954193 100644 --- a/src/common/c_api/filefunctions.cpp +++ b/src/common/c_api/filefunctions.cpp @@ -24,11 +24,21 @@ #include #include "../src/filefunctions.h" +#include "refcounted.h" -struct OakCommonFileFunctions { - int unused; /**< Stateless family; handle kept for API uniformity. */ +namespace +{ + +/** + * @brief Stateless family; the boxed object is empty and only exists so + * the handle has something to reference-count. + */ +struct FileFunctionsState { + int unused; }; +} // namespace + namespace { @@ -54,25 +64,27 @@ bool is_valid_string_out(const char *buf, int buf_size) } // namespace -OakCommonFileFunctions *oakcommon_filefunctions_init(void) +OakFileFunctions oakcommon_filefunctions_init(void) { try { - return new OakCommonFileFunctions{0}; + return oakcommon::make_handle( + FileFunctionsState{0}); } catch (...) { - return nullptr; + OakFileFunctions h = {}; + return h; } } -void oakcommon_filefunctions_free(OakCommonFileFunctions *self) +void oakcommon_filefunctions_free(OakFileFunctions *self) { - delete self; + oakcommon::free_handle(self); } int oakcommon_filefunctions_get_unique_file_identifier( - OakCommonFileFunctions *self, const char *filename, char *buf, + OakFileFunctions self, const char *filename, char *buf, int buf_size) { - if (self == nullptr || filename == nullptr || + if (self.ctx == nullptr || filename == nullptr || !is_valid_string_out(buf, buf_size)) { return OAKCOMMON_E_INVALID; } @@ -87,9 +99,9 @@ int oakcommon_filefunctions_get_unique_file_identifier( } int oakcommon_filefunctions_get_configuration_location( - OakCommonFileFunctions *self, char *buf, int buf_size) + OakFileFunctions self, char *buf, int buf_size) { - if (self == nullptr || !is_valid_string_out(buf, buf_size)) { + if (self.ctx == nullptr || !is_valid_string_out(buf, buf_size)) { return OAKCOMMON_E_INVALID; } @@ -103,9 +115,9 @@ int oakcommon_filefunctions_get_configuration_location( } int oakcommon_filefunctions_get_application_path( - OakCommonFileFunctions *self, char *buf, int buf_size) + OakFileFunctions self, char *buf, int buf_size) { - if (self == nullptr || !is_valid_string_out(buf, buf_size)) { + if (self.ctx == nullptr || !is_valid_string_out(buf, buf_size)) { return OAKCOMMON_E_INVALID; } @@ -118,9 +130,9 @@ int oakcommon_filefunctions_get_application_path( } int oakcommon_filefunctions_get_temp_file_path( - OakCommonFileFunctions *self, char *buf, int buf_size) + OakFileFunctions self, char *buf, int buf_size) { - if (self == nullptr || !is_valid_string_out(buf, buf_size)) { + if (self.ctx == nullptr || !is_valid_string_out(buf, buf_size)) { return OAKCOMMON_E_INVALID; } @@ -133,9 +145,9 @@ int oakcommon_filefunctions_get_temp_file_path( } int oakcommon_filefunctions_get_auto_recovery_root( - OakCommonFileFunctions *self, char *buf, int buf_size) + OakFileFunctions self, char *buf, int buf_size) { - if (self == nullptr || !is_valid_string_out(buf, buf_size)) { + if (self.ctx == nullptr || !is_valid_string_out(buf, buf_size)) { return OAKCOMMON_E_INVALID; } @@ -148,10 +160,10 @@ int oakcommon_filefunctions_get_auto_recovery_root( } 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) { - if (self == nullptr || source == nullptr || dest == nullptr || + if (self.ctx == nullptr || source == nullptr || dest == nullptr || out == nullptr) { return OAKCOMMON_E_INVALID; } @@ -167,11 +179,11 @@ int oakcommon_filefunctions_can_copy_directory_without_overwriting( } } -int oakcommon_filefunctions_copy_directory(OakCommonFileFunctions *self, +int oakcommon_filefunctions_copy_directory(OakFileFunctions self, const char *source, const char *dest, int overwrite) { - if (self == nullptr || source == nullptr || dest == nullptr) { + if (self.ctx == nullptr || source == nullptr || dest == nullptr) { return OAKCOMMON_E_INVALID; } @@ -184,10 +196,10 @@ int oakcommon_filefunctions_copy_directory(OakCommonFileFunctions *self, } 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) { - if (self == nullptr || dir == nullptr || out == nullptr) { + if (self.ctx == nullptr || dir == nullptr || out == nullptr) { return OAKCOMMON_E_INVALID; } @@ -203,10 +215,10 @@ int oakcommon_filefunctions_directory_is_valid( } int oakcommon_filefunctions_ensure_filename_extension( - OakCommonFileFunctions *self, const char *filename, + OakFileFunctions self, const char *filename, const char *extension, char *buf, int buf_size) { - if (self == nullptr || filename == nullptr || extension == nullptr || + if (self.ctx == nullptr || filename == nullptr || extension == nullptr || !is_valid_string_out(buf, buf_size)) { return OAKCOMMON_E_INVALID; } @@ -222,10 +234,10 @@ int oakcommon_filefunctions_ensure_filename_extension( } int oakcommon_filefunctions_read_file_as_string( - OakCommonFileFunctions *self, const char *filename, char *buf, + OakFileFunctions self, const char *filename, char *buf, int buf_size) { - if (self == nullptr || filename == nullptr || + if (self.ctx == nullptr || filename == nullptr || !is_valid_string_out(buf, buf_size)) { return OAKCOMMON_E_INVALID; } @@ -240,10 +252,10 @@ int oakcommon_filefunctions_read_file_as_string( } int oakcommon_filefunctions_get_safe_temporary_filename( - OakCommonFileFunctions *self, const char *original, char *buf, + OakFileFunctions self, const char *original, char *buf, int buf_size) { - if (self == nullptr || original == nullptr || + if (self.ctx == nullptr || original == nullptr || !is_valid_string_out(buf, buf_size)) { return OAKCOMMON_E_INVALID; } @@ -258,10 +270,10 @@ int oakcommon_filefunctions_get_safe_temporary_filename( } 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) { - if (self == nullptr || from == nullptr || to == nullptr || + if (self.ctx == nullptr || from == nullptr || to == nullptr || out == nullptr) { return OAKCOMMON_E_INVALID; } @@ -276,10 +288,10 @@ int oakcommon_filefunctions_rename_file_allow_overwrite( } 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) { - if (self == nullptr || unformatted == nullptr || + if (self.ctx == nullptr || unformatted == nullptr || !is_valid_string_out(buf, buf_size)) { return OAKCOMMON_E_INVALID; } diff --git a/src/common/c_api/ocioutils.cpp b/src/common/c_api/ocioutils.cpp index a2a498411..5a312abd4 100644 --- a/src/common/c_api/ocioutils.cpp +++ b/src/common/c_api/ocioutils.cpp @@ -23,30 +23,42 @@ #include #include "../src/ocioutils.h" +#include "refcounted.h" -struct OakCommonOCIOUtils { - int unused; /**< Stateless; only the address matters. */ +namespace +{ + +/** + * @brief Stateless family; the boxed object is empty and only exists so + * the handle has something to reference-count. + */ +struct OCIOUtilsState { + int unused; }; -OakCommonOCIOUtils *oakcommon_ocioutils_init(void) +} // namespace + +OakOCIOUtils oakcommon_ocioutils_init(void) { try { - return new (std::nothrow) OakCommonOCIOUtils{}; + return oakcommon::make_handle( + OCIOUtilsState{0}); } catch (...) { - return NULL; + OakOCIOUtils h = {}; + return h; } } -void oakcommon_ocioutils_free(OakCommonOCIOUtils *self) +void oakcommon_ocioutils_free(OakOCIOUtils *self) { - delete self; + oakcommon::free_handle(self); } 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) { try { - if (self == NULL || out_bit_depth == NULL) + if (self.ctx == NULL || out_bit_depth == NULL) return OAKCOMMON_E_INVALID; if (pixel_format < OAKCOMMON_PIXEL_FORMAT_INVALID || pixel_format >= OAKCOMMON_PIXEL_FORMAT_COUNT) diff --git a/src/common/c_api/oiioutils.cpp b/src/common/c_api/oiioutils.cpp index bc27bda92..ffd3ee98f 100644 --- a/src/common/c_api/oiioutils.cpp +++ b/src/common/c_api/oiioutils.cpp @@ -23,30 +23,42 @@ #include #include "../src/oiioutils.h" +#include "refcounted.h" -struct OakCommonOIIOUtils { - int unused; /**< Stateless; only the address matters. */ +namespace +{ + +/** + * @brief Stateless family; the boxed object is empty and only exists so + * the handle has something to reference-count. + */ +struct OIIOUtilsState { + int unused; }; -OakCommonOIIOUtils *oakcommon_oiioutils_init(void) +} // namespace + +OakOIIOUtils oakcommon_oiioutils_init(void) { try { - return new (std::nothrow) OakCommonOIIOUtils{}; + return oakcommon::make_handle( + OIIOUtilsState{0}); } catch (...) { - return NULL; + OakOIIOUtils h = {}; + return h; } } -void oakcommon_oiioutils_free(OakCommonOIIOUtils *self) +void oakcommon_oiioutils_free(OakOIIOUtils *self) { - delete self; + oakcommon::free_handle(self); } 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) { try { - if (self == NULL || out_base_type == NULL) + if (self.ctx == NULL || out_base_type == NULL) return OAKCOMMON_E_INVALID; if (pixel_format < OAKCOMMON_PIXEL_FORMAT_INVALID || pixel_format >= OAKCOMMON_PIXEL_FORMAT_COUNT) @@ -63,10 +75,10 @@ int oakcommon_oiioutils_get_oiio_base_type_from_format( } 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) { try { - if (self == NULL || out_pixel_format == NULL) + if (self.ctx == NULL || out_pixel_format == NULL) return OAKCOMMON_E_INVALID; if (base_type < 0 || base_type >= OIIO::TypeDesc::LASTBASE) return OAKCOMMON_E_INVALID; @@ -82,11 +94,11 @@ int oakcommon_oiioutils_get_format_from_oiio_basetype( } 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) { try { - if (self == NULL || out_numerator == NULL || out_denominator == NULL) + if (self.ctx == NULL || out_numerator == NULL || out_denominator == NULL) return OAKCOMMON_E_INVALID; olive::core::Rational par = diff --git a/src/common/c_api/refcounted.h b/src/common/c_api/refcounted.h new file mode 100644 index 000000000..13005017f --- /dev/null +++ b/src/common/c_api/refcounted.h @@ -0,0 +1,132 @@ +/*** + + 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 OAKCOMMON_C_API_REFCOUNTED_H +#define OAKCOMMON_C_API_REFCOUNTED_H + +#include +#include +#include +#include + +#include "common/handle.h" + +namespace oakcommon +{ + +/** + * @brief Heap box behind every handle's ctx pointer. + * + * 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. + */ +template struct RefCounted { + T impl; + std::atomic refs; + + template + explicit RefCounted(Args &&...args) + : impl(std::forward(args)...) + , refs(1) + { + } +}; + +/** + * @brief Handle addref thunk: atomically increments the count. + */ +template void ref_counted_addref(void *ctx) +{ + auto *box = static_cast *>(ctx); + if (box) + box->refs.fetch_add(1, std::memory_order_relaxed); +} + +/** + * @brief Handle release thunk: decrements the count, destroys at zero. + */ +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; +} + +/** + * @brief Build a by-value handle owning a freshly boxed object (count 1). + * + * The object is constructed in place inside the box, so non-movable + * types are supported. On allocation failure the returned handle has + * ctx == NULL (all C API functions treat that as OAKCOMMON_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)...); + } catch (...) { + h.ctx = nullptr; + } + h.addref = &ref_counted_addref; + h.release = &ref_counted_release; + h.abi_version = OAKCOMMON_ABI_VERSION; + return h; +} + +/** + * @brief Build a by-value handle from an existing object (copied/moved + * into the box, reference count 1). + */ +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. + * + * Clears ctx afterwards so a double free through the same (copied) + * struct is caught by the caller's own bookkeeping, not by us. + */ +template void free_handle(Handle *h) +{ + if (!h || !h->ctx || !h->release) + return; + h->release(h->ctx); + h->ctx = nullptr; +} + +} // namespace oakcommon + +#endif // OAKCOMMON_C_API_REFCOUNTED_H diff --git a/src/common/c_api/subtitleparams.cpp b/src/common/c_api/subtitleparams.cpp index df9f77fb6..706122a6d 100644 --- a/src/common/c_api/subtitleparams.cpp +++ b/src/common/c_api/subtitleparams.cpp @@ -23,14 +23,19 @@ #include #include "../src/subtitleparams.h" - -struct OakCommonSubtitleParams { - olive::SubtitleParams impl; -}; +#include "refcounted.h" namespace { +/** + * @brief Recover the boxed olive::SubtitleParams from a handle (NULL-safe). + */ +olive::SubtitleParams *sp(OakSubtitleParams params) +{ + return oakcommon::handle_impl(params.ctx); +} + int copy_string(const std::string &value, char *buf, int buf_size) { int needed = (int)value.size() + 1; @@ -41,92 +46,110 @@ int copy_string(const std::string &value, char *buf, int buf_size) } // namespace -OakCommonSubtitleParams *oakcommon_subtitleparams_init(void) +OakSubtitleParams oakcommon_subtitleparams_init(void) { try { - return new OakCommonSubtitleParams{olive::SubtitleParams()}; + return oakcommon::make_handle( + olive::SubtitleParams()); } catch (...) { - return nullptr; + OakSubtitleParams h = {}; + return h; } } -void oakcommon_subtitleparams_free(OakCommonSubtitleParams *params) +OakSubtitleParams oakcommon_subtitleparams_init_from_native( + const olive::SubtitleParams *src) { - delete params; + if (!src) { + OakSubtitleParams h = {}; + return h; + } + try { + return oakcommon::make_handle( + olive::SubtitleParams(*src)); + } catch (...) { + OakSubtitleParams h = {}; + return h; + } +} + +void oakcommon_subtitleparams_free(OakSubtitleParams *params) +{ + oakcommon::free_handle(params); } int oakcommon_subtitleparams_get_stream_index( - OakCommonSubtitleParams *params, int *index) + OakSubtitleParams params, int *index) { - if (!params || !index) + if (!sp(params) || !index) return OAKCOMMON_E_INVALID; - *index = params->impl.stream_index(); + *index = sp(params)->stream_index(); return OAKCOMMON_OK; } int oakcommon_subtitleparams_set_stream_index( - OakCommonSubtitleParams *params, int index) + OakSubtitleParams params, int index) { - if (!params) + if (!sp(params)) return OAKCOMMON_E_INVALID; - params->impl.set_stream_index(index); + sp(params)->set_stream_index(index); return OAKCOMMON_OK; } -int oakcommon_subtitleparams_get_enabled(OakCommonSubtitleParams *params, +int oakcommon_subtitleparams_get_enabled(OakSubtitleParams params, int *enabled) { - if (!params || !enabled) + if (!sp(params) || !enabled) return OAKCOMMON_E_INVALID; - *enabled = params->impl.enabled() ? 1 : 0; + *enabled = sp(params)->enabled() ? 1 : 0; return OAKCOMMON_OK; } -int oakcommon_subtitleparams_set_enabled(OakCommonSubtitleParams *params, +int oakcommon_subtitleparams_set_enabled(OakSubtitleParams params, int enabled) { - if (!params) + if (!sp(params)) return OAKCOMMON_E_INVALID; - params->impl.set_enabled(enabled != 0); + sp(params)->set_enabled(enabled != 0); return OAKCOMMON_OK; } -int oakcommon_subtitleparams_is_valid(OakCommonSubtitleParams *params, +int oakcommon_subtitleparams_is_valid(OakSubtitleParams params, int *is_valid) { - if (!params || !is_valid) + if (!sp(params) || !is_valid) return OAKCOMMON_E_INVALID; - *is_valid = params->impl.is_valid() ? 1 : 0; + *is_valid = sp(params)->is_valid() ? 1 : 0; return OAKCOMMON_OK; } -int oakcommon_subtitleparams_count(OakCommonSubtitleParams *params, int *count) +int oakcommon_subtitleparams_count(OakSubtitleParams params, int *count) { - if (!params || !count) + if (!sp(params) || !count) return OAKCOMMON_E_INVALID; - *count = (int)params->impl.size(); + *count = (int)sp(params)->size(); return OAKCOMMON_OK; } -int oakcommon_subtitleparams_duration(OakCommonSubtitleParams *params, +int oakcommon_subtitleparams_duration(OakSubtitleParams params, int *numerator, int *denominator) { - if (!params || !numerator || !denominator) + if (!sp(params) || !numerator || !denominator) return OAKCOMMON_E_INVALID; - olive::core::Rational d = params->impl.duration(); + olive::core::Rational d = sp(params)->duration(); *numerator = d.numerator(); *denominator = d.denominator(); return OAKCOMMON_OK; } -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) { - if (!params || !text) + if (!sp(params) || !text) return OAKCOMMON_E_INVALID; try { - params->impl.push_back(olive::Subtitle( + sp(params)->push_back(olive::Subtitle( olive::core::TimeRange(olive::core::Rational(in_num, in_den), olive::core::Rational(out_num, out_den)), text)); @@ -136,23 +159,23 @@ int oakcommon_subtitleparams_add_subtitle(OakCommonSubtitleParams *params, } } -int oakcommon_subtitleparams_clear(OakCommonSubtitleParams *params) +int oakcommon_subtitleparams_clear(OakSubtitleParams params) { - if (!params) + if (!sp(params)) return OAKCOMMON_E_INVALID; - params->impl.clear(); + sp(params)->clear(); return OAKCOMMON_OK; } -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) { - if (!params || !in_num || !in_den || !out_num || !out_den) + if (!sp(params) || !in_num || !in_den || !out_num || !out_den) return OAKCOMMON_E_INVALID; - if (index < 0 || index >= (int)params->impl.size()) + if (index < 0 || index >= (int)sp(params)->size()) return OAKCOMMON_E_NOT_FOUND; - const olive::Subtitle &s = params->impl.at(index); + const olive::Subtitle &s = sp(params)->at(index); olive::core::Rational in = s.time().in(); olive::core::Rational out = s.time().out(); *in_num = in.numerator(); @@ -162,16 +185,16 @@ int oakcommon_subtitleparams_get_subtitle(OakCommonSubtitleParams *params, return OAKCOMMON_OK; } -int oakcommon_subtitleparams_get_subtitle_text(OakCommonSubtitleParams *params, +int oakcommon_subtitleparams_get_subtitle_text(OakSubtitleParams params, int index, char *buf, int buf_size) { - if (!params) + if (!sp(params)) return OAKCOMMON_E_INVALID; - if (index < 0 || index >= (int)params->impl.size()) + if (index < 0 || index >= (int)sp(params)->size()) return OAKCOMMON_E_NOT_FOUND; try { - return copy_string(params->impl.at(index).text(), buf, buf_size); + return copy_string(sp(params)->at(index).text(), buf, buf_size); } catch (...) { return OAKCOMMON_E_FAILED; } @@ -187,10 +210,10 @@ int oakcommon_subtitleparams_generate_ass_header(char *buf, int buf_size) } } -int oakcommon_subtitleparams_load_xml(OakCommonSubtitleParams *params, +int oakcommon_subtitleparams_load_xml(OakSubtitleParams params, const char *xml) { - if (!params || !xml) + if (!sp(params) || !xml) return OAKCOMMON_E_INVALID; try { olive::XmlStreamReader reader(xml); @@ -199,22 +222,22 @@ int oakcommon_subtitleparams_load_xml(OakCommonSubtitleParams *params, // Position on the root element; load() consumes its children. if (!olive::xml_read_next_start_element(&reader)) return OAKCOMMON_E_FAILED; - params->impl.load(&reader); + sp(params)->load(&reader); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_subtitleparams_save_xml(OakCommonSubtitleParams *params, +int oakcommon_subtitleparams_save_xml(OakSubtitleParams params, char *buf, int buf_size) { - if (!params) + if (!sp(params)) return OAKCOMMON_E_INVALID; try { olive::XmlStreamWriter writer; writer.write_start_element("subtitleparams"); - params->impl.save(&writer); + sp(params)->save(&writer); writer.write_end_element(); return copy_string(writer.output(), buf, buf_size); } catch (...) { diff --git a/src/common/c_api/videoparams.cpp b/src/common/c_api/videoparams.cpp index 31b9cea12..bc58f8f1c 100644 --- a/src/common/c_api/videoparams.cpp +++ b/src/common/c_api/videoparams.cpp @@ -23,10 +23,20 @@ #include #include "../src/videoparams.h" +#include "refcounted.h" -struct OakCommonVideoParams { - olive::VideoParams impl; -}; +namespace +{ + +/** + * @brief Recover the boxed olive::VideoParams from a handle (NULL-safe). + */ +olive::VideoParams *vp(OakVideoParams params) +{ + return oakcommon::handle_impl(params.ctx); +} + +} // namespace namespace { @@ -51,317 +61,345 @@ int get_rational(const olive::core::Rational &r, int *numerator, } // namespace -OakCommonVideoParams *oakcommon_videoparams_init(void) +OakVideoParams oakcommon_videoparams_init(void) { try { - return new OakCommonVideoParams{olive::VideoParams()}; + return oakcommon::make_handle( + olive::VideoParams()); } catch (...) { - return nullptr; + OakVideoParams h = {}; + return h; } } -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) { try { - return new OakCommonVideoParams{olive::VideoParams( - width, height, - static_cast(pixel_format), - nb_channels, - olive::core::Rational(pixel_aspect_num, pixel_aspect_den), - static_cast(interlacing), - divider)}; + return oakcommon::make_handle( + olive::VideoParams( + width, height, + static_cast(pixel_format), + nb_channels, + olive::core::Rational(pixel_aspect_num, pixel_aspect_den), + static_cast(interlacing), + divider)); } catch (...) { - return nullptr; + OakVideoParams h = {}; + return h; } } -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) { try { - return new OakCommonVideoParams{olive::VideoParams( - width, height, - olive::core::Rational(time_base_num, time_base_den), - static_cast(pixel_format), - nb_channels, - olive::core::Rational(pixel_aspect_num, pixel_aspect_den), - static_cast(interlacing), - divider)}; + return oakcommon::make_handle( + olive::VideoParams( + width, height, + olive::core::Rational(time_base_num, time_base_den), + static_cast(pixel_format), + nb_channels, + olive::core::Rational(pixel_aspect_num, pixel_aspect_den), + static_cast(interlacing), + divider)); } catch (...) { - return nullptr; + OakVideoParams h = {}; + return h; } } -void oakcommon_videoparams_free(OakCommonVideoParams *params) +OakVideoParams oakcommon_videoparams_init_from_native( + const olive::VideoParams *src) { - delete params; + if (!src) { + OakVideoParams h = {}; + return h; + } + try { + return oakcommon::make_handle( + olive::VideoParams(*src)); + } catch (...) { + OakVideoParams h = {}; + return h; + } +} + +const olive::VideoParams *oakcommon_videoparams_get_native( + OakVideoParams params) +{ + return vp(params); +} + +void oakcommon_videoparams_free(OakVideoParams *params) +{ + oakcommon::free_handle(params); } #define OAKCOMMON_VIDEOPARAMS_INT_GETTER(name, expr) \ - int oakcommon_videoparams_get_##name(OakCommonVideoParams *params, \ + int oakcommon_videoparams_get_##name(OakVideoParams params, \ int *out) \ { \ - if (!params || !out) \ + if (!vp(params) || !out) \ return OAKCOMMON_E_INVALID; \ *out = (expr); \ return OAKCOMMON_OK; \ } #define OAKCOMMON_VIDEOPARAMS_INT_SETTER(name, stmt) \ - int oakcommon_videoparams_set_##name(OakCommonVideoParams *params, \ + int oakcommon_videoparams_set_##name(OakVideoParams params, \ int value) \ { \ - if (!params) \ + if (!vp(params)) \ return OAKCOMMON_E_INVALID; \ stmt; \ return OAKCOMMON_OK; \ } -OAKCOMMON_VIDEOPARAMS_INT_GETTER(width, params->impl.width()) -OAKCOMMON_VIDEOPARAMS_INT_SETTER(width, params->impl.set_width(value)) -OAKCOMMON_VIDEOPARAMS_INT_GETTER(height, params->impl.height()) -OAKCOMMON_VIDEOPARAMS_INT_SETTER(height, params->impl.set_height(value)) -OAKCOMMON_VIDEOPARAMS_INT_GETTER(depth, params->impl.depth()) -OAKCOMMON_VIDEOPARAMS_INT_SETTER(depth, params->impl.set_depth(value)) -OAKCOMMON_VIDEOPARAMS_INT_GETTER(is_3d, params->impl.is_3d() ? 1 : 0) -OAKCOMMON_VIDEOPARAMS_INT_GETTER(format, static_cast(params->impl.format())) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(width, vp(params)->width()) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(width, vp(params)->set_width(value)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(height, vp(params)->height()) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(height, vp(params)->set_height(value)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(depth, vp(params)->depth()) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(depth, vp(params)->set_depth(value)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(is_3d, vp(params)->is_3d() ? 1 : 0) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(format, static_cast(vp(params)->format())) OAKCOMMON_VIDEOPARAMS_INT_SETTER( - format, params->impl.set_format( + format, vp(params)->set_format( static_cast(value))) -OAKCOMMON_VIDEOPARAMS_INT_GETTER(channel_count, params->impl.channel_count()) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(channel_count, vp(params)->channel_count()) OAKCOMMON_VIDEOPARAMS_INT_SETTER(channel_count, - params->impl.set_channel_count(value)) + vp(params)->set_channel_count(value)) OAKCOMMON_VIDEOPARAMS_INT_GETTER(interlacing, - static_cast(params->impl.interlacing())) + static_cast(vp(params)->interlacing())) OAKCOMMON_VIDEOPARAMS_INT_SETTER( interlacing, - params->impl.set_interlacing( + vp(params)->set_interlacing( static_cast(value))) -OAKCOMMON_VIDEOPARAMS_INT_GETTER(divider, params->impl.divider()) -OAKCOMMON_VIDEOPARAMS_INT_SETTER(divider, params->impl.set_divider(value)) -OAKCOMMON_VIDEOPARAMS_INT_GETTER(enabled, params->impl.enabled() ? 1 : 0) -OAKCOMMON_VIDEOPARAMS_INT_SETTER(enabled, params->impl.set_enabled(value != 0)) -OAKCOMMON_VIDEOPARAMS_INT_GETTER(stream_index, params->impl.stream_index()) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(divider, vp(params)->divider()) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(divider, vp(params)->set_divider(value)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(enabled, vp(params)->enabled() ? 1 : 0) +OAKCOMMON_VIDEOPARAMS_INT_SETTER(enabled, vp(params)->set_enabled(value != 0)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(stream_index, vp(params)->stream_index()) OAKCOMMON_VIDEOPARAMS_INT_SETTER(stream_index, - params->impl.set_stream_index(value)) + vp(params)->set_stream_index(value)) OAKCOMMON_VIDEOPARAMS_INT_GETTER(video_type, - static_cast(params->impl.video_type())) + static_cast(vp(params)->video_type())) OAKCOMMON_VIDEOPARAMS_INT_SETTER( video_type, - params->impl.set_video_type(static_cast(value))) + vp(params)->set_video_type(static_cast(value))) OAKCOMMON_VIDEOPARAMS_INT_GETTER(premultiplied_alpha, - params->impl.premultiplied_alpha() ? 1 : 0) + vp(params)->premultiplied_alpha() ? 1 : 0) OAKCOMMON_VIDEOPARAMS_INT_SETTER(premultiplied_alpha, - params->impl.set_premultiplied_alpha( + vp(params)->set_premultiplied_alpha( value != 0)) OAKCOMMON_VIDEOPARAMS_INT_GETTER(color_range, - static_cast(params->impl.color_range())) + static_cast(vp(params)->color_range())) OAKCOMMON_VIDEOPARAMS_INT_SETTER( - color_range, params->impl.set_color_range( + color_range, vp(params)->set_color_range( static_cast(value))) OAKCOMMON_VIDEOPARAMS_INT_GETTER(color_primaries, - params->impl.color_primaries()) + vp(params)->color_primaries()) OAKCOMMON_VIDEOPARAMS_INT_SETTER(color_primaries, - params->impl.set_color_primaries(value)) -OAKCOMMON_VIDEOPARAMS_INT_GETTER(color_transfer, params->impl.color_transfer()) + vp(params)->set_color_primaries(value)) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(color_transfer, vp(params)->color_transfer()) OAKCOMMON_VIDEOPARAMS_INT_SETTER(color_transfer, - params->impl.set_color_transfer(value)) + vp(params)->set_color_transfer(value)) OAKCOMMON_VIDEOPARAMS_INT_GETTER(square_pixel_width, - params->impl.square_pixel_width()) + vp(params)->square_pixel_width()) OAKCOMMON_VIDEOPARAMS_INT_GETTER(effective_width, - params->impl.effective_width()) + vp(params)->effective_width()) OAKCOMMON_VIDEOPARAMS_INT_GETTER(effective_height, - params->impl.effective_height()) + vp(params)->effective_height()) OAKCOMMON_VIDEOPARAMS_INT_GETTER(effective_depth, - params->impl.effective_depth()) -OAKCOMMON_VIDEOPARAMS_INT_GETTER(is_valid, params->impl.is_valid() ? 1 : 0) + vp(params)->effective_depth()) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(is_valid, vp(params)->is_valid() ? 1 : 0) OAKCOMMON_VIDEOPARAMS_INT_GETTER(bytes_per_channel, - params->impl.get_bytes_per_channel()) + vp(params)->get_bytes_per_channel()) OAKCOMMON_VIDEOPARAMS_INT_GETTER(bytes_per_pixel, - params->impl.get_bytes_per_pixel()) -OAKCOMMON_VIDEOPARAMS_INT_GETTER(buffer_size, params->impl.get_buffer_size()) + vp(params)->get_bytes_per_pixel()) +OAKCOMMON_VIDEOPARAMS_INT_GETTER(buffer_size, vp(params)->get_buffer_size()) -int oakcommon_videoparams_get_x(OakCommonVideoParams *params, float *x) +int oakcommon_videoparams_get_x(OakVideoParams params, float *x) { - if (!params || !x) + if (!vp(params) || !x) return OAKCOMMON_E_INVALID; - *x = params->impl.x(); + *x = vp(params)->x(); return OAKCOMMON_OK; } -int oakcommon_videoparams_set_x(OakCommonVideoParams *params, float x) +int oakcommon_videoparams_set_x(OakVideoParams params, float x) { - if (!params) + if (!vp(params)) return OAKCOMMON_E_INVALID; - params->impl.set_x(x); + vp(params)->set_x(x); return OAKCOMMON_OK; } -int oakcommon_videoparams_get_y(OakCommonVideoParams *params, float *y) +int oakcommon_videoparams_get_y(OakVideoParams params, float *y) { - if (!params || !y) + if (!vp(params) || !y) return OAKCOMMON_E_INVALID; - *y = params->impl.y(); + *y = vp(params)->y(); return OAKCOMMON_OK; } -int oakcommon_videoparams_set_y(OakCommonVideoParams *params, float y) +int oakcommon_videoparams_set_y(OakVideoParams params, float y) { - if (!params) + if (!vp(params)) return OAKCOMMON_E_INVALID; - params->impl.set_y(y); + vp(params)->set_y(y); return OAKCOMMON_OK; } -int oakcommon_videoparams_get_start_time(OakCommonVideoParams *params, +int oakcommon_videoparams_get_start_time(OakVideoParams params, int64_t *start_time) { - if (!params || !start_time) + if (!vp(params) || !start_time) return OAKCOMMON_E_INVALID; - *start_time = params->impl.start_time(); + *start_time = vp(params)->start_time(); return OAKCOMMON_OK; } -int oakcommon_videoparams_set_start_time(OakCommonVideoParams *params, +int oakcommon_videoparams_set_start_time(OakVideoParams params, int64_t start_time) { - if (!params) + if (!vp(params)) return OAKCOMMON_E_INVALID; - params->impl.set_start_time(start_time); + vp(params)->set_start_time(start_time); return OAKCOMMON_OK; } -int oakcommon_videoparams_get_duration(OakCommonVideoParams *params, +int oakcommon_videoparams_get_duration(OakVideoParams params, int64_t *duration) { - if (!params || !duration) + if (!vp(params) || !duration) return OAKCOMMON_E_INVALID; - *duration = params->impl.duration(); + *duration = vp(params)->duration(); return OAKCOMMON_OK; } -int oakcommon_videoparams_set_duration(OakCommonVideoParams *params, +int oakcommon_videoparams_set_duration(OakVideoParams params, int64_t duration) { - if (!params) + if (!vp(params)) return OAKCOMMON_E_INVALID; - params->impl.set_duration(duration); + vp(params)->set_duration(duration); return OAKCOMMON_OK; } -int oakcommon_videoparams_get_time_base(OakCommonVideoParams *params, +int oakcommon_videoparams_get_time_base(OakVideoParams params, int *numerator, int *denominator) { - if (!params) + if (!vp(params)) return OAKCOMMON_E_INVALID; - return get_rational(params->impl.time_base(), numerator, denominator); + return get_rational(vp(params)->time_base(), numerator, denominator); } -int oakcommon_videoparams_set_time_base(OakCommonVideoParams *params, +int oakcommon_videoparams_set_time_base(OakVideoParams params, int numerator, int denominator) { - if (!params) + if (!vp(params)) return OAKCOMMON_E_INVALID; - params->impl.set_time_base(olive::core::Rational(numerator, denominator)); + vp(params)->set_time_base(olive::core::Rational(numerator, denominator)); return OAKCOMMON_OK; } -int oakcommon_videoparams_get_frame_rate(OakCommonVideoParams *params, +int oakcommon_videoparams_get_frame_rate(OakVideoParams params, int *numerator, int *denominator) { - if (!params) + if (!vp(params)) return OAKCOMMON_E_INVALID; - return get_rational(params->impl.frame_rate(), numerator, denominator); + return get_rational(vp(params)->frame_rate(), numerator, denominator); } -int oakcommon_videoparams_set_frame_rate(OakCommonVideoParams *params, +int oakcommon_videoparams_set_frame_rate(OakVideoParams params, int numerator, int denominator) { - if (!params) + if (!vp(params)) return OAKCOMMON_E_INVALID; - params->impl.set_frame_rate(olive::core::Rational(numerator, denominator)); + vp(params)->set_frame_rate(olive::core::Rational(numerator, denominator)); return OAKCOMMON_OK; } -int oakcommon_videoparams_frame_rate_as_time_base(OakCommonVideoParams *params, +int oakcommon_videoparams_frame_rate_as_time_base(OakVideoParams params, int *numerator, int *denominator) { - if (!params) + if (!vp(params)) return OAKCOMMON_E_INVALID; - return get_rational(params->impl.frame_rate_as_time_base(), numerator, + return get_rational(vp(params)->frame_rate_as_time_base(), numerator, denominator); } -int oakcommon_videoparams_get_pixel_aspect_ratio(OakCommonVideoParams *params, +int oakcommon_videoparams_get_pixel_aspect_ratio(OakVideoParams params, int *numerator, int *denominator) { - if (!params) + if (!vp(params)) return OAKCOMMON_E_INVALID; - return get_rational(params->impl.pixel_aspect_ratio(), numerator, + return get_rational(vp(params)->pixel_aspect_ratio(), numerator, denominator); } -int oakcommon_videoparams_set_pixel_aspect_ratio(OakCommonVideoParams *params, +int oakcommon_videoparams_set_pixel_aspect_ratio(OakVideoParams params, int numerator, int denominator) { - if (!params) + if (!vp(params)) return OAKCOMMON_E_INVALID; - params->impl.set_pixel_aspect_ratio( + vp(params)->set_pixel_aspect_ratio( olive::core::Rational(numerator, denominator)); return OAKCOMMON_OK; } -int oakcommon_videoparams_get_colorspace(OakCommonVideoParams *params, +int oakcommon_videoparams_get_colorspace(OakVideoParams params, char *buf, int buf_size) { - if (!params) + if (!vp(params)) return OAKCOMMON_E_INVALID; try { - return copy_string(params->impl.colorspace(), buf, buf_size); + return copy_string(vp(params)->colorspace(), buf, buf_size); } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_videoparams_set_colorspace(OakCommonVideoParams *params, +int oakcommon_videoparams_set_colorspace(OakVideoParams params, const char *colorspace) { - if (!params || !colorspace) + if (!vp(params) || !colorspace) return OAKCOMMON_E_INVALID; - params->impl.set_colorspace(colorspace); + vp(params)->set_colorspace(colorspace); return OAKCOMMON_OK; } 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) { - if (!params || !timestamp) + if (!vp(params) || !timestamp) return OAKCOMMON_E_INVALID; - *timestamp = params->impl.get_time_in_timebase_units( + *timestamp = vp(params)->get_time_in_timebase_units( olive::core::Rational(time_num, time_den)); return OAKCOMMON_OK; } -int oakcommon_videoparams_equals(OakCommonVideoParams *params, - OakCommonVideoParams *other, int *equal) +int oakcommon_videoparams_equals(OakVideoParams params, + OakVideoParams other, int *equal) { - if (!params || !other || !equal) + if (!vp(params) || !vp(other) || !equal) return OAKCOMMON_E_INVALID; - *equal = (params->impl == other->impl) ? 1 : 0; + *equal = (*vp(params) == *vp(other)) ? 1 : 0; return OAKCOMMON_OK; } -int oakcommon_videoparams_load_xml(OakCommonVideoParams *params, +int oakcommon_videoparams_load_xml(OakVideoParams params, const char *xml) { - if (!params || !xml) + if (!vp(params) || !xml) return OAKCOMMON_E_INVALID; try { olive::XmlStreamReader reader(xml); @@ -370,22 +408,22 @@ int oakcommon_videoparams_load_xml(OakCommonVideoParams *params, // Position on the root element; load() consumes its children. if (!olive::xml_read_next_start_element(&reader)) return OAKCOMMON_E_FAILED; - params->impl.load(&reader); + vp(params)->load(&reader); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_videoparams_save_xml(OakCommonVideoParams *params, char *buf, +int oakcommon_videoparams_save_xml(OakVideoParams params, char *buf, int buf_size) { - if (!params) + if (!vp(params)) return OAKCOMMON_E_INVALID; try { olive::XmlStreamWriter writer; writer.write_start_element("videoparams"); - params->impl.save(&writer); + vp(params)->save(&writer); writer.write_end_element(); return copy_string(writer.output(), buf, buf_size); } catch (...) { @@ -476,3 +514,33 @@ int oakcommon_videoparams_frame_rate_to_string(int numerator, int denominator, return OAKCOMMON_E_FAILED; } } + +static olive::PixelFormat convert_to_olive_format(OakPixelFormat format) +{ + switch (format) { + case OAKCOMMON_PIXEL_FORMAT_INVALID: + return olive::PixelFormat::invalid; + case OAKCOMMON_PIXEL_FORMAT_COUNT: + return olive::PixelFormat::count; + case OAKCOMMON_PIXEL_FORMAT_U8: + return olive::PixelFormat::u8; + case OAKCOMMON_PIXEL_FORMAT_U10: + return olive::PixelFormat::u10; + case OAKCOMMON_PIXEL_FORMAT_U16: + return olive::PixelFormat::u16; + case OAKCOMMON_PIXEL_FORMAT_F16: + return olive::PixelFormat::f16; + case OAKCOMMON_PIXEL_FORMAT_F32: + return olive::PixelFormat::f32; + } + return olive::PixelFormat::invalid; +} +int oakcommon_videoparams_static_get_bytes_per_channel(OakPixelFormat format) +{ + return olive::VideoParams::get_bytes_per_channel(convert_to_olive_format(format)); +} + +int oakcommon_videoparams_static_get_bytes_per_pixel(OakPixelFormat format, int channels) +{ + return olive::VideoParams::get_bytes_per_pixel(convert_to_olive_format(format), channels); +} diff --git a/src/common/c_api/xmlutils.cpp b/src/common/c_api/xmlutils.cpp index be9a84390..999e59aac 100644 --- a/src/common/c_api/xmlutils.cpp +++ b/src/common/c_api/xmlutils.cpp @@ -24,24 +24,40 @@ #include #include "../src/xmlutils.h" +#include "refcounted.h" -struct OakCommonXmlReader { +namespace +{ + +/** + * @brief Reader state boxed behind the handle's ctx pointer. + */ +struct XmlReaderState { olive::XmlStreamReader reader; std::string cached_text; bool has_cached_text = false; - explicit OakCommonXmlReader(const char *data) + explicit XmlReaderState(const char *data) : reader(data) { } }; -struct OakCommonXmlWriter { - olive::XmlStreamWriter writer; -}; - -namespace +/** + * @brief Recover the boxed reader state from a handle (NULL-safe). + */ +XmlReaderState *xr(OakXmlReader reader) { + return oakcommon::handle_impl(reader.ctx); +} + +/** + * @brief Recover the boxed writer from a handle (NULL-safe). + */ +olive::XmlStreamWriter *xw(OakXmlWriter writer) +{ + return oakcommon::handle_impl(writer.ctx); +} /** * @brief Copy @p value into the two-stage string buffer. @@ -62,99 +78,102 @@ int copy_string(const std::string &value, char *buf, int buf_size) extern "C" { -OakCommonXmlReader *oakcommon_xml_reader_init(const char *data) +OakXmlReader oakcommon_xml_reader_init(const char *data) { + OakXmlReader h = {}; if (!data) - return nullptr; + return h; try { - return new (std::nothrow) OakCommonXmlReader(data); + return oakcommon::make_handle( + XmlReaderState(data)); } catch (...) { - return nullptr; + OakXmlReader empty = {}; + return empty; } } -void oakcommon_xml_reader_free(OakCommonXmlReader *reader) +void oakcommon_xml_reader_free(OakXmlReader *reader) { - delete reader; + oakcommon::free_handle(reader); } -int oakcommon_xml_reader_read_next_start_element(OakCommonXmlReader *reader, +int oakcommon_xml_reader_read_next_start_element(OakXmlReader reader, int *found) { - if (!reader || !found) + if (!xr(reader) || !found) return OAKCOMMON_E_INVALID; try { - reader->has_cached_text = false; - *found = olive::xml_read_next_start_element(&reader->reader) ? 1 : 0; + xr(reader)->has_cached_text = false; + *found = olive::xml_read_next_start_element(&xr(reader)->reader) ? 1 : 0; return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_xml_reader_name(OakCommonXmlReader *reader, char *buf, +int oakcommon_xml_reader_name(OakXmlReader reader, char *buf, int buf_size) { - if (!reader) + if (!xr(reader)) return OAKCOMMON_E_INVALID; try { - return copy_string(reader->reader.name(), buf, buf_size); + return copy_string(xr(reader)->reader.name(), buf, buf_size); } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_xml_reader_read_element_text(OakCommonXmlReader *reader, +int oakcommon_xml_reader_read_element_text(OakXmlReader reader, char *buf, int buf_size) { - if (!reader) + if (!xr(reader)) return OAKCOMMON_E_INVALID; try { // read_element_text() consumes the stream, so cache the result to // keep the two-stage (size query then copy) buffer convention working. - if (!reader->has_cached_text) { - reader->cached_text = reader->reader.read_element_text(); - reader->has_cached_text = true; + if (!xr(reader)->has_cached_text) { + xr(reader)->cached_text = xr(reader)->reader.read_element_text(); + xr(reader)->has_cached_text = true; } - return copy_string(reader->cached_text, buf, buf_size); + return copy_string(xr(reader)->cached_text, buf, buf_size); } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_xml_reader_skip_current_element(OakCommonXmlReader *reader) +int oakcommon_xml_reader_skip_current_element(OakXmlReader reader) { - if (!reader) + if (!xr(reader)) return OAKCOMMON_E_INVALID; try { - reader->has_cached_text = false; - reader->reader.skip_current_element(); + xr(reader)->has_cached_text = false; + xr(reader)->reader.skip_current_element(); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_xml_reader_attribute_count(OakCommonXmlReader *reader, +int oakcommon_xml_reader_attribute_count(OakXmlReader reader, int *count) { - if (!reader || !count) + if (!xr(reader) || !count) return OAKCOMMON_E_INVALID; try { - *count = static_cast(reader->reader.attributes().size()); + *count = static_cast(xr(reader)->reader.attributes().size()); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -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) { - if (!reader) + if (!xr(reader)) return OAKCOMMON_E_INVALID; try { - const auto &attrs = reader->reader.attributes(); + const auto &attrs = xr(reader)->reader.attributes(); if (index < 0 || index >= static_cast(attrs.size())) return OAKCOMMON_E_NOT_FOUND; return copy_string(attrs[index].name, buf, buf_size); @@ -163,13 +182,13 @@ int oakcommon_xml_reader_attribute_name(OakCommonXmlReader *reader, int index, } } -int oakcommon_xml_reader_attribute_value(OakCommonXmlReader *reader, +int oakcommon_xml_reader_attribute_value(OakXmlReader reader, int index, char *buf, int buf_size) { - if (!reader) + if (!xr(reader)) return OAKCOMMON_E_INVALID; try { - const auto &attrs = reader->reader.attributes(); + const auto &attrs = xr(reader)->reader.attributes(); if (index < 0 || index >= static_cast(attrs.size())) return OAKCOMMON_E_NOT_FOUND; return copy_string(attrs[index].value, buf, buf_size); @@ -178,117 +197,119 @@ int oakcommon_xml_reader_attribute_value(OakCommonXmlReader *reader, } } -int oakcommon_xml_reader_has_error(OakCommonXmlReader *reader, +int oakcommon_xml_reader_has_error(OakXmlReader reader, int *has_error) { - if (!reader || !has_error) + if (!xr(reader) || !has_error) return OAKCOMMON_E_INVALID; try { - *has_error = reader->reader.has_error() ? 1 : 0; + *has_error = xr(reader)->reader.has_error() ? 1 : 0; return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -OakCommonXmlWriter *oakcommon_xml_writer_init(void) +OakXmlWriter oakcommon_xml_writer_init(void) { try { - return new (std::nothrow) OakCommonXmlWriter(); + return oakcommon::make_handle( + olive::XmlStreamWriter()); } catch (...) { - return nullptr; + OakXmlWriter h = {}; + return h; } } -void oakcommon_xml_writer_free(OakCommonXmlWriter *writer) +void oakcommon_xml_writer_free(OakXmlWriter *writer) { - delete writer; + oakcommon::free_handle(writer); } -int oakcommon_xml_writer_write_start_element(OakCommonXmlWriter *writer, +int oakcommon_xml_writer_write_start_element(OakXmlWriter writer, const char *name) { - if (!writer || !name) + if (!xw(writer) || !name) return OAKCOMMON_E_INVALID; try { - writer->writer.write_start_element(name); + xw(writer)->write_start_element(name); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_xml_writer_write_attribute(OakCommonXmlWriter *writer, +int oakcommon_xml_writer_write_attribute(OakXmlWriter writer, const char *name, const char *value) { - if (!writer || !name || !value) + if (!xw(writer) || !name || !value) return OAKCOMMON_E_INVALID; try { - writer->writer.write_attribute(name, value); + xw(writer)->write_attribute(name, value); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_xml_writer_write_characters(OakCommonXmlWriter *writer, +int oakcommon_xml_writer_write_characters(OakXmlWriter writer, const char *text) { - if (!writer || !text) + if (!xw(writer) || !text) return OAKCOMMON_E_INVALID; try { - writer->writer.write_characters(text); + xw(writer)->write_characters(text); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_xml_writer_write_text_element(OakCommonXmlWriter *writer, +int oakcommon_xml_writer_write_text_element(OakXmlWriter writer, const char *name, const char *text) { - if (!writer || !name || !text) + if (!xw(writer) || !name || !text) return OAKCOMMON_E_INVALID; try { - writer->writer.write_text_element(name, text); + xw(writer)->write_text_element(name, text); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_xml_writer_write_end_element(OakCommonXmlWriter *writer) +int oakcommon_xml_writer_write_end_element(OakXmlWriter writer) { - if (!writer) + if (!xw(writer)) return OAKCOMMON_E_INVALID; try { - writer->writer.write_end_element(); + xw(writer)->write_end_element(); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_xml_writer_write_end_document(OakCommonXmlWriter *writer) +int oakcommon_xml_writer_write_end_document(OakXmlWriter writer) { - if (!writer) + if (!xw(writer)) return OAKCOMMON_E_INVALID; try { - writer->writer.write_end_document(); + xw(writer)->write_end_document(); return OAKCOMMON_OK; } catch (...) { return OAKCOMMON_E_FAILED; } } -int oakcommon_xml_writer_output(OakCommonXmlWriter *writer, char *buf, +int oakcommon_xml_writer_output(OakXmlWriter writer, char *buf, int buf_size) { - if (!writer) + if (!xw(writer)) return OAKCOMMON_E_INVALID; try { - return copy_string(writer->writer.output(), buf, buf_size); + return copy_string(xw(writer)->output(), buf, buf_size); } catch (...) { return OAKCOMMON_E_FAILED; } diff --git a/src/common/src/debug.cpp b/src/common/src/debug.cpp index 555c8a743..45108647c 100644 --- a/src/common/src/debug.cpp +++ b/src/common/src/debug.cpp @@ -20,12 +20,39 @@ #include "debug.h" +#include #include #include +#include namespace olive { +namespace +{ + +/** + * @brief Minimum level emitted by log_message(); default k_debug_info. + */ +std::atomic g_log_level{ k_debug_info }; + +/** + * @brief Guards g_log_sink (std::function is not atomic). + */ +std::mutex g_sink_mutex; +LogSink g_log_sink; + +/** + * @brief Default sink: stderr + flush, same as debug_handler(). + */ +void stderr_sink(const std::string &line) +{ + fputs(line.c_str(), stderr); + fflush(stderr); +} + +} // namespace + int debug_level_name(int level, char *buf, int buf_size) { const char *name; @@ -58,16 +85,82 @@ int debug_level_name(int level, char *buf, int buf_size) return needed; } -void debug_handler(int level, const char *msg) +std::string format_log_line(int level, const std::string &msg) { char level_name[16]; debug_level_name(level, level_name, sizeof(level_name)); - fprintf(stderr, "[%s] %s\n", level_name, msg ? msg : ""); - // Always flush so debug messages appear immediately, even on - // platforms that buffer stderr. - fflush(stderr); + std::string line; + line.reserve(strlen(level_name) + msg.size() + 4); + line += '['; + line += level_name; + line += "] "; + line += msg; + line += '\n'; + return line; +} + +void debug_handler(int level, const char *msg) +{ + stderr_sink(format_log_line(level, msg ? msg : "")); +} + +void set_log_sink(LogSink sink) +{ + std::lock_guard lock(g_sink_mutex); + g_log_sink = std::move(sink); +} + +void set_log_level(DebugLevel level) +{ + if (level < k_debug_debug || level > k_debug_fatal) + return; + g_log_level.store(static_cast(level), std::memory_order_relaxed); +} + +DebugLevel get_log_level() +{ + return static_cast(g_log_level.load(std::memory_order_relaxed)); +} + +void log_message(int level, const std::string &msg) +{ + if (level < g_log_level.load(std::memory_order_relaxed)) + return; + + std::string line = format_log_line(level, msg); + + LogSink sink; + { + std::lock_guard lock(g_sink_mutex); + sink = g_log_sink; + } + // Invoke outside the lock so a sink may log re-entrantly. + if (sink) + sink(line); + else + stderr_sink(line); +} + +void log_debug(const std::string &msg) +{ + log_message(k_debug_debug, msg); +} + +void log_info(const std::string &msg) +{ + log_message(k_debug_info, msg); +} + +void log_warning(const std::string &msg) +{ + log_message(k_debug_warning, msg); +} + +void log_critical(const std::string &msg) +{ + log_message(k_debug_error, msg); } } diff --git a/src/common/src/debug.h b/src/common/src/debug.h index 6c7224e8c..0b833d33a 100644 --- a/src/common/src/debug.h +++ b/src/common/src/debug.h @@ -21,6 +21,9 @@ #ifndef OAK_DEBUG_H #define OAK_DEBUG_H +#include +#include + namespace olive { @@ -28,7 +31,8 @@ namespace olive * @brief Severity levels for debug output. * * Replaces Qt's QtMsgType now that the debug handler no longer depends - * on QDebug. + * on QDebug. Values are ordered by ascending severity so that a simple + * `level < threshold` comparison implements level filtering. */ enum DebugLevel { k_debug_debug, @@ -56,9 +60,65 @@ int debug_level_name(int level, char *buf, int buf_size); * De-Qt replacement for the old Qt message handler: qDebug() output is * replaced by fprintf(stderr). A NULL message is treated as an empty * string. Lines are always flushed so messages appear immediately. + * + * This is the unfiltered low-level writer; use log_message() (or the + * log_debug()/log_info()/... helpers) for level-filtered logging. */ void debug_handler(int level, const char *msg); +/** + * @brief Format a log line as "[LEVEL] message\n". + */ +std::string format_log_line(int level, const std::string &msg); + +/** + * @brief Destination for filtered log lines. + * + * Receives the fully formatted line ("[LEVEL] message\n"). The default + * sink writes to stderr and flushes, matching debug_handler(). + */ +using LogSink = std::function; + +/** + * @brief Install a custom log sink (e.g. for tests or log files). + * + * Passing an empty LogSink restores the default stderr sink. + * Thread-safe; the sink is invoked without the internal lock held. + */ +void set_log_sink(LogSink sink); + +/** + * @brief Set the minimum level emitted by log_message(). + * + * Messages with a lower level are dropped. The default is + * k_debug_info. Values outside the DebugLevel range are ignored. + * Thread-safe (atomic store). + */ +void set_log_level(DebugLevel level); + +/** + * @brief The current minimum level emitted by log_message(). + */ +DebugLevel get_log_level(); + +/** + * @brief Emit a message if `level` passes the current level filter. + * + * The formatted line ("[LEVEL] message\n") is handed to the installed + * sink. Messages below the level set with set_log_level() are dropped. + */ +void log_message(int level, const std::string &msg); + +/** + * @brief Level-filtered convenience wrappers, replacing qDebug(), + * qInfo(), qWarning() and qCritical(). Callers compose the message + * themselves (plain std::string concatenation). + */ +void log_debug(const std::string &msg); +void log_info(const std::string &msg); +void log_warning(const std::string &msg); +void log_critical(const std::string &msg); + } #endif // OAK_DEBUG_H diff --git a/src/common/src/oiioutils.cpp b/src/common/src/oiioutils.cpp index 4a4bbfca2..20cbbbee2 100644 --- a/src/common/src/oiioutils.cpp +++ b/src/common/src/oiioutils.cpp @@ -88,17 +88,3 @@ OIIOUtils::get_pixel_aspect_ratio_from_oiio(const OIIO::ImageSpec &spec) return olive::core::Rational::from_double( spec.get_float_attribute("PixelAspectRatio", 1)); } - -void OIIOUtils::frame_to_buffer(const void *data, int64_t linesize_bytes, - OIIO::ImageBuf *buf) -{ - buf->set_pixels(OIIO::ROI(), buf->spec().format, data, OIIO::AutoStride, - static_cast(linesize_bytes)); -} - -void OIIOUtils::buffer_to_frame(OIIO::ImageBuf *buf, void *data, - int64_t linesize_bytes) -{ - buf->get_pixels(OIIO::ROI(), buf->spec().format, data, OIIO::AutoStride, - static_cast(linesize_bytes)); -} diff --git a/src/common/src/oiioutils.h b/src/common/src/oiioutils.h index 32c1d6517..e8b7b39ca 100644 --- a/src/common/src/oiioutils.h +++ b/src/common/src/oiioutils.h @@ -35,8 +35,9 @@ * Qt-free reimplementation of the former olive::OIIOUtils. The reverse * dependencies on codec/frame.h and render/videoparams.h (which pull in * Qt) were replaced with the Qt-free olive/core/render/pixelformat.h and - * olive/core/util/rational.h. The Frame-based helpers were flattened to - * raw data pointer + linesize, which is all they ever used from Frame. + * olive/core/util/rational.h. The Frame-based helpers (frame_to_buffer / + * buffer_to_frame) moved to oakcodec in M5 + * (src/codec/src/oiioframebridge.h) โ€” they are only used by codec. */ class OIIOUtils { public: @@ -65,24 +66,6 @@ public: */ static olive::core::Rational get_pixel_aspect_ratio_from_oiio(const OIIO::ImageSpec &spec); - - /** - * @brief Copies raw pixel data into an OIIO image buffer - * - * Flattened form of the former Frame-based frame_to_buffer(); pass - * Frame::const_data() and Frame::linesize_bytes() at the call site. - */ - static void frame_to_buffer(const void *data, int64_t linesize_bytes, - OIIO::ImageBuf *buf); - - /** - * @brief Copies an OIIO image buffer's pixels into raw memory - * - * Flattened form of the former Frame-based buffer_to_frame(); pass - * Frame::data() and Frame::linesize_bytes() at the call site. - */ - static void buffer_to_frame(OIIO::ImageBuf *buf, void *data, - int64_t linesize_bytes); }; #endif // OAK_OIIOUTILS_H diff --git a/src/common/tests/CMakeLists.txt b/src/common/tests/CMakeLists.txt index aa9b9d561..e4fa8c00a 100644 --- a/src/common/tests/CMakeLists.txt +++ b/src/common/tests/CMakeLists.txt @@ -26,6 +26,8 @@ add_executable(oakcommon-gtest dropworkflowbehavior_test.cpp ffmpegutils_test.cpp filefunctions_test.cpp + handle_test.cpp + log_test.cpp memorypool_test.cpp miscutils_test.cpp ocioutils_test.cpp diff --git a/src/common/tests/colortransform_test.cpp b/src/common/tests/colortransform_test.cpp index 4923cdce4..84dbfaf56 100644 --- a/src/common/tests/colortransform_test.cpp +++ b/src/common/tests/colortransform_test.cpp @@ -29,8 +29,8 @@ namespace { -std::string read_string(int (*fn)(OakCommonColorTransform *, char *, int), - OakCommonColorTransform *t) +std::string read_string(int (*fn)(OakColorTransform, char *, int), + OakColorTransform t) { int needed = fn(t, nullptr, 0); EXPECT_GT(needed, 0); @@ -43,9 +43,8 @@ std::string read_string(int (*fn)(OakCommonColorTransform *, char *, int), TEST(CommonColorTransformCApi, InitOutput) { - OakCommonColorTransform *t = - oakcommon_colortransform_init_output("sRGB"); - ASSERT_NE(t, nullptr); + OakColorTransform t = oakcommon_colortransform_init_output("sRGB"); + ASSERT_NE(t.ctx, nullptr); int is_display = -1; EXPECT_EQ(oakcommon_colortransform_is_display(t, &is_display), @@ -54,19 +53,19 @@ TEST(CommonColorTransformCApi, InitOutput) EXPECT_EQ(read_string(oakcommon_colortransform_get_output, t), "sRGB"); EXPECT_EQ(read_string(oakcommon_colortransform_get_display, t), "sRGB"); - oakcommon_colortransform_free(t); + oakcommon_colortransform_free(&t); } TEST(CommonColorTransformCApi, InitOutputNullString) { - EXPECT_EQ(oakcommon_colortransform_init_output(nullptr), nullptr); + EXPECT_EQ(oakcommon_colortransform_init_output(nullptr).ctx, + nullptr); } TEST(CommonColorTransformCApi, InitDisplay) { - OakCommonColorTransform *t = - oakcommon_colortransform_init_display("sRGB", "Studio", "None"); - ASSERT_NE(t, nullptr); + OakColorTransform t = oakcommon_colortransform_init_display("sRGB", "Studio", "None"); + ASSERT_NE(t.ctx, nullptr); int is_display = 0; EXPECT_EQ(oakcommon_colortransform_is_display(t, &is_display), @@ -76,16 +75,16 @@ TEST(CommonColorTransformCApi, InitDisplay) EXPECT_EQ(read_string(oakcommon_colortransform_get_view, t), "Studio"); EXPECT_EQ(read_string(oakcommon_colortransform_get_look, t), "None"); - oakcommon_colortransform_free(t); + oakcommon_colortransform_free(&t); } TEST(CommonColorTransformCApi, InitDisplayNullString) { - EXPECT_EQ(oakcommon_colortransform_init_display(nullptr, "v", "l"), + EXPECT_EQ(oakcommon_colortransform_init_display(nullptr, "v", "l").ctx, nullptr); - EXPECT_EQ(oakcommon_colortransform_init_display("d", nullptr, "l"), + EXPECT_EQ(oakcommon_colortransform_init_display("d", nullptr, "l").ctx, nullptr); - EXPECT_EQ(oakcommon_colortransform_init_display("d", "v", nullptr), + EXPECT_EQ(oakcommon_colortransform_init_display("d", "v", nullptr).ctx, nullptr); } @@ -98,24 +97,23 @@ TEST(CommonColorTransformCApi, NullHandleErrors) { int i = 0; char buf[16]; - EXPECT_EQ(oakcommon_colortransform_is_display(nullptr, &i), + EXPECT_EQ(oakcommon_colortransform_is_display(OakColorTransform{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_colortransform_get_display(nullptr, buf, sizeof(buf)), + EXPECT_EQ(oakcommon_colortransform_get_display(OakColorTransform{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_colortransform_get_output(nullptr, buf, sizeof(buf)), + EXPECT_EQ(oakcommon_colortransform_get_output(OakColorTransform{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_colortransform_get_view(nullptr, buf, sizeof(buf)), + EXPECT_EQ(oakcommon_colortransform_get_view(OakColorTransform{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_colortransform_get_look(nullptr, buf, sizeof(buf)), + EXPECT_EQ(oakcommon_colortransform_get_look(OakColorTransform{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); } TEST(CommonColorTransformCApi, NullOutParam) { - OakCommonColorTransform *t = - oakcommon_colortransform_init_output("sRGB"); - ASSERT_NE(t, nullptr); + OakColorTransform t = oakcommon_colortransform_init_output("sRGB"); + ASSERT_NE(t.ctx, nullptr); EXPECT_EQ(oakcommon_colortransform_is_display(t, nullptr), OAKCOMMON_E_INVALID); - oakcommon_colortransform_free(t); + oakcommon_colortransform_free(&t); } diff --git a/src/common/tests/commandlineparser_test.cpp b/src/common/tests/commandlineparser_test.cpp index 346e0a422..0eaf637a7 100644 --- a/src/common/tests/commandlineparser_test.cpp +++ b/src/common/tests/commandlineparser_test.cpp @@ -27,9 +27,9 @@ TEST(CommandLineParser, InitFree) { - OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); - ASSERT_NE(parser, nullptr); - oakcommon_commandlineparser_free(parser); + OakCommandLineParser parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser.ctx, nullptr); + oakcommon_commandlineparser_free(&parser); } TEST(CommandLineParser, FreeNull) @@ -41,14 +41,13 @@ TEST(CommandLineParser, FreeNull) TEST(CommandLineParser, AddOptionInvalidArgs) { - OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); - ASSERT_NE(parser, nullptr); + OakCommandLineParser parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser.ctx, nullptr); const char *names[] = { "h", "-help" }; - OakCommonCommandLineOption *option = nullptr; + OakCommandLineOption option = {}; - EXPECT_EQ(oakcommon_commandlineparser_add_option( - nullptr, names, 2, "desc", 0, nullptr, 0, &option), + EXPECT_EQ(oakcommon_commandlineparser_add_option(OakCommandLineParser{}, names, 2, "desc", 0, nullptr, 0, &option), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_commandlineparser_add_option( parser, nullptr, 2, "desc", 0, nullptr, 0, &option), @@ -57,28 +56,28 @@ TEST(CommandLineParser, AddOptionInvalidArgs) parser, names, 0, "desc", 0, nullptr, 0, &option), OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(parser); + oakcommon_commandlineparser_free(&parser); } TEST(CommandLineParser, ProcessOptionHitAndMiss) { - OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); - ASSERT_NE(parser, nullptr); + OakCommandLineParser parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser.ctx, nullptr); const char *names[] = { "h", "-help" }; - OakCommonCommandLineOption *hit = nullptr; - OakCommonCommandLineOption *miss = nullptr; + OakCommandLineOption hit = {}; + OakCommandLineOption miss = {}; ASSERT_EQ(oakcommon_commandlineparser_add_option( parser, names, 2, "Show help", 0, nullptr, 0, &hit), OAKCOMMON_OK); - ASSERT_NE(hit, nullptr); + ASSERT_NE(hit.ctx, nullptr); const char *other_names[] = { "v", "-version" }; ASSERT_EQ(oakcommon_commandlineparser_add_option( parser, other_names, 2, "Show version", 0, nullptr, 0, &miss), OAKCOMMON_OK); - ASSERT_NE(miss, nullptr); + ASSERT_NE(miss.ctx, nullptr); const char *argv[] = { "oak", "-h" }; ASSERT_EQ(oakcommon_commandlineparser_process(parser, argv, 2), @@ -92,18 +91,18 @@ TEST(CommandLineParser, ProcessOptionHitAndMiss) ASSERT_EQ(oakcommon_commandlineoption_is_set(miss, &is_set), OAKCOMMON_OK); EXPECT_FALSE(is_set); - oakcommon_commandlineoption_free(hit); - oakcommon_commandlineoption_free(miss); - oakcommon_commandlineparser_free(parser); + oakcommon_commandlineoption_free(&hit); + oakcommon_commandlineoption_free(&miss); + oakcommon_commandlineparser_free(&parser); } TEST(CommandLineParser, ProcessMatchesSecondAliasCaseInsensitive) { - OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); - ASSERT_NE(parser, nullptr); + OakCommandLineParser parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser.ctx, nullptr); const char *names[] = { "h", "-help" }; - OakCommonCommandLineOption *option = nullptr; + OakCommandLineOption option = {}; ASSERT_EQ(oakcommon_commandlineparser_add_option( parser, names, 2, "Show help", 0, nullptr, 0, &option), OAKCOMMON_OK); @@ -118,17 +117,17 @@ TEST(CommandLineParser, ProcessMatchesSecondAliasCaseInsensitive) OAKCOMMON_OK); EXPECT_TRUE(is_set); - oakcommon_commandlineoption_free(option); - oakcommon_commandlineparser_free(parser); + oakcommon_commandlineoption_free(&option); + oakcommon_commandlineparser_free(&parser); } TEST(CommandLineParser, OptionTakesArg) { - OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); - ASSERT_NE(parser, nullptr); + OakCommandLineParser parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser.ctx, nullptr); const char *names[] = { "e", "-export" }; - OakCommonCommandLineOption *option = nullptr; + OakCommandLineOption option = {}; ASSERT_EQ(oakcommon_commandlineparser_add_option( parser, names, 2, "Export", 1, "filename", 0, &option), OAKCOMMON_OK); @@ -153,20 +152,20 @@ TEST(CommandLineParser, OptionTakesArg) required); EXPECT_STREQ(buf.data(), "/tmp/out.mp4"); - oakcommon_commandlineoption_free(option); - oakcommon_commandlineparser_free(parser); + oakcommon_commandlineoption_free(&option); + oakcommon_commandlineparser_free(&parser); } TEST(CommandLineParser, PositionalArgument) { - OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); - ASSERT_NE(parser, nullptr); + OakCommandLineParser parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser.ctx, nullptr); - OakCommonCommandLinePositionalArgument *arg = nullptr; + OakCommandLinePositionalArgument arg = {}; ASSERT_EQ(oakcommon_commandlineparser_add_positional_argument( parser, "project", "Project file", 0, &arg), OAKCOMMON_OK); - ASSERT_NE(arg, nullptr); + ASSERT_NE(arg.ctx, nullptr); const char *argv[] = { "oak", "/tmp/project.ove" }; ASSERT_EQ(oakcommon_commandlineparser_process(parser, argv, 2), @@ -182,20 +181,20 @@ TEST(CommandLineParser, PositionalArgument) required); EXPECT_STREQ(buf.data(), "/tmp/project.ove"); - oakcommon_commandlinepositionalargument_free(arg); - oakcommon_commandlineparser_free(parser); + oakcommon_commandlinepositionalargument_free(&arg); + oakcommon_commandlineparser_free(&parser); } TEST(CommandLineParser, PositionalArgumentSetGetSetting) { - OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); - ASSERT_NE(parser, nullptr); + OakCommandLineParser parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser.ctx, nullptr); - OakCommonCommandLinePositionalArgument *arg = nullptr; + OakCommandLinePositionalArgument arg = {}; ASSERT_EQ(oakcommon_commandlineparser_add_positional_argument( parser, "project", "Project file", 0, &arg), OAKCOMMON_OK); - ASSERT_NE(arg, nullptr); + ASSERT_NE(arg.ctx, nullptr); ASSERT_EQ(oakcommon_commandlinepositionalargument_set_setting(arg, "hello.ove"), @@ -209,28 +208,28 @@ TEST(CommandLineParser, PositionalArgumentSetGetSetting) EXPECT_STREQ(buf, "hello.ove"); // Error paths - EXPECT_EQ(oakcommon_commandlinepositionalargument_set_setting(nullptr, + EXPECT_EQ(oakcommon_commandlinepositionalargument_set_setting(OakCommandLinePositionalArgument{}, "x"), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_commandlinepositionalargument_set_setting(arg, nullptr), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_commandlinepositionalargument_get_setting(nullptr, + EXPECT_EQ(oakcommon_commandlinepositionalargument_get_setting(OakCommandLinePositionalArgument{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); - oakcommon_commandlinepositionalargument_free(arg); - oakcommon_commandlineparser_free(parser); + oakcommon_commandlinepositionalargument_free(&arg); + oakcommon_commandlineparser_free(&parser); } TEST(CommandLineParser, TwoStageStringGetterSmallBuffer) { - OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); - ASSERT_NE(parser, nullptr); + OakCommandLineParser parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser.ctx, nullptr); const char *names[] = { "e" }; - OakCommonCommandLineOption *option = nullptr; + OakCommandLineOption option = {}; ASSERT_EQ(oakcommon_commandlineparser_add_option( parser, names, 1, "Export", 1, "filename", 0, &option), OAKCOMMON_OK); @@ -249,27 +248,27 @@ TEST(CommandLineParser, TwoStageStringGetterSmallBuffer) EXPECT_EQ(oakcommon_commandlineoption_get_setting(option, nullptr, 0), 7); // Error paths - EXPECT_EQ(oakcommon_commandlineoption_get_setting(nullptr, buf, + EXPECT_EQ(oakcommon_commandlineoption_get_setting(OakCommandLineOption{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_commandlineoption_is_set(option, nullptr), OAKCOMMON_E_INVALID); bool dummy = false; - EXPECT_EQ(oakcommon_commandlineoption_is_set(nullptr, &dummy), + EXPECT_EQ(oakcommon_commandlineoption_is_set(OakCommandLineOption{}, &dummy), OAKCOMMON_E_INVALID); - oakcommon_commandlineoption_free(option); - oakcommon_commandlineparser_free(parser); + oakcommon_commandlineoption_free(&option); + oakcommon_commandlineparser_free(&parser); } TEST(CommandLineParser, ProcessInvalidArgs) { - EXPECT_EQ(oakcommon_commandlineparser_process(nullptr, nullptr, 0), + EXPECT_EQ(oakcommon_commandlineparser_process(OakCommandLineParser{}, nullptr, 0), OAKCOMMON_E_INVALID); - OakCommonCommandLineParser *parser = oakcommon_commandlineparser_init(); - ASSERT_NE(parser, nullptr); + OakCommandLineParser parser = oakcommon_commandlineparser_init(); + ASSERT_NE(parser.ctx, nullptr); EXPECT_EQ(oakcommon_commandlineparser_process(parser, nullptr, 1), OAKCOMMON_E_INVALID); - oakcommon_commandlineparser_free(parser); + oakcommon_commandlineparser_free(&parser); } diff --git a/src/common/tests/current_test.cpp b/src/common/tests/current_test.cpp index bff59b32c..70939c03a 100644 --- a/src/common/tests/current_test.cpp +++ b/src/common/tests/current_test.cpp @@ -24,25 +24,29 @@ #include "common/current.h" -TEST(OakCommonCurrent, InstanceIsSingleton) +TEST(OakCurrent, InstanceIsSingleton) { - OakCommonCurrent *a = oakcommon_current_instance(); - OakCommonCurrent *b = oakcommon_current_instance(); + OakCurrent a = oakcommon_current_instance(); + OakCurrent b = oakcommon_current_instance(); - ASSERT_NE(a, nullptr); - EXPECT_EQ(a, b); + ASSERT_NE(a.ctx, nullptr); + EXPECT_EQ(a.ctx, b.ctx); + EXPECT_EQ(a.abi_version, OAKCOMMON_ABI_VERSION); } -TEST(OakCommonCurrent, FreeNullIsNoOp) +TEST(OakCurrent, FreeNullIsNoOp) { oakcommon_current_free(nullptr); - oakcommon_current_free(oakcommon_current_instance()); + OakCurrent c = oakcommon_current_instance(); + oakcommon_current_free(&c); + // Releasing the singleton handle never destroys the object. + EXPECT_NE(c.ctx, nullptr); SUCCEED(); } -TEST(OakCommonCurrent, VideoParamsSetGetRoundTrip) +TEST(OakCurrent, VideoParamsSetGetRoundTrip) { - OakCommonCurrent *c = oakcommon_current_instance(); + OakCurrent c = oakcommon_current_instance(); int *params = static_cast(malloc(sizeof(int))); ASSERT_NE(params, nullptr); *params = 42; @@ -62,9 +66,9 @@ TEST(OakCommonCurrent, VideoParamsSetGetRoundTrip) EXPECT_EQ(out, nullptr); } -TEST(OakCommonCurrent, AudioParamsSetGetRoundTrip) +TEST(OakCurrent, AudioParamsSetGetRoundTrip) { - OakCommonCurrent *c = oakcommon_current_instance(); + OakCurrent c = oakcommon_current_instance(); int value = 7; // non-owning storage, no destroy callback ASSERT_EQ(oakcommon_current_set_audio_params(c, &value, nullptr), @@ -78,9 +82,9 @@ TEST(OakCommonCurrent, AudioParamsSetGetRoundTrip) OAKCOMMON_OK); } -TEST(OakCommonCurrent, PluginHostAndCacheRoundTrip) +TEST(OakCurrent, PluginHostAndCacheRoundTrip) { - OakCommonCurrent *c = oakcommon_current_instance(); + OakCurrent c = oakcommon_current_instance(); int host = 1, cache = 2; ASSERT_EQ(oakcommon_current_set_plugin_host(c, &host, nullptr), @@ -100,31 +104,29 @@ TEST(OakCommonCurrent, PluginHostAndCacheRoundTrip) OAKCOMMON_OK); } -TEST(OakCommonCurrent, NullHandleAndOutArgs) +TEST(OakCurrent, NullHandleAndOutArgs) { void *out = nullptr; int flag = 0; - EXPECT_EQ(oakcommon_current_set_video_params(nullptr, &flag, nullptr), + EXPECT_EQ(oakcommon_current_set_video_params(OakCurrent{}, &flag, nullptr), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_current_get_video_params(nullptr, &out), + EXPECT_EQ(oakcommon_current_get_video_params(OakCurrent{}, &out), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_current_get_video_params( - oakcommon_current_instance(), nullptr), + OakCurrent c = oakcommon_current_instance(); + EXPECT_EQ(oakcommon_current_get_video_params(c, nullptr), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_current_is_interactive(nullptr, &flag), + EXPECT_EQ(oakcommon_current_is_interactive(OakCurrent{}, &flag), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_current_is_interactive( - oakcommon_current_instance(), nullptr), + EXPECT_EQ(oakcommon_current_is_interactive(c, nullptr), OAKCOMMON_E_INVALID); } -TEST(OakCommonCurrent, IsInteractive) +TEST(OakCurrent, IsInteractive) { int flag = 0; - ASSERT_EQ(oakcommon_current_is_interactive( - oakcommon_current_instance(), &flag), - OAKCOMMON_OK); + OakCurrent c = oakcommon_current_instance(); + ASSERT_EQ(oakcommon_current_is_interactive(c, &flag), OAKCOMMON_OK); EXPECT_EQ(flag, 1); } diff --git a/src/common/tests/debug_test.cpp b/src/common/tests/debug_test.cpp index a824c3b23..c545c1bdb 100644 --- a/src/common/tests/debug_test.cpp +++ b/src/common/tests/debug_test.cpp @@ -22,25 +22,25 @@ #include "common/debug.h" -TEST(OakCommonDebug, LogValidMessage) +TEST(OakDebug, LogValidMessage) { EXPECT_EQ(oakcommon_debug_log(OAKCOMMON_DEBUG_WARNING, "hello"), OAKCOMMON_OK); } -TEST(OakCommonDebug, LogNullMessage) +TEST(OakDebug, LogNullMessage) { EXPECT_EQ(oakcommon_debug_log(OAKCOMMON_DEBUG_WARNING, nullptr), OAKCOMMON_E_INVALID); } -TEST(OakCommonDebug, LogOutOfRangeLevel) +TEST(OakDebug, LogOutOfRangeLevel) { // Out-of-range levels are tolerated and print as UNKNOWN. EXPECT_EQ(oakcommon_debug_log(999, "odd level"), OAKCOMMON_OK); } -TEST(OakCommonDebug, LevelNameRoundTrip) +TEST(OakDebug, LevelNameRoundTrip) { char buf[16]; @@ -51,14 +51,14 @@ TEST(OakCommonDebug, LevelNameRoundTrip) EXPECT_STREQ(buf, "WARNING"); } -TEST(OakCommonDebug, LevelNameQuerySize) +TEST(OakDebug, LevelNameQuerySize) { int needed = oakcommon_debug_level_name(OAKCOMMON_DEBUG_DEBUG, nullptr, 0); EXPECT_EQ(needed, 6); // "DEBUG" + NUL } -TEST(OakCommonDebug, LevelNameUnknownLevel) +TEST(OakDebug, LevelNameUnknownLevel) { char buf[16]; diff --git a/src/common/tests/dropworkflowbehavior_test.cpp b/src/common/tests/dropworkflowbehavior_test.cpp index 1e41a9edf..fcf62d1a0 100644 --- a/src/common/tests/dropworkflowbehavior_test.cpp +++ b/src/common/tests/dropworkflowbehavior_test.cpp @@ -22,7 +22,7 @@ #include "common/dropworkflowbehavior.h" -TEST(OakCommonDropWorkflowBehavior, IsValidAcceptsAllEnumerators) +TEST(OakDropWorkflowBehavior, IsValidAcceptsAllEnumerators) { EXPECT_EQ(oakcommon_drop_workflow_behavior_is_valid(OAKCOMMON_DWS_ASK), 1); @@ -36,13 +36,13 @@ TEST(OakCommonDropWorkflowBehavior, IsValidAcceptsAllEnumerators) 1); } -TEST(OakCommonDropWorkflowBehavior, IsValidRejectsOutOfRange) +TEST(OakDropWorkflowBehavior, IsValidRejectsOutOfRange) { EXPECT_EQ(oakcommon_drop_workflow_behavior_is_valid(-1), 0); EXPECT_EQ(oakcommon_drop_workflow_behavior_is_valid(4), 0); } -TEST(OakCommonDropWorkflowBehavior, NameRoundTrip) +TEST(OakDropWorkflowBehavior, NameRoundTrip) { char buf[16]; @@ -53,7 +53,7 @@ TEST(OakCommonDropWorkflowBehavior, NameRoundTrip) EXPECT_STREQ(buf, "AUTO"); } -TEST(OakCommonDropWorkflowBehavior, NameQuerySizeAndTooSmallBuffer) +TEST(OakDropWorkflowBehavior, NameQuerySizeAndTooSmallBuffer) { int needed = oakcommon_drop_workflow_behavior_name(OAKCOMMON_DWS_DISABLE, nullptr, 0); @@ -65,7 +65,7 @@ TEST(OakCommonDropWorkflowBehavior, NameQuerySizeAndTooSmallBuffer) needed); } -TEST(OakCommonDropWorkflowBehavior, NameInvalidValue) +TEST(OakDropWorkflowBehavior, NameInvalidValue) { char buf[16]; diff --git a/src/common/tests/ffmpegutils_test.cpp b/src/common/tests/ffmpegutils_test.cpp index 0a9de163b..765ae3dea 100644 --- a/src/common/tests/ffmpegutils_test.cpp +++ b/src/common/tests/ffmpegutils_test.cpp @@ -26,7 +26,7 @@ #include "../src/ffmpegutils.h" -TEST(OakCommonFFmpegUtils, GetCompatiblePixelFormatMapsCorrectly) +TEST(OakFFmpegUtils, GetCompatiblePixelFormatMapsCorrectly) { int out = -2; @@ -51,14 +51,14 @@ TEST(OakCommonFFmpegUtils, GetCompatiblePixelFormatMapsCorrectly) EXPECT_EQ(out, olive::core::PixelFormat::invalid); } -TEST(OakCommonFFmpegUtils, GetCompatiblePixelFormatNullOut) +TEST(OakFFmpegUtils, GetCompatiblePixelFormatNullOut) { EXPECT_EQ(oakcommon_ffmpegutils_get_compatible_pixel_format( olive::core::PixelFormat::u8, nullptr), OAKCOMMON_E_INVALID); } -TEST(OakCommonFFmpegUtils, GetFFmpegPixelFormatMapsCorrectly) +TEST(OakFFmpegUtils, GetFFmpegPixelFormatMapsCorrectly) { int out = -2; @@ -92,7 +92,7 @@ TEST(OakCommonFFmpegUtils, GetFFmpegPixelFormatMapsCorrectly) EXPECT_EQ(out, fb_pix_fmt_none); } -TEST(OakCommonFFmpegUtils, GetFFmpegPixelFormatNullOut) +TEST(OakFFmpegUtils, GetFFmpegPixelFormatNullOut) { EXPECT_EQ(oakcommon_ffmpegutils_get_ffmpeg_pixel_format( olive::core::PixelFormat::u8, @@ -100,7 +100,7 @@ TEST(OakCommonFFmpegUtils, GetFFmpegPixelFormatNullOut) OAKCOMMON_E_INVALID); } -TEST(OakCommonFFmpegUtils, GetNativeSampleFormatMapsCorrectly) +TEST(OakFFmpegUtils, GetNativeSampleFormatMapsCorrectly) { int out = -2; @@ -125,14 +125,14 @@ TEST(OakCommonFFmpegUtils, GetNativeSampleFormatMapsCorrectly) EXPECT_EQ(out, olive::core::SampleFormat::invalid); } -TEST(OakCommonFFmpegUtils, GetNativeSampleFormatNullOut) +TEST(OakFFmpegUtils, GetNativeSampleFormatNullOut) { EXPECT_EQ(oakcommon_ffmpegutils_get_native_sample_format( fb_sample_fmt_u8, nullptr), OAKCOMMON_E_INVALID); } -TEST(OakCommonFFmpegUtils, GetFFmpegSampleFormatMapsCorrectly) +TEST(OakFFmpegUtils, GetFFmpegSampleFormatMapsCorrectly) { int out = -2; @@ -157,14 +157,14 @@ TEST(OakCommonFFmpegUtils, GetFFmpegSampleFormatMapsCorrectly) EXPECT_EQ(out, fb_sample_fmt_none); } -TEST(OakCommonFFmpegUtils, GetFFmpegSampleFormatNullOut) +TEST(OakFFmpegUtils, GetFFmpegSampleFormatNullOut) { EXPECT_EQ(oakcommon_ffmpegutils_get_ffmpeg_sample_format( olive::core::SampleFormat::u8, nullptr), OAKCOMMON_E_INVALID); } -TEST(OakCommonFFmpegUtils, ConvertJpegSpaceToRegularSpaceMapsCorrectly) +TEST(OakFFmpegUtils, ConvertJpegSpaceToRegularSpaceMapsCorrectly) { int out = -2; @@ -184,21 +184,21 @@ TEST(OakCommonFFmpegUtils, ConvertJpegSpaceToRegularSpaceMapsCorrectly) EXPECT_EQ(out, fb_pix_fmt_rgba); } -TEST(OakCommonFFmpegUtils, ConvertJpegSpaceToRegularSpaceNullOut) +TEST(OakFFmpegUtils, ConvertJpegSpaceToRegularSpaceNullOut) { EXPECT_EQ(oakcommon_ffmpegutils_convert_jpeg_space_to_regular_space( fb_pix_fmt_rgba, nullptr), OAKCOMMON_E_INVALID); } -TEST(OakCommonFFmpegUtils, GetCompatibleBridgePixelFormatNullOut) +TEST(OakFFmpegUtils, GetCompatibleBridgePixelFormatNullOut) { EXPECT_EQ(oakcommon_ffmpegutils_get_compatible_bridge_pixel_format( fb_pix_fmt_rgba, -1, nullptr), OAKCOMMON_E_INVALID); } -TEST(OakCommonFFmpegUtils, GetCompatibleBridgePixelFormatMapsCorrectly) +TEST(OakFFmpegUtils, GetCompatibleBridgePixelFormatMapsCorrectly) { /* Calls fb_find_best_pix_fmt_of_list() in ffmpeg_bridge, which * requires a working FFmpeg runtime environment. */ diff --git a/src/common/tests/filefunctions_test.cpp b/src/common/tests/filefunctions_test.cpp index d7b01ba0c..3f815f271 100644 --- a/src/common/tests/filefunctions_test.cpp +++ b/src/common/tests/filefunctions_test.cpp @@ -35,7 +35,7 @@ protected: void SetUp() override { handle_ = oakcommon_filefunctions_init(); - ASSERT_NE(handle_, nullptr); + ASSERT_NE(handle_.ctx, nullptr); temp_dir_ = fs::temp_directory_path() / fs::path("oakcommon_filefunctions_test_" + @@ -46,8 +46,8 @@ protected: void TearDown() override { - oakcommon_filefunctions_free(handle_); - handle_ = nullptr; + oakcommon_filefunctions_free(&handle_); + handle_.ctx = nullptr; std::error_code ec; fs::remove_all(temp_dir_, ec); @@ -84,7 +84,7 @@ protected: return std::string(buf.data()); } - OakCommonFileFunctions *handle_ = nullptr; + OakFileFunctions handle_ = {}; fs::path temp_dir_; }; @@ -98,43 +98,36 @@ TEST_F(FileFunctionsTest, NullHandleReturnsInvalid) { char buf[16]; int out = 0; - EXPECT_EQ(oakcommon_filefunctions_get_configuration_location( - nullptr, buf, sizeof(buf)), + EXPECT_EQ(oakcommon_filefunctions_get_configuration_location(OakFileFunctions{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_filefunctions_get_application_path(nullptr, buf, + EXPECT_EQ(oakcommon_filefunctions_get_application_path(OakFileFunctions{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_filefunctions_get_temp_file_path(nullptr, buf, + EXPECT_EQ(oakcommon_filefunctions_get_temp_file_path(OakFileFunctions{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_filefunctions_get_auto_recovery_root(nullptr, buf, + EXPECT_EQ(oakcommon_filefunctions_get_auto_recovery_root(OakFileFunctions{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_filefunctions_get_unique_file_identifier( - nullptr, "x", buf, sizeof(buf)), + EXPECT_EQ(oakcommon_filefunctions_get_unique_file_identifier(OakFileFunctions{}, "x", buf, sizeof(buf)), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_filefunctions_ensure_filename_extension( - nullptr, "x", "y", buf, sizeof(buf)), + EXPECT_EQ(oakcommon_filefunctions_ensure_filename_extension(OakFileFunctions{}, "x", "y", buf, sizeof(buf)), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_filefunctions_read_file_as_string(nullptr, "x", + EXPECT_EQ(oakcommon_filefunctions_read_file_as_string(OakFileFunctions{}, "x", buf, sizeof(buf)), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_filefunctions_get_safe_temporary_filename( - nullptr, "x", buf, sizeof(buf)), + EXPECT_EQ(oakcommon_filefunctions_get_safe_temporary_filename(OakFileFunctions{}, "x", buf, sizeof(buf)), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_filefunctions_get_formatted_executable_for_platform( - nullptr, "x", buf, sizeof(buf)), + EXPECT_EQ(oakcommon_filefunctions_get_formatted_executable_for_platform(OakFileFunctions{}, "x", buf, sizeof(buf)), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_filefunctions_directory_is_valid(nullptr, "x", 1, + EXPECT_EQ(oakcommon_filefunctions_directory_is_valid(OakFileFunctions{}, "x", 1, &out), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_filefunctions_can_copy_directory_without_overwriting( - nullptr, "a", "b", &out), + EXPECT_EQ(oakcommon_filefunctions_can_copy_directory_without_overwriting(OakFileFunctions{}, "a", "b", &out), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_filefunctions_copy_directory(nullptr, "a", "b", 0), + EXPECT_EQ(oakcommon_filefunctions_copy_directory(OakFileFunctions{}, "a", "b", 0), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_filefunctions_rename_file_allow_overwrite( - nullptr, "a", "b", &out), + EXPECT_EQ(oakcommon_filefunctions_rename_file_allow_overwrite(OakFileFunctions{}, "a", "b", &out), OAKCOMMON_E_INVALID); } diff --git a/src/common/tests/handle_test.cpp b/src/common/tests/handle_test.cpp new file mode 100644 index 000000000..8788cd48e --- /dev/null +++ b/src/common/tests/handle_test.cpp @@ -0,0 +1,259 @@ +/*** + + 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 + +#include "common/colortransform.h" +#include "common/commandlineparser.h" +#include "common/current.h" +#include "common/filefunctions.h" +#include "common/ocioutils.h" +#include "common/oiioutils.h" +#include "common/subtitleparams.h" +#include "common/videoparams.h" +#include "common/xmlutils.h" + +// Native C++ headers (exported through the oakcommon target's public +// include dirs) for the init_from_native tests. +#include "colortransform.h" +#include "subtitleparams.h" +#include "videoparams.h" + +namespace +{ + +/** + * @brief Every init-produced handle must carry the current ABI version + * and non-NULL addref/release thunks. + */ +template void expect_valid_handle(const Handle &h) +{ + EXPECT_NE(h.ctx, nullptr); + EXPECT_NE(h.addref, nullptr); + EXPECT_NE(h.release, nullptr); + EXPECT_EQ(h.abi_version, OAKCOMMON_ABI_VERSION); +} + +} // namespace + +TEST(OakHandle, AbiVersionStampedEverywhere) +{ + OakVideoParams vp = oakcommon_videoparams_init(); + expect_valid_handle(vp); + oakcommon_videoparams_free(&vp); + + OakSubtitleParams sp = oakcommon_subtitleparams_init(); + expect_valid_handle(sp); + oakcommon_subtitleparams_free(&sp); + + OakColorTransform ct = oakcommon_colortransform_init_output("sRGB"); + expect_valid_handle(ct); + oakcommon_colortransform_free(&ct); + + OakCommandLineParser parser = oakcommon_commandlineparser_init(); + expect_valid_handle(parser); + + const char *names[] = { "h" }; + OakCommandLineOption option = {}; + ASSERT_EQ(oakcommon_commandlineparser_add_option( + parser, names, 1, "help", 0, nullptr, 0, &option), + OAKCOMMON_OK); + expect_valid_handle(option); + oakcommon_commandlineoption_free(&option); + + OakCommandLinePositionalArgument arg = {}; + ASSERT_EQ(oakcommon_commandlineparser_add_positional_argument( + parser, "file", "desc", 0, &arg), + OAKCOMMON_OK); + expect_valid_handle(arg); + oakcommon_commandlinepositionalargument_free(&arg); + oakcommon_commandlineparser_free(&parser); + + OakXmlReader reader = + oakcommon_xml_reader_init(""); + expect_valid_handle(reader); + oakcommon_xml_reader_free(&reader); + + OakXmlWriter writer = oakcommon_xml_writer_init(); + expect_valid_handle(writer); + oakcommon_xml_writer_free(&writer); + + OakFileFunctions ff = oakcommon_filefunctions_init(); + expect_valid_handle(ff); + oakcommon_filefunctions_free(&ff); + + OakOCIOUtils ocio = oakcommon_ocioutils_init(); + expect_valid_handle(ocio); + oakcommon_ocioutils_free(&ocio); + + OakOIIOUtils oiio = oakcommon_oiioutils_init(); + expect_valid_handle(oiio); + oakcommon_oiioutils_free(&oiio); + + OakCurrent current = oakcommon_current_instance(); + expect_valid_handle(current); +} + +TEST(OakHandle, AddrefReleaseCountSemantics) +{ + OakVideoParams h = oakcommon_videoparams_init(); + ASSERT_NE(h.ctx, nullptr); + + ASSERT_EQ(oakcommon_videoparams_set_width(h, 1920), OAKCOMMON_OK); + + // Copy the handle struct and take a second reference through the + // function pointer, like a foreign (Rust/DLL) consumer would. + OakVideoParams copy = h; + h.addref(h.ctx); + + // Dropping the first reference must not destroy the object: the copy + // is still fully usable. + h.release(h.ctx); + int width = 0; + ASSERT_EQ(oakcommon_videoparams_get_width(copy, &width), OAKCOMMON_OK); + EXPECT_EQ(width, 1920); + + // Dropping the last reference destroys the object; free() clears ctx. + oakcommon_videoparams_free(©); + EXPECT_EQ(copy.ctx, nullptr); +} + +TEST(OakHandle, FreeNullAndEmptyCtxAreNoOp) +{ + // NULL handle pointer. + oakcommon_videoparams_free(nullptr); + oakcommon_subtitleparams_free(nullptr); + oakcommon_colortransform_free(nullptr); + oakcommon_commandlineparser_free(nullptr); + oakcommon_commandlineoption_free(nullptr); + oakcommon_commandlinepositionalargument_free(nullptr); + oakcommon_xml_reader_free(nullptr); + oakcommon_xml_writer_free(nullptr); + oakcommon_filefunctions_free(nullptr); + oakcommon_ocioutils_free(nullptr); + oakcommon_oiioutils_free(nullptr); + oakcommon_current_free(nullptr); + + // Handle whose ctx is NULL (e.g. after a failed init or a free). + OakVideoParams h = {}; + oakcommon_videoparams_free(&h); + int width = 0; + EXPECT_EQ(oakcommon_videoparams_get_width(h, &width), + OAKCOMMON_E_INVALID); + + // release() itself must tolerate a NULL ctx (foreign consumers may + // call it directly). + h.release = nullptr; // no thunk available on a zero-initialized handle + oakcommon_videoparams_free(&h); + SUCCEED(); +} + +TEST(OakHandle, VideoParamsFromNativeSurvivesSource) +{ + OakVideoParams h = {}; + { + olive::VideoParams native( + 1920, 1080, olive::core::Rational(1, 25), + olive::core::PixelFormat::u8, 4, olive::core::Rational(1, 1), + olive::VideoParams::k_interlace_none, 1); + h = oakcommon_videoparams_init_from_native(&native); + ASSERT_NE(h.ctx, nullptr); + EXPECT_EQ(h.abi_version, OAKCOMMON_ABI_VERSION); + } // native stack object destroyed here + + int width = 0, height = 0, num = 0, den = 0; + ASSERT_EQ(oakcommon_videoparams_get_width(h, &width), OAKCOMMON_OK); + ASSERT_EQ(oakcommon_videoparams_get_height(h, &height), OAKCOMMON_OK); + ASSERT_EQ(oakcommon_videoparams_get_time_base(h, &num, &den), + OAKCOMMON_OK); + EXPECT_EQ(width, 1920); + EXPECT_EQ(height, 1080); + EXPECT_EQ(num, 1); + EXPECT_EQ(den, 25); + oakcommon_videoparams_free(&h); + + // NULL source yields an empty handle, not a crash. + OakVideoParams empty = + oakcommon_videoparams_init_from_native(nullptr); + EXPECT_EQ(empty.ctx, nullptr); +} + +TEST(OakHandle, SubtitleParamsFromNativeSurvivesSource) +{ + OakSubtitleParams h = {}; + { + olive::SubtitleParams native; + native.push_back(olive::Subtitle( + olive::core::TimeRange(olive::core::Rational(0, 1), + olive::core::Rational(2, 1)), + "hello")); + h = oakcommon_subtitleparams_init_from_native(&native); + ASSERT_NE(h.ctx, nullptr); + } // native stack object destroyed here + + int count = 0; + ASSERT_EQ(oakcommon_subtitleparams_count(h, &count), OAKCOMMON_OK); + EXPECT_EQ(count, 1); + + char buf[16]; + int needed = oakcommon_subtitleparams_get_subtitle_text(h, 0, buf, + sizeof(buf)); + ASSERT_EQ(needed, 6); + EXPECT_STREQ(buf, "hello"); + oakcommon_subtitleparams_free(&h); +} + +TEST(OakHandle, ColorTransformFromNativeSurvivesSource) +{ + OakColorTransform h = {}; + { + olive::ColorTransform native(std::string("Display"), + std::string("Standard"), + std::string("None")); + h = oakcommon_colortransform_init_from_native(&native); + ASSERT_NE(h.ctx, nullptr); + } // native stack object destroyed here + + int is_display = 0; + ASSERT_EQ(oakcommon_colortransform_is_display(h, &is_display), + OAKCOMMON_OK); + EXPECT_EQ(is_display, 1); + + char buf[16]; + int needed = oakcommon_colortransform_get_view(h, buf, sizeof(buf)); + ASSERT_EQ(needed, 9); + EXPECT_STREQ(buf, "Standard"); + oakcommon_colortransform_free(&h); +} + +TEST(OakHandle, CurrentSingletonReleaseNeverDestroys) +{ + OakCurrent h = oakcommon_current_instance(); + ASSERT_NE(h.ctx, nullptr); + void *ctx = h.ctx; + + // The singleton's addref/release are deliberate no-ops. + h.addref(h.ctx); + h.release(h.ctx); + oakcommon_current_free(&h); + + OakCurrent again = oakcommon_current_instance(); + EXPECT_EQ(again.ctx, ctx); +} diff --git a/src/common/tests/log_test.cpp b/src/common/tests/log_test.cpp new file mode 100644 index 000000000..50a00d3b8 --- /dev/null +++ b/src/common/tests/log_test.cpp @@ -0,0 +1,174 @@ +/*** + + 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 +#include + +#include + +#include "common/debug.h" + +// Native C++ header (exported through the oakcommon target's public +// include dirs) for sink injection. +#include "debug.h" + +namespace +{ + +/** + * @brief RAII helper: captures all filtered log lines into a vector and + * restores the default stderr sink and level on destruction. + */ +class LogCapture { +public: + LogCapture() + { + olive::set_log_sink([this](const std::string &line) { + lines.push_back(line); + }); + } + + ~LogCapture() + { + olive::set_log_sink(nullptr); + olive::set_log_level(olive::k_debug_info); + } + + std::vector lines; +}; + +} // namespace + +TEST(OakLog, LevelSetGetRoundTrip) +{ + ASSERT_EQ(oakcommon_log_set_level(OAKCOMMON_DEBUG_WARNING), + OAKCOMMON_OK); + int level = -1; + ASSERT_EQ(oakcommon_log_get_level(&level), OAKCOMMON_OK); + EXPECT_EQ(level, OAKCOMMON_DEBUG_WARNING); + + ASSERT_EQ(oakcommon_log_set_level(OAKCOMMON_DEBUG_DEBUG), + OAKCOMMON_OK); + ASSERT_EQ(oakcommon_log_get_level(&level), OAKCOMMON_OK); + EXPECT_EQ(level, OAKCOMMON_DEBUG_DEBUG); + + // Restore default for other tests. + ASSERT_EQ(oakcommon_log_set_level(OAKCOMMON_DEBUG_INFO), OAKCOMMON_OK); +} + +TEST(OakLog, LevelSetGetInvalidArgs) +{ + EXPECT_EQ(oakcommon_log_set_level(-1), OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_log_set_level(999), OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_log_get_level(nullptr), OAKCOMMON_E_INVALID); + + // The failed sets above must not have changed the level. + int level = -1; + ASSERT_EQ(oakcommon_log_get_level(&level), OAKCOMMON_OK); + EXPECT_EQ(level, OAKCOMMON_DEBUG_INFO); +} + +TEST(OakLog, LevelFilterDropsLowerLevels) +{ + LogCapture capture; + + ASSERT_EQ(oakcommon_log_set_level(OAKCOMMON_DEBUG_WARNING), + OAKCOMMON_OK); + + EXPECT_EQ(oakcommon_log(OAKCOMMON_DEBUG_DEBUG, "dbg"), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_log(OAKCOMMON_DEBUG_INFO, "inf"), OAKCOMMON_OK); + EXPECT_TRUE(capture.lines.empty()); + + EXPECT_EQ(oakcommon_log(OAKCOMMON_DEBUG_WARNING, "wrn"), OAKCOMMON_OK); + EXPECT_EQ(oakcommon_log(OAKCOMMON_DEBUG_ERROR, "err"), OAKCOMMON_OK); + ASSERT_EQ(capture.lines.size(), 2u); + EXPECT_EQ(capture.lines[0], "[WARNING] wrn\n"); + EXPECT_EQ(capture.lines[1], "[ERROR] err\n"); +} + +TEST(OakLog, DefaultLevelIsInfo) +{ + // The default filter is INFO: DEBUG is dropped, INFO passes. + LogCapture capture; + + olive::log_debug("invisible"); + olive::log_info("visible"); + ASSERT_EQ(capture.lines.size(), 1u); + EXPECT_EQ(capture.lines[0], "[INFO] visible\n"); +} + +TEST(OakLog, ConvenienceWrappersUseTheirLevels) +{ + LogCapture capture; + olive::set_log_level(olive::k_debug_debug); + + olive::log_debug("d"); + olive::log_info("i"); + olive::log_warning("w"); + olive::log_critical("c"); + ASSERT_EQ(capture.lines.size(), 4u); + EXPECT_EQ(capture.lines[0], "[DEBUG] d\n"); + EXPECT_EQ(capture.lines[1], "[INFO] i\n"); + EXPECT_EQ(capture.lines[2], "[WARNING] w\n"); + EXPECT_EQ(capture.lines[3], "[ERROR] c\n"); +} + +TEST(OakLog, PrintfFormatting) +{ + LogCapture capture; + olive::set_log_level(olive::k_debug_debug); + + EXPECT_EQ(oakcommon_log(OAKCOMMON_DEBUG_INFO, "w=%d h=%d name=%s", + 1920, 1080, "clip"), + OAKCOMMON_OK); + ASSERT_EQ(capture.lines.size(), 1u); + EXPECT_EQ(capture.lines[0], "[INFO] w=1920 h=1080 name=clip\n"); +} + +TEST(OakLog, PrintfLongMessageNotTruncated) +{ + LogCapture capture; + + std::string long_msg(100 * 1024, 'x'); + EXPECT_EQ(oakcommon_log(OAKCOMMON_DEBUG_WARNING, "%s", + long_msg.c_str()), + OAKCOMMON_OK); + ASSERT_EQ(capture.lines.size(), 1u); + EXPECT_EQ(capture.lines[0], + "[WARNING] " + long_msg + "\n"); +} + +TEST(OakLog, PrintfNullFormat) +{ + EXPECT_EQ(oakcommon_log(OAKCOMMON_DEBUG_WARNING, nullptr), + OAKCOMMON_E_INVALID); +} + +TEST(OakLog, OutOfRangeLevelPrintsUnknown) +{ + LogCapture capture; + olive::set_log_level(olive::k_debug_debug); + + // Out-of-range levels are tolerated and print as UNKNOWN (same as + // oakcommon_debug_log). + EXPECT_EQ(oakcommon_log(999, "odd"), OAKCOMMON_OK); + ASSERT_EQ(capture.lines.size(), 1u); + EXPECT_EQ(capture.lines[0], "[UNKNOWN] odd\n"); +} diff --git a/src/common/tests/ocioutils_test.cpp b/src/common/tests/ocioutils_test.cpp index bf204f230..46430445d 100644 --- a/src/common/tests/ocioutils_test.cpp +++ b/src/common/tests/ocioutils_test.cpp @@ -34,9 +34,9 @@ namespace ocio = OCIO_NAMESPACE; TEST(OCIOUtilsCApi, InitReturnsHandle) { - OakCommonOCIOUtils *utils = oakcommon_ocioutils_init(); - ASSERT_NE(utils, nullptr); - oakcommon_ocioutils_free(utils); + OakOCIOUtils utils = oakcommon_ocioutils_init(); + ASSERT_NE(utils.ctx, nullptr); + oakcommon_ocioutils_free(&utils); } TEST(OCIOUtilsCApi, FreeNullIsNoOp) @@ -46,8 +46,8 @@ TEST(OCIOUtilsCApi, FreeNullIsNoOp) TEST(OCIOUtilsCApi, BitDepthMappingMatchesOCIO) { - OakCommonOCIOUtils *utils = oakcommon_ocioutils_init(); - ASSERT_NE(utils, nullptr); + OakOCIOUtils utils = oakcommon_ocioutils_init(); + ASSERT_NE(utils.ctx, nullptr); const struct { int pixel_format; @@ -68,13 +68,13 @@ TEST(OCIOUtilsCApi, BitDepthMappingMatchesOCIO) EXPECT_EQ(depth, static_cast(c.expected)); } - oakcommon_ocioutils_free(utils); + oakcommon_ocioutils_free(&utils); } TEST(OCIOUtilsCApi, InvalidFormatYieldsUnknownDepth) { - OakCommonOCIOUtils *utils = oakcommon_ocioutils_init(); - ASSERT_NE(utils, nullptr); + OakOCIOUtils utils = oakcommon_ocioutils_init(); + ASSERT_NE(utils.ctx, nullptr); int depth = -1; EXPECT_EQ(oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format( @@ -82,33 +82,32 @@ TEST(OCIOUtilsCApi, InvalidFormatYieldsUnknownDepth) OAKCOMMON_OK); EXPECT_EQ(depth, static_cast(ocio::BIT_DEPTH_UNKNOWN)); - oakcommon_ocioutils_free(utils); + oakcommon_ocioutils_free(&utils); } TEST(OCIOUtilsCApi, NullHandleReturnsInvalid) { int depth = 0; - EXPECT_EQ(oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format( - NULL, OAKCOMMON_PIXEL_FORMAT_U8, &depth), + EXPECT_EQ(oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format(OakOCIOUtils{}, OAKCOMMON_PIXEL_FORMAT_U8, &depth), OAKCOMMON_E_INVALID); } TEST(OCIOUtilsCApi, NullOutParamReturnsInvalid) { - OakCommonOCIOUtils *utils = oakcommon_ocioutils_init(); - ASSERT_NE(utils, nullptr); + OakOCIOUtils utils = oakcommon_ocioutils_init(); + ASSERT_NE(utils.ctx, nullptr); EXPECT_EQ(oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format( utils, OAKCOMMON_PIXEL_FORMAT_U8, NULL), OAKCOMMON_E_INVALID); - oakcommon_ocioutils_free(utils); + oakcommon_ocioutils_free(&utils); } TEST(OCIOUtilsCApi, OutOfRangeFormatReturnsInvalid) { - OakCommonOCIOUtils *utils = oakcommon_ocioutils_init(); - ASSERT_NE(utils, nullptr); + OakOCIOUtils utils = oakcommon_ocioutils_init(); + ASSERT_NE(utils.ctx, nullptr); int depth = 0; EXPECT_EQ(oakcommon_ocioutils_get_ocio_bit_depth_from_pixel_format( @@ -118,5 +117,5 @@ TEST(OCIOUtilsCApi, OutOfRangeFormatReturnsInvalid) utils, -2, &depth), OAKCOMMON_E_INVALID); - oakcommon_ocioutils_free(utils); + oakcommon_ocioutils_free(&utils); } diff --git a/src/common/tests/oiioutils_test.cpp b/src/common/tests/oiioutils_test.cpp index ac0678af3..ae61fdfad 100644 --- a/src/common/tests/oiioutils_test.cpp +++ b/src/common/tests/oiioutils_test.cpp @@ -37,9 +37,9 @@ TEST(OIIOUtilsCApi, InitReturnsHandle) { - OakCommonOIIOUtils *utils = oakcommon_oiioutils_init(); - ASSERT_NE(utils, nullptr); - oakcommon_oiioutils_free(utils); + OakOIIOUtils utils = oakcommon_oiioutils_init(); + ASSERT_NE(utils.ctx, nullptr); + oakcommon_oiioutils_free(&utils); } TEST(OIIOUtilsCApi, FreeNullIsNoOp) @@ -49,8 +49,8 @@ TEST(OIIOUtilsCApi, FreeNullIsNoOp) TEST(OIIOUtilsCApi, BaseTypeFromPixelFormat) { - OakCommonOIIOUtils *utils = oakcommon_oiioutils_init(); - ASSERT_NE(utils, nullptr); + OakOIIOUtils utils = oakcommon_oiioutils_init(); + ASSERT_NE(utils.ctx, nullptr); const struct { int pixel_format; @@ -72,17 +72,16 @@ TEST(OIIOUtilsCApi, BaseTypeFromPixelFormat) EXPECT_EQ(base_type, c.expected_base_type); } - oakcommon_oiioutils_free(utils); + oakcommon_oiioutils_free(&utils); } TEST(OIIOUtilsCApi, BaseTypeFromPixelFormatRejectsBadArgs) { - OakCommonOIIOUtils *utils = oakcommon_oiioutils_init(); - ASSERT_NE(utils, nullptr); + OakOIIOUtils utils = oakcommon_oiioutils_init(); + ASSERT_NE(utils.ctx, nullptr); int base_type = 0; - EXPECT_EQ(oakcommon_oiioutils_get_oiio_base_type_from_format( - NULL, OAKCOMMON_PIXEL_FORMAT_U8, &base_type), + EXPECT_EQ(oakcommon_oiioutils_get_oiio_base_type_from_format(OakOIIOUtils{}, OAKCOMMON_PIXEL_FORMAT_U8, &base_type), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_oiioutils_get_oiio_base_type_from_format( utils, OAKCOMMON_PIXEL_FORMAT_U8, NULL), @@ -94,13 +93,13 @@ TEST(OIIOUtilsCApi, BaseTypeFromPixelFormatRejectsBadArgs) utils, -2, &base_type), OAKCOMMON_E_INVALID); - oakcommon_oiioutils_free(utils); + oakcommon_oiioutils_free(&utils); } TEST(OIIOUtilsCApi, PixelFormatFromBaseType) { - OakCommonOIIOUtils *utils = oakcommon_oiioutils_init(); - ASSERT_NE(utils, nullptr); + OakOIIOUtils utils = oakcommon_oiioutils_init(); + ASSERT_NE(utils.ctx, nullptr); const struct { int base_type; @@ -123,17 +122,16 @@ TEST(OIIOUtilsCApi, PixelFormatFromBaseType) EXPECT_EQ(pixel_format, c.expected_pixel_format); } - oakcommon_oiioutils_free(utils); + oakcommon_oiioutils_free(&utils); } TEST(OIIOUtilsCApi, PixelFormatFromBaseTypeRejectsBadArgs) { - OakCommonOIIOUtils *utils = oakcommon_oiioutils_init(); - ASSERT_NE(utils, nullptr); + OakOIIOUtils utils = oakcommon_oiioutils_init(); + ASSERT_NE(utils.ctx, nullptr); int pixel_format = 0; - EXPECT_EQ(oakcommon_oiioutils_get_format_from_oiio_basetype( - NULL, 2, &pixel_format), + EXPECT_EQ(oakcommon_oiioutils_get_format_from_oiio_basetype(OakOIIOUtils{}, 2, &pixel_format), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_oiioutils_get_format_from_oiio_basetype( utils, 2, NULL), @@ -145,13 +143,13 @@ TEST(OIIOUtilsCApi, PixelFormatFromBaseTypeRejectsBadArgs) utils, 100, &pixel_format), OAKCOMMON_E_INVALID); /* >= LASTBASE */ - oakcommon_oiioutils_free(utils); + oakcommon_oiioutils_free(&utils); } TEST(OIIOUtilsCApi, PixelAspectRatioConvertsToRational) { - OakCommonOIIOUtils *utils = oakcommon_oiioutils_init(); - ASSERT_NE(utils, nullptr); + OakOIIOUtils utils = oakcommon_oiioutils_init(); + ASSERT_NE(utils.ctx, nullptr); int num = 0; int den = 0; @@ -167,17 +165,17 @@ TEST(OIIOUtilsCApi, PixelAspectRatioConvertsToRational) ASSERT_NE(den, 0); EXPECT_NEAR(static_cast(num) / den, 1.5, 1e-9); - oakcommon_oiioutils_free(utils); + oakcommon_oiioutils_free(&utils); } TEST(OIIOUtilsCApi, PixelAspectRatioRejectsBadArgs) { - OakCommonOIIOUtils *utils = oakcommon_oiioutils_init(); - ASSERT_NE(utils, nullptr); + OakOIIOUtils utils = oakcommon_oiioutils_init(); + ASSERT_NE(utils.ctx, nullptr); int num = 0; int den = 0; - EXPECT_EQ(oakcommon_oiioutils_get_pixel_aspect_ratio(NULL, 1.0, &num, + EXPECT_EQ(oakcommon_oiioutils_get_pixel_aspect_ratio(OakOIIOUtils{}, 1.0, &num, &den), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_oiioutils_get_pixel_aspect_ratio(utils, 1.0, NULL, @@ -187,5 +185,5 @@ TEST(OIIOUtilsCApi, PixelAspectRatioRejectsBadArgs) NULL), OAKCOMMON_E_INVALID); - oakcommon_oiioutils_free(utils); + oakcommon_oiioutils_free(&utils); } diff --git a/src/common/tests/subtitleparams_test.cpp b/src/common/tests/subtitleparams_test.cpp index ca2e8711e..981f5ef01 100644 --- a/src/common/tests/subtitleparams_test.cpp +++ b/src/common/tests/subtitleparams_test.cpp @@ -30,8 +30,8 @@ namespace { std::string read_indexed_string( - int (*fn)(OakCommonSubtitleParams *, int, char *, int), - OakCommonSubtitleParams *p, int index) + int (*fn)(OakSubtitleParams, int, char *, int), + OakSubtitleParams p, int index) { int needed = fn(p, index, nullptr, 0); EXPECT_GT(needed, 0); @@ -53,8 +53,8 @@ std::string read_static_string(int (*fn)(char *, int)) TEST(CommonSubtitleParamsCApi, InitFree) { - OakCommonSubtitleParams *p = oakcommon_subtitleparams_init(); - ASSERT_NE(p, nullptr); + OakSubtitleParams p = oakcommon_subtitleparams_init(); + ASSERT_NE(p.ctx, nullptr); int index = -1; EXPECT_EQ(oakcommon_subtitleparams_get_stream_index(p, &index), @@ -66,7 +66,7 @@ TEST(CommonSubtitleParamsCApi, InitFree) OAKCOMMON_OK); EXPECT_EQ(enabled, 1); - oakcommon_subtitleparams_free(p); + oakcommon_subtitleparams_free(&p); } TEST(CommonSubtitleParamsCApi, FreeNull) @@ -76,48 +76,48 @@ TEST(CommonSubtitleParamsCApi, FreeNull) TEST(CommonSubtitleParamsCApi, SetStreamIndex) { - OakCommonSubtitleParams *p = oakcommon_subtitleparams_init(); - ASSERT_NE(p, nullptr); + OakSubtitleParams p = oakcommon_subtitleparams_init(); + ASSERT_NE(p.ctx, nullptr); EXPECT_EQ(oakcommon_subtitleparams_set_stream_index(p, 3), OAKCOMMON_OK); int index = 0; EXPECT_EQ(oakcommon_subtitleparams_get_stream_index(p, &index), OAKCOMMON_OK); EXPECT_EQ(index, 3); - oakcommon_subtitleparams_free(p); + oakcommon_subtitleparams_free(&p); } TEST(CommonSubtitleParamsCApi, SetStreamIndexNullHandle) { - EXPECT_EQ(oakcommon_subtitleparams_set_stream_index(nullptr, 3), + EXPECT_EQ(oakcommon_subtitleparams_set_stream_index(OakSubtitleParams{}, 3), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_subtitleparams_get_stream_index(nullptr, nullptr), + EXPECT_EQ(oakcommon_subtitleparams_get_stream_index(OakSubtitleParams{}, nullptr), OAKCOMMON_E_INVALID); } TEST(CommonSubtitleParamsCApi, SetEnabled) { - OakCommonSubtitleParams *p = oakcommon_subtitleparams_init(); - ASSERT_NE(p, nullptr); + OakSubtitleParams p = oakcommon_subtitleparams_init(); + ASSERT_NE(p.ctx, nullptr); EXPECT_EQ(oakcommon_subtitleparams_set_enabled(p, 0), OAKCOMMON_OK); int enabled = 1; EXPECT_EQ(oakcommon_subtitleparams_get_enabled(p, &enabled), OAKCOMMON_OK); EXPECT_EQ(enabled, 0); - oakcommon_subtitleparams_free(p); + oakcommon_subtitleparams_free(&p); } TEST(CommonSubtitleParamsCApi, SetEnabledNullHandle) { - EXPECT_EQ(oakcommon_subtitleparams_set_enabled(nullptr, 0), + EXPECT_EQ(oakcommon_subtitleparams_set_enabled(OakSubtitleParams{}, 0), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_subtitleparams_get_enabled(nullptr, nullptr), + EXPECT_EQ(oakcommon_subtitleparams_get_enabled(OakSubtitleParams{}, nullptr), OAKCOMMON_E_INVALID); } TEST(CommonSubtitleParamsCApi, AddAndReadSubtitles) { - OakCommonSubtitleParams *p = oakcommon_subtitleparams_init(); - ASSERT_NE(p, nullptr); + OakSubtitleParams p = oakcommon_subtitleparams_init(); + ASSERT_NE(p.ctx, nullptr); int is_valid = 1; EXPECT_EQ(oakcommon_subtitleparams_is_valid(p, &is_valid), OAKCOMMON_OK); @@ -156,40 +156,40 @@ TEST(CommonSubtitleParamsCApi, AddAndReadSubtitles) EXPECT_EQ(oakcommon_subtitleparams_count(p, &count), OAKCOMMON_OK); EXPECT_EQ(count, 0); - oakcommon_subtitleparams_free(p); + oakcommon_subtitleparams_free(&p); } TEST(CommonSubtitleParamsCApi, SubtitleErrorPaths) { - OakCommonSubtitleParams *p = oakcommon_subtitleparams_init(); - ASSERT_NE(p, nullptr); + OakSubtitleParams p = oakcommon_subtitleparams_init(); + ASSERT_NE(p.ctx, nullptr); int i = 0; char buf[16]; - EXPECT_EQ(oakcommon_subtitleparams_add_subtitle(nullptr, 0, 1, 1, 1, "x"), + EXPECT_EQ(oakcommon_subtitleparams_add_subtitle(OakSubtitleParams{}, 0, 1, 1, 1, "x"), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_subtitleparams_add_subtitle(p, 0, 1, 1, 1, nullptr), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_subtitleparams_get_subtitle(p, 0, &i, &i, &i, &i), OAKCOMMON_E_NOT_FOUND); - EXPECT_EQ(oakcommon_subtitleparams_get_subtitle(nullptr, 0, &i, &i, &i, + EXPECT_EQ(oakcommon_subtitleparams_get_subtitle(OakSubtitleParams{}, 0, &i, &i, &i, &i), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_subtitleparams_get_subtitle_text(p, 5, buf, sizeof(buf)), OAKCOMMON_E_NOT_FOUND); - EXPECT_EQ(oakcommon_subtitleparams_get_subtitle_text(nullptr, 0, buf, + EXPECT_EQ(oakcommon_subtitleparams_get_subtitle_text(OakSubtitleParams{}, 0, buf, sizeof(buf)), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_subtitleparams_is_valid(nullptr, &i), + EXPECT_EQ(oakcommon_subtitleparams_is_valid(OakSubtitleParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_subtitleparams_count(nullptr, &i), + EXPECT_EQ(oakcommon_subtitleparams_count(OakSubtitleParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_subtitleparams_duration(nullptr, &i, &i), + EXPECT_EQ(oakcommon_subtitleparams_duration(OakSubtitleParams{}, &i, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_subtitleparams_clear(nullptr), OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_subtitleparams_clear(OakSubtitleParams{}), OAKCOMMON_E_INVALID); - oakcommon_subtitleparams_free(p); + oakcommon_subtitleparams_free(&p); } TEST(CommonSubtitleParamsCApi, GenerateAssHeader) @@ -203,8 +203,8 @@ TEST(CommonSubtitleParamsCApi, GenerateAssHeader) TEST(CommonSubtitleParamsCApi, XmlRoundTrip) { - OakCommonSubtitleParams *p = oakcommon_subtitleparams_init(); - ASSERT_NE(p, nullptr); + OakSubtitleParams p = oakcommon_subtitleparams_init(); + ASSERT_NE(p.ctx, nullptr); EXPECT_EQ(oakcommon_subtitleparams_set_stream_index(p, 2), OAKCOMMON_OK); EXPECT_EQ(oakcommon_subtitleparams_add_subtitle(p, 1, 25, 2, 25, "First "), @@ -215,8 +215,8 @@ TEST(CommonSubtitleParamsCApi, XmlRoundTrip) std::vector buf(needed); ASSERT_EQ(oakcommon_subtitleparams_save_xml(p, buf.data(), needed), needed); - OakCommonSubtitleParams *q = oakcommon_subtitleparams_init(); - ASSERT_NE(q, nullptr); + OakSubtitleParams q = oakcommon_subtitleparams_init(); + ASSERT_NE(q.ctx, nullptr); ASSERT_EQ(oakcommon_subtitleparams_load_xml(q, buf.data()), OAKCOMMON_OK); int index = 0; @@ -230,22 +230,22 @@ TEST(CommonSubtitleParamsCApi, XmlRoundTrip) oakcommon_subtitleparams_get_subtitle_text, q, 0), "First "); - oakcommon_subtitleparams_free(p); - oakcommon_subtitleparams_free(q); + oakcommon_subtitleparams_free(&p); + oakcommon_subtitleparams_free(&q); } TEST(CommonSubtitleParamsCApi, XmlErrorPaths) { - OakCommonSubtitleParams *p = oakcommon_subtitleparams_init(); - ASSERT_NE(p, nullptr); + OakSubtitleParams p = oakcommon_subtitleparams_init(); + ASSERT_NE(p.ctx, nullptr); char buf[16]; - EXPECT_EQ(oakcommon_subtitleparams_load_xml(nullptr, ""), + EXPECT_EQ(oakcommon_subtitleparams_load_xml(OakSubtitleParams{}, ""), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_subtitleparams_load_xml(p, nullptr), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_subtitleparams_load_xml(p, "not xml"), OAKCOMMON_E_FAILED); - EXPECT_EQ(oakcommon_subtitleparams_save_xml(nullptr, buf, sizeof(buf)), + EXPECT_EQ(oakcommon_subtitleparams_save_xml(OakSubtitleParams{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); - oakcommon_subtitleparams_free(p); + oakcommon_subtitleparams_free(&p); } diff --git a/src/common/tests/videoparams_test.cpp b/src/common/tests/videoparams_test.cpp index 2d3bf20d8..8f31df58b 100644 --- a/src/common/tests/videoparams_test.cpp +++ b/src/common/tests/videoparams_test.cpp @@ -29,8 +29,8 @@ TEST(CommonVideoParamsCApi, InitDefaults) { - OakCommonVideoParams *p = oakcommon_videoparams_init(); - ASSERT_NE(p, nullptr); + OakVideoParams p = oakcommon_videoparams_init(); + ASSERT_NE(p.ctx, nullptr); int v = -1; EXPECT_EQ(oakcommon_videoparams_get_width(p, &v), OAKCOMMON_OK); @@ -38,7 +38,7 @@ TEST(CommonVideoParamsCApi, InitDefaults) EXPECT_EQ(oakcommon_videoparams_get_is_valid(p, &v), OAKCOMMON_OK); EXPECT_EQ(v, 0); - oakcommon_videoparams_free(p); + oakcommon_videoparams_free(&p); } TEST(CommonVideoParamsCApi, FreeNull) @@ -48,10 +48,10 @@ TEST(CommonVideoParamsCApi, FreeNull) TEST(CommonVideoParamsCApi, InitBasic) { - OakCommonVideoParams *p = oakcommon_videoparams_init_basic( + OakVideoParams p = oakcommon_videoparams_init_basic( 1920, 1080, OAKCOMMON_PIXEL_FORMAT_U8, 4, 1, 1, OAKCOMMON_VIDEO_INTERLACE_NONE, 2); - ASSERT_NE(p, nullptr); + ASSERT_NE(p.ctx, nullptr); int v; EXPECT_EQ(oakcommon_videoparams_get_width(p, &v), OAKCOMMON_OK); @@ -91,15 +91,15 @@ TEST(CommonVideoParamsCApi, InitBasic) EXPECT_EQ(oakcommon_videoparams_get_buffer_size(p, &v), OAKCOMMON_OK); EXPECT_EQ(v, 1920 * 1080 * 4); - oakcommon_videoparams_free(p); + oakcommon_videoparams_free(&p); } TEST(CommonVideoParamsCApi, InitWithTimeBase) { - OakCommonVideoParams *p = oakcommon_videoparams_init_with_time_base( + OakVideoParams p = oakcommon_videoparams_init_with_time_base( 1280, 720, 1001, 30000, OAKCOMMON_PIXEL_FORMAT_F32, 4, 1, 1, OAKCOMMON_VIDEO_INTERLACE_NONE, 1); - ASSERT_NE(p, nullptr); + ASSERT_NE(p.ctx, nullptr); int num, den; EXPECT_EQ(oakcommon_videoparams_get_time_base(p, &num, &den), @@ -117,13 +117,13 @@ TEST(CommonVideoParamsCApi, InitWithTimeBase) EXPECT_EQ(num, 1001); EXPECT_EQ(den, 30000); - oakcommon_videoparams_free(p); + oakcommon_videoparams_free(&p); } TEST(CommonVideoParamsCApi, ScalarSetters) { - OakCommonVideoParams *p = oakcommon_videoparams_init(); - ASSERT_NE(p, nullptr); + OakVideoParams p = oakcommon_videoparams_init(); + ASSERT_NE(p.ctx, nullptr); int v; EXPECT_EQ(oakcommon_videoparams_set_width(p, 640), OAKCOMMON_OK); @@ -205,7 +205,7 @@ TEST(CommonVideoParamsCApi, ScalarSetters) EXPECT_EQ(oakcommon_videoparams_get_effective_depth(p, &v), OAKCOMMON_OK); EXPECT_EQ(v, 1); - oakcommon_videoparams_free(p); + oakcommon_videoparams_free(&p); } TEST(CommonVideoParamsCApi, NullHandleErrors) @@ -214,123 +214,123 @@ TEST(CommonVideoParamsCApi, NullHandleErrors) float f; int64_t i64; char buf[16]; - EXPECT_EQ(oakcommon_videoparams_get_width(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_width(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_width(nullptr, 1), + EXPECT_EQ(oakcommon_videoparams_set_width(OakVideoParams{}, 1), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_height(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_height(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_height(nullptr, 1), + EXPECT_EQ(oakcommon_videoparams_set_height(OakVideoParams{}, 1), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_depth(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_depth(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_depth(nullptr, 1), + EXPECT_EQ(oakcommon_videoparams_set_depth(OakVideoParams{}, 1), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_is_3d(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_is_3d(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_time_base(nullptr, &i, &i), + EXPECT_EQ(oakcommon_videoparams_get_time_base(OakVideoParams{}, &i, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_time_base(nullptr, 1, 1), + EXPECT_EQ(oakcommon_videoparams_set_time_base(OakVideoParams{}, 1, 1), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_frame_rate(nullptr, &i, &i), + EXPECT_EQ(oakcommon_videoparams_get_frame_rate(OakVideoParams{}, &i, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_frame_rate(nullptr, 1, 1), + EXPECT_EQ(oakcommon_videoparams_set_frame_rate(OakVideoParams{}, 1, 1), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_frame_rate_as_time_base(nullptr, &i, &i), + EXPECT_EQ(oakcommon_videoparams_frame_rate_as_time_base(OakVideoParams{}, &i, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_pixel_aspect_ratio(nullptr, &i, &i), + EXPECT_EQ(oakcommon_videoparams_get_pixel_aspect_ratio(OakVideoParams{}, &i, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_pixel_aspect_ratio(nullptr, 1, 1), + EXPECT_EQ(oakcommon_videoparams_set_pixel_aspect_ratio(OakVideoParams{}, 1, 1), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_format(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_format(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_format(nullptr, 0), + EXPECT_EQ(oakcommon_videoparams_set_format(OakVideoParams{}, 0), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_channel_count(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_channel_count(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_channel_count(nullptr, 1), + EXPECT_EQ(oakcommon_videoparams_set_channel_count(OakVideoParams{}, 1), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_interlacing(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_interlacing(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_interlacing(nullptr, 0), + EXPECT_EQ(oakcommon_videoparams_set_interlacing(OakVideoParams{}, 0), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_divider(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_divider(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_divider(nullptr, 1), + EXPECT_EQ(oakcommon_videoparams_set_divider(OakVideoParams{}, 1), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_enabled(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_enabled(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_enabled(nullptr, 1), + EXPECT_EQ(oakcommon_videoparams_set_enabled(OakVideoParams{}, 1), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_x(nullptr, &f), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_x(nullptr, 0.0f), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_y(nullptr, &f), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_y(nullptr, 0.0f), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_stream_index(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_x(OakVideoParams{}, &f), OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_x(OakVideoParams{}, 0.0f), OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_y(OakVideoParams{}, &f), OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_set_y(OakVideoParams{}, 0.0f), OAKCOMMON_E_INVALID); + EXPECT_EQ(oakcommon_videoparams_get_stream_index(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_stream_index(nullptr, 0), + EXPECT_EQ(oakcommon_videoparams_set_stream_index(OakVideoParams{}, 0), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_video_type(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_video_type(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_video_type(nullptr, 0), + EXPECT_EQ(oakcommon_videoparams_set_video_type(OakVideoParams{}, 0), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_start_time(nullptr, &i64), + EXPECT_EQ(oakcommon_videoparams_get_start_time(OakVideoParams{}, &i64), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_start_time(nullptr, 0), + EXPECT_EQ(oakcommon_videoparams_set_start_time(OakVideoParams{}, 0), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_duration(nullptr, &i64), + EXPECT_EQ(oakcommon_videoparams_get_duration(OakVideoParams{}, &i64), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_duration(nullptr, 0), + EXPECT_EQ(oakcommon_videoparams_set_duration(OakVideoParams{}, 0), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_premultiplied_alpha(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_premultiplied_alpha(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_premultiplied_alpha(nullptr, 0), + EXPECT_EQ(oakcommon_videoparams_set_premultiplied_alpha(OakVideoParams{}, 0), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_color_range(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_color_range(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_color_range(nullptr, 0), + EXPECT_EQ(oakcommon_videoparams_set_color_range(OakVideoParams{}, 0), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_color_primaries(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_color_primaries(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_color_primaries(nullptr, 0), + EXPECT_EQ(oakcommon_videoparams_set_color_primaries(OakVideoParams{}, 0), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_color_transfer(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_color_transfer(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_color_transfer(nullptr, 0), + EXPECT_EQ(oakcommon_videoparams_set_color_transfer(OakVideoParams{}, 0), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_colorspace(nullptr, buf, sizeof(buf)), + EXPECT_EQ(oakcommon_videoparams_get_colorspace(OakVideoParams{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_set_colorspace(nullptr, "x"), + EXPECT_EQ(oakcommon_videoparams_set_colorspace(OakVideoParams{}, "x"), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_square_pixel_width(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_square_pixel_width(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_effective_width(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_effective_width(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_effective_height(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_effective_height(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_effective_depth(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_effective_depth(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_is_valid(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_is_valid(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_bytes_per_channel(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_bytes_per_channel(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_bytes_per_pixel(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_bytes_per_pixel(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_buffer_size(nullptr, &i), + EXPECT_EQ(oakcommon_videoparams_get_buffer_size(OakVideoParams{}, &i), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_get_time_in_timebase_units(nullptr, 0, 1, + EXPECT_EQ(oakcommon_videoparams_get_time_in_timebase_units(OakVideoParams{}, 0, 1, &i64), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_load_xml(nullptr, ""), + EXPECT_EQ(oakcommon_videoparams_load_xml(OakVideoParams{}, ""), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_save_xml(nullptr, buf, sizeof(buf)), + EXPECT_EQ(oakcommon_videoparams_save_xml(OakVideoParams{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); } TEST(CommonVideoParamsCApi, NullOutParamErrors) { - OakCommonVideoParams *p = oakcommon_videoparams_init(); - ASSERT_NE(p, nullptr); + OakVideoParams p = oakcommon_videoparams_init(); + ASSERT_NE(p.ctx, nullptr); EXPECT_EQ(oakcommon_videoparams_get_width(p, nullptr), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_videoparams_get_time_base(p, nullptr, nullptr), @@ -340,13 +340,13 @@ TEST(CommonVideoParamsCApi, NullOutParamErrors) EXPECT_EQ(oakcommon_videoparams_load_xml(p, nullptr), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_videoparams_load_xml(p, "not xml"), OAKCOMMON_E_FAILED); - oakcommon_videoparams_free(p); + oakcommon_videoparams_free(&p); } TEST(CommonVideoParamsCApi, Colorspace) { - OakCommonVideoParams *p = oakcommon_videoparams_init(); - ASSERT_NE(p, nullptr); + OakVideoParams p = oakcommon_videoparams_init(); + ASSERT_NE(p.ctx, nullptr); EXPECT_EQ(oakcommon_videoparams_set_colorspace(p, "rec709"), OAKCOMMON_OK); int needed = oakcommon_videoparams_get_colorspace(p, nullptr, 0); @@ -355,15 +355,15 @@ TEST(CommonVideoParamsCApi, Colorspace) EXPECT_EQ(oakcommon_videoparams_get_colorspace(p, buf.data(), needed), needed); EXPECT_STREQ(buf.data(), "rec709"); - oakcommon_videoparams_free(p); + oakcommon_videoparams_free(&p); } TEST(CommonVideoParamsCApi, TimeInTimebaseUnits) { - OakCommonVideoParams *p = oakcommon_videoparams_init_with_time_base( + OakVideoParams p = oakcommon_videoparams_init_with_time_base( 1920, 1080, 1, 25, OAKCOMMON_PIXEL_FORMAT_U8, 4, 1, 1, OAKCOMMON_VIDEO_INTERLACE_NONE, 1); - ASSERT_NE(p, nullptr); + ASSERT_NE(p.ctx, nullptr); int64_t ts = -1; EXPECT_EQ(oakcommon_videoparams_get_time_in_timebase_units(p, 2, 1, &ts), @@ -371,26 +371,26 @@ TEST(CommonVideoParamsCApi, TimeInTimebaseUnits) EXPECT_EQ(ts, 50); // 2 seconds at 25 fps // Without a time base the result is AV_NOPTS_VALUE - OakCommonVideoParams *q = oakcommon_videoparams_init(); - ASSERT_NE(q, nullptr); + OakVideoParams q = oakcommon_videoparams_init(); + ASSERT_NE(q.ctx, nullptr); EXPECT_EQ(oakcommon_videoparams_get_time_in_timebase_units(q, 2, 1, &ts), OAKCOMMON_OK); EXPECT_EQ(ts, INT64_MIN); - oakcommon_videoparams_free(p); - oakcommon_videoparams_free(q); + oakcommon_videoparams_free(&p); + oakcommon_videoparams_free(&q); } TEST(CommonVideoParamsCApi, Equals) { - OakCommonVideoParams *a = oakcommon_videoparams_init_basic( + OakVideoParams a = oakcommon_videoparams_init_basic( 1920, 1080, OAKCOMMON_PIXEL_FORMAT_U8, 4, 1, 1, OAKCOMMON_VIDEO_INTERLACE_NONE, 1); - OakCommonVideoParams *b = oakcommon_videoparams_init_basic( + OakVideoParams b = oakcommon_videoparams_init_basic( 1920, 1080, OAKCOMMON_PIXEL_FORMAT_U8, 4, 1, 1, OAKCOMMON_VIDEO_INTERLACE_NONE, 1); - ASSERT_NE(a, nullptr); - ASSERT_NE(b, nullptr); + ASSERT_NE(a.ctx, nullptr); + ASSERT_NE(b.ctx, nullptr); int equal = 0; EXPECT_EQ(oakcommon_videoparams_equals(a, b, &equal), OAKCOMMON_OK); @@ -400,23 +400,23 @@ TEST(CommonVideoParamsCApi, Equals) EXPECT_EQ(oakcommon_videoparams_equals(a, b, &equal), OAKCOMMON_OK); EXPECT_EQ(equal, 0); - EXPECT_EQ(oakcommon_videoparams_equals(nullptr, b, &equal), + EXPECT_EQ(oakcommon_videoparams_equals(OakVideoParams{}, b, &equal), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_videoparams_equals(a, nullptr, &equal), + EXPECT_EQ(oakcommon_videoparams_equals(a, OakVideoParams{}, &equal), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_videoparams_equals(a, b, nullptr), OAKCOMMON_E_INVALID); - oakcommon_videoparams_free(a); - oakcommon_videoparams_free(b); + oakcommon_videoparams_free(&a); + oakcommon_videoparams_free(&b); } TEST(CommonVideoParamsCApi, XmlRoundTrip) { - OakCommonVideoParams *p = oakcommon_videoparams_init_with_time_base( + OakVideoParams p = oakcommon_videoparams_init_with_time_base( 1920, 1080, 1, 25, OAKCOMMON_PIXEL_FORMAT_F16, 4, 4, 3, OAKCOMMON_VIDEO_INTERLACED_BOTTOM_FIRST, 2); - ASSERT_NE(p, nullptr); + ASSERT_NE(p.ctx, nullptr); EXPECT_EQ(oakcommon_videoparams_set_colorspace(p, "rec709"), OAKCOMMON_OK); EXPECT_EQ(oakcommon_videoparams_set_color_primaries(p, 9), OAKCOMMON_OK); @@ -426,8 +426,8 @@ TEST(CommonVideoParamsCApi, XmlRoundTrip) std::vector buf(needed); ASSERT_EQ(oakcommon_videoparams_save_xml(p, buf.data(), needed), needed); - OakCommonVideoParams *q = oakcommon_videoparams_init(); - ASSERT_NE(q, nullptr); + OakVideoParams q = oakcommon_videoparams_init(); + ASSERT_NE(q.ctx, nullptr); ASSERT_EQ(oakcommon_videoparams_load_xml(q, buf.data()), OAKCOMMON_OK); int equal = 0; @@ -450,8 +450,8 @@ TEST(CommonVideoParamsCApi, XmlRoundTrip) EXPECT_EQ(oakcommon_videoparams_get_color_primaries(q, &v), OAKCOMMON_OK); EXPECT_EQ(v, 9); - oakcommon_videoparams_free(p); - oakcommon_videoparams_free(q); + oakcommon_videoparams_free(&p); + oakcommon_videoparams_free(&q); } TEST(CommonVideoParamsCApi, StaticHelpers) diff --git a/src/common/tests/xmlutils_test.cpp b/src/common/tests/xmlutils_test.cpp index 42be51d26..abace915d 100644 --- a/src/common/tests/xmlutils_test.cpp +++ b/src/common/tests/xmlutils_test.cpp @@ -29,8 +29,8 @@ namespace { -std::string read_string(int (*fn)(OakCommonXmlReader *, char *, int), - OakCommonXmlReader *reader) +std::string read_string(int (*fn)(OakXmlReader, char *, int), + OakXmlReader reader) { int needed = fn(reader, nullptr, 0); EXPECT_GT(needed, 0); @@ -43,7 +43,7 @@ std::string read_string(int (*fn)(OakCommonXmlReader *, char *, int), TEST(CommonXmlUtilsCApi, ReaderInitNullData) { - EXPECT_EQ(oakcommon_xml_reader_init(nullptr), nullptr); + EXPECT_EQ(oakcommon_xml_reader_init(nullptr).ctx, nullptr); } TEST(CommonXmlUtilsCApi, ReaderFreeNull) @@ -53,9 +53,8 @@ TEST(CommonXmlUtilsCApi, ReaderFreeNull) TEST(CommonXmlUtilsCApi, ReadNextStartElement) { - OakCommonXmlReader *r = - oakcommon_xml_reader_init("value"); - ASSERT_NE(r, nullptr); + OakXmlReader r = oakcommon_xml_reader_init("value"); + ASSERT_NE(r.ctx, nullptr); int found = 0; EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), @@ -68,26 +67,26 @@ TEST(CommonXmlUtilsCApi, ReadNextStartElement) EXPECT_EQ(found, 1); EXPECT_EQ(read_string(oakcommon_xml_reader_name, r), "child"); - oakcommon_xml_reader_free(r); + oakcommon_xml_reader_free(&r); } TEST(CommonXmlUtilsCApi, ReadNextStartElementNullHandle) { int found = 0; - EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(nullptr, &found), + EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(OakXmlReader{}, &found), OAKCOMMON_E_INVALID); - OakCommonXmlReader *r = oakcommon_xml_reader_init(""); - ASSERT_NE(r, nullptr); + OakXmlReader r = oakcommon_xml_reader_init(""); + ASSERT_NE(r.ctx, nullptr); EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, nullptr), OAKCOMMON_E_INVALID); - oakcommon_xml_reader_free(r); + oakcommon_xml_reader_free(&r); } TEST(CommonXmlUtilsCApi, ReadNextStartElementReturnsFalseAtEnd) { - OakCommonXmlReader *r = oakcommon_xml_reader_init(""); - ASSERT_NE(r, nullptr); + OakXmlReader r = oakcommon_xml_reader_init(""); + ASSERT_NE(r.ctx, nullptr); int found = -1; EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), @@ -97,21 +96,21 @@ TEST(CommonXmlUtilsCApi, ReadNextStartElementReturnsFalseAtEnd) OAKCOMMON_OK); EXPECT_EQ(found, 0); - oakcommon_xml_reader_free(r); + oakcommon_xml_reader_free(&r); } TEST(CommonXmlUtilsCApi, NameNullHandle) { char buf[16]; - EXPECT_EQ(oakcommon_xml_reader_name(nullptr, buf, sizeof(buf)), + EXPECT_EQ(oakcommon_xml_reader_name(OakXmlReader{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); } TEST(CommonXmlUtilsCApi, ReadElementText) { - OakCommonXmlReader *r = + OakXmlReader r = oakcommon_xml_reader_init("a & b"); - ASSERT_NE(r, nullptr); + ASSERT_NE(r.ctx, nullptr); int found = 0; EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), @@ -122,22 +121,22 @@ TEST(CommonXmlUtilsCApi, ReadElementText) EXPECT_EQ(read_string(oakcommon_xml_reader_read_element_text, r), "a & b"); - oakcommon_xml_reader_free(r); + oakcommon_xml_reader_free(&r); } TEST(CommonXmlUtilsCApi, ReadElementTextNullHandle) { char buf[16]; - EXPECT_EQ(oakcommon_xml_reader_read_element_text(nullptr, buf, + EXPECT_EQ(oakcommon_xml_reader_read_element_text(OakXmlReader{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); } TEST(CommonXmlUtilsCApi, SkipCurrentElement) { - OakCommonXmlReader *r = oakcommon_xml_reader_init( + OakXmlReader r = oakcommon_xml_reader_init( ""); - ASSERT_NE(r, nullptr); + ASSERT_NE(r.ctx, nullptr); int found = 0; EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), @@ -153,20 +152,20 @@ TEST(CommonXmlUtilsCApi, SkipCurrentElement) EXPECT_EQ(found, 1); EXPECT_EQ(read_string(oakcommon_xml_reader_name, r), "known"); - oakcommon_xml_reader_free(r); + oakcommon_xml_reader_free(&r); } TEST(CommonXmlUtilsCApi, SkipCurrentElementNullHandle) { - EXPECT_EQ(oakcommon_xml_reader_skip_current_element(nullptr), + EXPECT_EQ(oakcommon_xml_reader_skip_current_element(OakXmlReader{}), OAKCOMMON_E_INVALID); } TEST(CommonXmlUtilsCApi, Attributes) { - OakCommonXmlReader *r = oakcommon_xml_reader_init( + OakXmlReader r = oakcommon_xml_reader_init( ""); - ASSERT_NE(r, nullptr); + ASSERT_NE(r.ctx, nullptr); int found = 0; EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), @@ -191,16 +190,16 @@ TEST(CommonXmlUtilsCApi, Attributes) 0); EXPECT_STREQ(buf, "a\"b"); - oakcommon_xml_reader_free(r); + oakcommon_xml_reader_free(&r); } TEST(CommonXmlUtilsCApi, AttributeErrorPaths) { - EXPECT_EQ(oakcommon_xml_reader_attribute_count(nullptr, nullptr), + EXPECT_EQ(oakcommon_xml_reader_attribute_count(OakXmlReader{}, nullptr), OAKCOMMON_E_INVALID); - OakCommonXmlReader *r = oakcommon_xml_reader_init(""); - ASSERT_NE(r, nullptr); + OakXmlReader r = oakcommon_xml_reader_init(""); + ASSERT_NE(r.ctx, nullptr); int found = 0; EXPECT_EQ(oakcommon_xml_reader_read_next_start_element(r, &found), OAKCOMMON_OK); @@ -210,30 +209,29 @@ TEST(CommonXmlUtilsCApi, AttributeErrorPaths) OAKCOMMON_E_NOT_FOUND); EXPECT_EQ(oakcommon_xml_reader_attribute_value(r, -1, buf, sizeof(buf)), OAKCOMMON_E_NOT_FOUND); - EXPECT_EQ(oakcommon_xml_reader_attribute_name(nullptr, 0, buf, + EXPECT_EQ(oakcommon_xml_reader_attribute_name(OakXmlReader{}, 0, buf, sizeof(buf)), OAKCOMMON_E_INVALID); - oakcommon_xml_reader_free(r); + oakcommon_xml_reader_free(&r); } TEST(CommonXmlUtilsCApi, HasError) { - OakCommonXmlReader *bad = - oakcommon_xml_reader_init(""); - ASSERT_NE(bad, nullptr); + OakXmlReader bad = oakcommon_xml_reader_init(""); + ASSERT_NE(bad.ctx, nullptr); int has_error = 0; EXPECT_EQ(oakcommon_xml_reader_has_error(bad, &has_error), OAKCOMMON_OK); EXPECT_EQ(has_error, 1); - oakcommon_xml_reader_free(bad); + oakcommon_xml_reader_free(&bad); - OakCommonXmlReader *good = oakcommon_xml_reader_init(""); - ASSERT_NE(good, nullptr); + OakXmlReader good = oakcommon_xml_reader_init(""); + ASSERT_NE(good.ctx, nullptr); EXPECT_EQ(oakcommon_xml_reader_has_error(good, &has_error), OAKCOMMON_OK); EXPECT_EQ(has_error, 0); - EXPECT_EQ(oakcommon_xml_reader_has_error(nullptr, &has_error), + EXPECT_EQ(oakcommon_xml_reader_has_error(OakXmlReader{}, &has_error), OAKCOMMON_E_INVALID); - oakcommon_xml_reader_free(good); + oakcommon_xml_reader_free(&good); } TEST(CommonXmlUtilsCApi, WriterFreeNull) @@ -244,17 +242,17 @@ TEST(CommonXmlUtilsCApi, WriterFreeNull) TEST(CommonXmlUtilsCApi, WriterNullHandleAndArgs) { char buf[16]; - EXPECT_EQ(oakcommon_xml_writer_write_start_element(nullptr, "a"), + EXPECT_EQ(oakcommon_xml_writer_write_start_element(OakXmlWriter{}, "a"), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_xml_writer_write_end_element(nullptr), + EXPECT_EQ(oakcommon_xml_writer_write_end_element(OakXmlWriter{}), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_xml_writer_write_end_document(nullptr), + EXPECT_EQ(oakcommon_xml_writer_write_end_document(OakXmlWriter{}), OAKCOMMON_E_INVALID); - EXPECT_EQ(oakcommon_xml_writer_output(nullptr, buf, sizeof(buf)), + EXPECT_EQ(oakcommon_xml_writer_output(OakXmlWriter{}, buf, sizeof(buf)), OAKCOMMON_E_INVALID); - OakCommonXmlWriter *w = oakcommon_xml_writer_init(); - ASSERT_NE(w, nullptr); + OakXmlWriter w = oakcommon_xml_writer_init(); + ASSERT_NE(w.ctx, nullptr); EXPECT_EQ(oakcommon_xml_writer_write_start_element(w, nullptr), OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_xml_writer_write_attribute(w, "a", nullptr), @@ -263,13 +261,13 @@ TEST(CommonXmlUtilsCApi, WriterNullHandleAndArgs) OAKCOMMON_E_INVALID); EXPECT_EQ(oakcommon_xml_writer_write_text_element(w, nullptr, "x"), OAKCOMMON_E_INVALID); - oakcommon_xml_writer_free(w); + oakcommon_xml_writer_free(&w); } TEST(CommonXmlUtilsCApi, WriterRoundTrip) { - OakCommonXmlWriter *w = oakcommon_xml_writer_init(); - ASSERT_NE(w, nullptr); + OakXmlWriter w = oakcommon_xml_writer_init(); + ASSERT_NE(w.ctx, nullptr); EXPECT_EQ(oakcommon_xml_writer_write_start_element(w, "root"), OAKCOMMON_OK); @@ -287,11 +285,11 @@ TEST(CommonXmlUtilsCApi, WriterRoundTrip) ASSERT_GT(needed, 0); std::vector buf(needed); EXPECT_EQ(oakcommon_xml_writer_output(w, buf.data(), needed), needed); - oakcommon_xml_writer_free(w); + oakcommon_xml_writer_free(&w); // Read the produced document back. - OakCommonXmlReader *r = oakcommon_xml_reader_init(buf.data()); - ASSERT_NE(r, nullptr); + OakXmlReader r = oakcommon_xml_reader_init(buf.data()); + ASSERT_NE(r.ctx, nullptr); int has_error = 1; EXPECT_EQ(oakcommon_xml_reader_has_error(r, &has_error), OAKCOMMON_OK); EXPECT_EQ(has_error, 0); @@ -326,5 +324,5 @@ TEST(CommonXmlUtilsCApi, WriterRoundTrip) OAKCOMMON_OK); EXPECT_EQ(found, 0); - oakcommon_xml_reader_free(r); + oakcommon_xml_reader_free(&r); } diff --git a/src/node/CMakeLists.txt b/src/node/CMakeLists.txt index f41570121..b965d217a 100644 --- a/src/node/CMakeLists.txt +++ b/src/node/CMakeLists.txt @@ -1,6 +1,6 @@ add_subdirectory(src) add_subdirectory(c_api) - +add_subdirectory(wrappers) if(BUILD_TESTS) add_subdirectory(tests) endif() diff --git a/src/node/c_api/colormanager.cpp b/src/node/c_api/colormanager.cpp index 2029bc870..316f78417 100644 --- a/src/node/c_api/colormanager.cpp +++ b/src/node/c_api/colormanager.cpp @@ -28,13 +28,6 @@ #include "colortransform.h" #include "project.h" -// Same handle-echo pattern as sequence.cpp: oakcommon defines -// `struct OakCommonColorTransform { olive::ColorTransform impl; }` -// (src/common/c_api/colortransform.cpp) without exporting the definition. -struct OakCommonColorTransform { - olive::ColorTransform impl; -}; - struct OakNodeColorManager { olive::ColorManager impl; }; @@ -349,21 +342,30 @@ int oaknode_colormanager_get_default_luma_coefs(OakNodeColorManager *manager, } 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) { - if (!manager || !transform || !out) { + if (!manager || !out) { + return OAKNODE_E_INVALID; + } + const olive::ColorTransform *native = + oakcommon_colortransform_get_native(transform); + if (!native) { return OAKNODE_E_INVALID; } if (!has_config(&manager->impl)) { return OAKNODE_E_STATE; } try { - *out = new OakCommonColorTransform{ - manager->impl.get_compliant_color_space(transform->impl, - force_display != 0)}; + const olive::ColorTransform compliant = + manager->impl.get_compliant_color_space(*native, + force_display != 0); + *out = oakcommon_colortransform_init_from_native(&compliant); } catch (...) { return OAKNODE_E_NOMEM; } + if (!out->ctx) { + return OAKNODE_E_NOMEM; + } return OAKNODE_OK; } diff --git a/src/node/c_api/sequence.cpp b/src/node/c_api/sequence.cpp index 28611373e..3204a5328 100644 --- a/src/node/c_api/sequence.cpp +++ b/src/node/c_api/sequence.cpp @@ -28,15 +28,6 @@ #include "project/sequence/sequence.h" #include "videoparams.h" -// oakcommon defines its handle as `struct OakCommonVideoParams { -// olive::VideoParams impl; }` (src/common/c_api/videoparams.cpp) without -// exporting the definition. Echoing the identical layout here is the only -// way to hand native VideoParams values across without a field-by-field -// copy; keep in sync with oakcommon (flagged in the family-C report). -struct OakCommonVideoParams { - olive::VideoParams impl; -}; - namespace { @@ -244,7 +235,7 @@ int oaknode_sequence_get_audio_stream_count(OakNodeSequence *sequence, } int oaknode_sequence_get_video_params(OakNodeSequence *sequence, int index, - OakCommonVideoParams **out) + OakVideoParams *out) { if (!sequence || !out || index < 0) { return OAKNODE_E_INVALID; @@ -253,24 +244,34 @@ int oaknode_sequence_get_video_params(OakNodeSequence *sequence, int index, return OAKNODE_E_NOT_FOUND; } try { - *out = new OakCommonVideoParams{impl(sequence)->get_video_params(index)}; + const olive::VideoParams params = + impl(sequence)->get_video_params(index); + *out = oakcommon_videoparams_init_from_native(¶ms); } catch (...) { return OAKNODE_E_NOMEM; } + if (!out->ctx) { + return OAKNODE_E_NOMEM; + } return OAKNODE_OK; } int oaknode_sequence_set_video_params(OakNodeSequence *sequence, int index, - const OakCommonVideoParams *params) + OakVideoParams params) { - if (!sequence || !params || index < 0) { + if (!sequence || index < 0) { + return OAKNODE_E_INVALID; + } + const olive::VideoParams *native = + oakcommon_videoparams_get_native(params); + if (!native) { return OAKNODE_E_INVALID; } if (index >= impl(sequence)->get_video_stream_count()) { return OAKNODE_E_NOT_FOUND; } try { - impl(sequence)->set_video_params(params->impl, index); + impl(sequence)->set_video_params(*native, index); } catch (...) { return OAKNODE_E_FAILED; } diff --git a/src/node/src/color/ociogradingtransformlog/ociogradingtransformlog.cpp b/src/node/src/color/ociogradingtransformlog/ociogradingtransformlog.cpp index 5cbc8a4b8..e6f4464c2 100644 --- a/src/node/src/color/ociogradingtransformlog/ociogradingtransformlog.cpp +++ b/src/node/src/color/ociogradingtransformlog/ociogradingtransformlog.cpp @@ -24,9 +24,7 @@ #include #include -#include "ocioutils.h" #include "project.h" -#include "render/colorprocessor.h" #include "sliderdisplaytype.h" namespace olive @@ -36,23 +34,23 @@ namespace olive // GradingPrimaryTransform; do not rename them. OCIO's log style maps the // classic wheels as: brightness = lift, contrast = gain, gamma = gamma. const std::string OCIOGradingTransformLogNode::k_lift_input = - "ocio_grading_primary_brightness"; + "OCIO_NAMESPACE_grading_primary_brightness"; const std::string OCIOGradingTransformLogNode::k_gain_input = - "ocio_grading_primary_contrast"; + "OCIO_NAMESPACE_grading_primary_contrast"; const std::string OCIOGradingTransformLogNode::k_gamma_input = - "ocio_grading_primary_gamma"; + "OCIO_NAMESPACE_grading_primary_gamma"; const std::string OCIOGradingTransformLogNode::k_saturation_input = - "ocio_grading_primary_saturation"; + "OCIO_NAMESPACE_grading_primary_saturation"; const std::string OCIOGradingTransformLogNode::k_pivot_input = - "ocio_grading_primary_pivot"; + "OCIO_NAMESPACE_grading_primary_pivot"; const std::string OCIOGradingTransformLogNode::k_clamp_black_enable_input = "clamp_black_enable_in"; const std::string OCIOGradingTransformLogNode::k_clamp_black_input = - "ocio_grading_primary_clampBlack"; + "OCIO_NAMESPACE_grading_primary_clampBlack"; const std::string OCIOGradingTransformLogNode::k_clamp_white_enable_input = "clamp_white_enable_in"; const std::string OCIOGradingTransformLogNode::k_clamp_white_input = - "ocio_grading_primary_clampWhite"; + "OCIO_NAMESPACE_grading_primary_clampWhite"; #define super OCIOBaseNode @@ -75,7 +73,7 @@ OCIOGradingTransformLogNode::OCIOGradingTransformLogNode() set_input_property(k_saturation_input, "min", 0.0); add_input(k_pivot_input, NodeValue::k_float, - -0.2); // Default for GRADING_LOG listed in ocio::GradingPrimary + -0.2); // Default for GRADING_LOG listed in OCIO_NAMESPACE::GradingPrimary set_input_property(k_pivot_input, "base", 0.01); add_input(k_clamp_black_enable_input, NodeValue::k_boolean, false); @@ -93,7 +91,7 @@ OCIOGradingTransformLogNode::OCIOGradingTransformLogNode() set_input_property(k_clamp_white_input, "base", 0.01); // Constrain the white clamp minimum to just above the (static) black clamp - // as per ocio::GradingPrimary::validate. When the black clamp is keyframed + // as per OCIO_NAMESPACE::GradingPrimary::validate. When the black clamp is keyframed // or connected, Value() enforces the invariant per frame instead. update_clamp_white_minimum(); } @@ -105,7 +103,7 @@ std::string OCIOGradingTransformLogNode::name() const std::string OCIOGradingTransformLogNode::id() const { - return "org.olivevideoeditor.Olive.ociogradingtransformlog"; + return "org.olivevideoeditor.Olive.OCIO_NAMESPACEgradingtransformlog"; } std::vector OCIOGradingTransformLogNode::category() const @@ -147,7 +145,7 @@ void OCIOGradingTransformLogNode::InputValueChangedEvent(const std::string &inpu get_standard_value(k_clamp_black_enable_input).to_bool()); } else if (input == k_clamp_black_input) { // Ensure the white clamp is always greater than the black clamp as per - // ocio::GradingPrimary::validate + // OCIO_NAMESPACE::GradingPrimary::validate update_clamp_white_minimum(); } @@ -192,15 +190,15 @@ void OCIOGradingTransformLogNode::update_clamp_white_minimum() void OCIOGradingTransformLogNode::generate_processor() { if (manager()) { - ocio::GradingPrimaryTransformRcPtr gp = - ocio::GradingPrimaryTransform::Create(ocio::GRADING_LOG); + OCIO_NAMESPACE::GradingPrimaryTransformRcPtr gp = + OCIO_NAMESPACE::GradingPrimaryTransform::Create(OCIO_NAMESPACE::GRADING_LOG); gp->makeDynamic(); - gp->setDirection(ocio::TransformDirection::TRANSFORM_DIR_FORWARD); + gp->setDirection(OCIO_NAMESPACE::TransformDirection::TRANSFORM_DIR_FORWARD); try { set_processor(ColorProcessor::create( manager()->get_config()->getProcessor(gp))); - } catch (const ocio::Exception &e) { + } catch (const OCIO_NAMESPACE::Exception &e) { std::cerr << std::endl << e.what() << std::endl; } } @@ -222,7 +220,7 @@ void OCIOGradingTransformLogNode::value(const NodeValueRow &value, // OCIO expects vec3s on the GPU but RGBMs (master + RGB) on the // CPU; the per-style master combination below mirrors - // ocio::GradingPrimary. Lift is additive, gain/gamma multiply. + // OCIO_NAMESPACE::GradingPrimary. Lift is additive, gain/gamma multiply. Vector4D lift = value.at(k_lift_input).to_vec4(); lift.set_y(lift.y() + lift.x()); lift.set_z(lift.z() + lift.x()); @@ -250,18 +248,18 @@ void OCIOGradingTransformLogNode::value(const NodeValueRow &value, if (!value.at(k_clamp_black_enable_input).to_bool()) { job.insert(k_clamp_black_input, NodeValue(NodeValue::k_float, - ocio::GradingPrimary::NoClampBlack())); + OCIO_NAMESPACE::GradingPrimary::NoClampBlack())); } if (!value.at(k_clamp_white_enable_input).to_bool()) { job.insert(k_clamp_white_input, NodeValue(NodeValue::k_float, - ocio::GradingPrimary::NoClampWhite())); + OCIO_NAMESPACE::GradingPrimary::NoClampWhite())); } if (value.at(k_clamp_black_enable_input).to_bool() && value.at(k_clamp_white_enable_input).to_bool()) { - // ocio::GradingPrimary::validate requires the white clamp to be + // OCIO_NAMESPACE::GradingPrimary::validate requires the white clamp to be // greater than the black clamp. Keyframed or connected values // can violate this at arbitrary times, so enforce the invariant // per frame here. diff --git a/src/node/src/node.h b/src/node/src/node.h index 73a7b71e9..b05bfa9d1 100644 --- a/src/node/src/node.h +++ b/src/node/src/node.h @@ -22,8 +22,6 @@ #ifndef OAK_NODE_H #define OAK_NODE_H -#include "ofxhImageEffectAPI.h" - #include #include #include @@ -37,6 +35,7 @@ #include "keyframe.h" #include "inputimmediate.h" #include "param.h" +#include "ofxhImageEffectAPI.h" #include "olive/core/util/timerange.h" #include "render/audioplaybackcache.h" #include "render/audiowaveformcache.h" diff --git a/src/node/standalone/CMakeLists.txt b/src/node/standalone/CMakeLists.txt index 78b42bcf4..ee93e915e 100644 --- a/src/node/standalone/CMakeLists.txt +++ b/src/node/standalone/CMakeLists.txt @@ -116,10 +116,12 @@ target_include_directories(oakrender PUBLIC /opt/homebrew/include/OpenEXR ) -foreach(t oakrender oakgl oakvulkan) - target_link_options(${t} PRIVATE - "-undefined" "dynamic_lookup" - ) +foreach(t oakrender oakgl oakgl2 oakvulkan) + if(TARGET ${t}) + target_link_options(${t} PRIVATE + "-undefined" "dynamic_lookup" + ) + endif() endforeach() target_link_libraries(oakrender PRIVATE diff --git a/src/node/tests/colormanager_test.cpp b/src/node/tests/colormanager_test.cpp index 8a0d58cea..fcbfef1f3 100644 --- a/src/node/tests/colormanager_test.cpp +++ b/src/node/tests/colormanager_test.cpp @@ -263,15 +263,15 @@ TEST_F(ColorManagerTest, CompliantColorTransform) get_string(oaknode_colormanager_get_default_display, m); ASSERT_FALSE(display.empty()); - OakCommonColorTransform *t = oakcommon_colortransform_init_display( + OakColorTransform t = oakcommon_colortransform_init_display( display.c_str(), "No Such View", ""); - ASSERT_NE(t, nullptr); + ASSERT_NE(t.ctx, nullptr); - OakCommonColorTransform *compliant = nullptr; + OakColorTransform compliant = {}; ASSERT_EQ(oaknode_colormanager_get_compliant_color_transform(m, t, 0, &compliant), OAKNODE_OK); - ASSERT_NE(compliant, nullptr); + ASSERT_NE(compliant.ctx, nullptr); int is_display = 0; ASSERT_EQ(oakcommon_colortransform_is_display(compliant, &is_display), @@ -286,11 +286,11 @@ TEST_F(ColorManagerTest, CompliantColorTransform) needed); EXPECT_STRNE(buf.data(), "No Such View"); - oakcommon_colortransform_free(compliant); - oakcommon_colortransform_free(t); + oakcommon_colortransform_free(&compliant); + oakcommon_colortransform_free(&t); // Error paths - EXPECT_EQ(oaknode_colormanager_get_compliant_color_transform(m, nullptr, 0, + EXPECT_EQ(oaknode_colormanager_get_compliant_color_transform(m, OakColorTransform{}, 0, &compliant), OAKNODE_E_INVALID); diff --git a/src/node/tests/sequence_test.cpp b/src/node/tests/sequence_test.cpp index 6065a9608..6dd4e5565 100644 --- a/src/node/tests/sequence_test.cpp +++ b/src/node/tests/sequence_test.cpp @@ -247,31 +247,34 @@ TEST(SequenceTest, VideoParamsRoundTrip) ASSERT_GE(count, 1); // Default slot is readable - OakCommonVideoParams *params = nullptr; + OakVideoParams params = {}; ASSERT_EQ(oaknode_sequence_get_video_params(seq, 0, ¶ms), OAKNODE_OK); - ASSERT_NE(params, nullptr); - oakcommon_videoparams_free(params); + ASSERT_NE(params.ctx, nullptr); + oakcommon_videoparams_free(¶ms); // Out of range EXPECT_EQ(oaknode_sequence_get_video_params(seq, count, ¶ms), OAKNODE_E_NOT_FOUND); - EXPECT_EQ(oaknode_sequence_set_video_params(seq, count, nullptr), + EXPECT_EQ(oaknode_sequence_set_video_params(seq, count, OakVideoParams{}), OAKNODE_E_INVALID); // Replace with explicit 1920x1080 @ 25fps params - OakCommonVideoParams *replacement = oakcommon_videoparams_init_with_time_base( - 1920, 1080, 1, 25, 0 /*pixel_format*/, 4 /*nb_channels*/, 1, 1, - OAKCOMMON_VIDEO_INTERLACE_NONE, 1); - ASSERT_NE(replacement, nullptr); + OakVideoParams replacement = + oakcommon_videoparams_init_with_time_base( + 1920, 1080, 1, 25, 0 /*pixel_format*/, 4 /*nb_channels*/, 1, 1, + OAKCOMMON_VIDEO_INTERLACE_NONE, 1); + ASSERT_NE(replacement.ctx, nullptr); ASSERT_EQ(oaknode_sequence_set_video_params(seq, 0, replacement), OAKNODE_OK); - oakcommon_videoparams_free(replacement); + oakcommon_videoparams_free(&replacement); - OakCommonVideoParams *readback = nullptr; - ASSERT_EQ(oaknode_sequence_get_video_params(seq, 0, &readback), OAKNODE_OK); - ASSERT_NE(readback, nullptr); + OakVideoParams readback = {}; + ASSERT_EQ(oaknode_sequence_get_video_params(seq, 0, &readback), + OAKNODE_OK); + ASSERT_NE(readback.ctx, nullptr); int width = 0, height = 0, tb_num = 0, tb_den = 0; - ASSERT_EQ(oakcommon_videoparams_get_width(readback, &width), OAKCOMMON_OK); + ASSERT_EQ(oakcommon_videoparams_get_width(readback, &width), + OAKCOMMON_OK); ASSERT_EQ(oakcommon_videoparams_get_height(readback, &height), OAKCOMMON_OK); ASSERT_EQ(oakcommon_videoparams_get_time_base(readback, &tb_num, &tb_den), @@ -279,7 +282,7 @@ TEST(SequenceTest, VideoParamsRoundTrip) EXPECT_EQ(width, 1920); EXPECT_EQ(height, 1080); expect_rational(tb_num, tb_den, 1, 25); - oakcommon_videoparams_free(readback); + oakcommon_videoparams_free(&readback); oaknode_sequence_free(seq); } diff --git a/src/node/wrappers/CMakeLists.txt b/src/node/wrappers/CMakeLists.txt new file mode 100644 index 000000000..e69de29bb diff --git a/src/render/c_api/CMakeLists.txt b/src/render/c_api/CMakeLists.txt index 489178911..182e9a97c 100644 --- a/src/render/c_api/CMakeLists.txt +++ b/src/render/c_api/CMakeLists.txt @@ -1,6 +1,7 @@ target_sources(oakrender PRIVATE renderer.cpp cache.cpp + cancelatom.cpp color.cpp manager.cpp ) diff --git a/src/render/c_api/cancelatom.cpp b/src/render/c_api/cancelatom.cpp new file mode 100644 index 000000000..52ede2877 --- /dev/null +++ b/src/render/c_api/cancelatom.cpp @@ -0,0 +1,135 @@ +/*** + + 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 "../../../include/render/cancelatom.h" + +#include +#include + +#include "alivecount.h" + +#include "cancelatom.h" + +namespace +{ + +/** + * @brief Heap box behind every OakCancelAtom's ctx pointer. + * + * Holds the wrapped CancelAtom plus its atomic reference count. addref + * and release are emitted in this translation unit so the function + * pointers stored in a handle always run code from the DLL that created + * the object. + */ +struct CancelAtomBox { + olive::CancelAtom impl; + std::atomic refs; + + CancelAtomBox() + : refs(1) + { + } +}; + +CancelAtomBox *box(OakCancelAtom atom) +{ + return static_cast(atom.ctx); +} + +olive::CancelAtom *impl(OakCancelAtom atom) +{ + auto *b = box(atom); + return b ? &b->impl : nullptr; +} + +/** + * @brief Handle addref thunk: atomically increments the count. + */ +void cancel_atom_addref(void *ctx) +{ + auto *b = static_cast(ctx); + if (b) + b->refs.fetch_add(1, std::memory_order_relaxed); +} + +/** + * @brief Handle release thunk: decrements the count, destroys at zero. + */ +void cancel_atom_release(void *ctx) +{ + auto *b = static_cast(ctx); + if (b && b->refs.fetch_sub(1, std::memory_order_acq_rel) == 1) { + delete b; + oakrender_c_api::alive_dec(); + } +} + +} // namespace + +OakCancelAtom oakrender_cancelatom_init(void) +{ + OakCancelAtom h = {}; + try { + h.ctx = new CancelAtomBox(); + } catch (...) { + h.ctx = nullptr; + } + h.addref = &cancel_atom_addref; + h.release = &cancel_atom_release; + h.abi_version = OAKRENDER_ABI_VERSION; + if (h.ctx) + oakrender_c_api::alive_inc(); + return h; +} + +void oakrender_cancelatom_free(OakCancelAtom *atom) +{ + if (!atom || !atom->ctx || !atom->release) + return; + atom->release(atom->ctx); + atom->ctx = nullptr; +} + +int oakrender_cancelatom_cancel(OakCancelAtom atom) +{ + auto *c = impl(atom); + if (!c) + return OAKRENDER_E_INVALID; + c->cancel(); + return OAKRENDER_OK; +} + +int oakrender_cancelatom_is_cancelled(OakCancelAtom atom, int *cancelled) +{ + auto *c = impl(atom); + if (!c || !cancelled) + return OAKRENDER_E_INVALID; + *cancelled = c->is_cancelled() ? 1 : 0; + return OAKRENDER_OK; +} + +int oakrender_cancelatom_heard_cancel(OakCancelAtom atom, int *heard) +{ + auto *c = impl(atom); + if (!c || !heard) + return OAKRENDER_E_INVALID; + *heard = c->heard_cancel() ? 1 : 0; + return OAKRENDER_OK; +} diff --git a/src/render/c_api/color.cpp b/src/render/c_api/color.cpp index 90e89f501..1811c3927 100644 --- a/src/render/c_api/color.cpp +++ b/src/render/c_api/color.cpp @@ -30,6 +30,7 @@ #include "color/colormanager/colormanager.h" #include "filefunctions.h" +#include namespace { @@ -57,7 +58,7 @@ OakColorProcessor *oakrender_color_processor_create(const char *src_space, return nullptr; } try { - ocio::ConstConfigRcPtr config = olive::ColorManager::get_default_config(); + OCIO_NAMESPACE::ConstConfigRcPtr config = olive::ColorManager::get_default_config(); if (!config) { return nullptr; } @@ -69,17 +70,17 @@ OakColorProcessor *oakrender_color_processor_create(const char *src_space, src = config->getCanonicalName(src_space); } - // OCIO failures are non-fatal (matching the C++ behavior): the + // OCIO_NAMESPACE failures are non-fatal (matching the C++ behavior): the // handle is still returned, but holds a null processor and // conversions pass through. - ocio::ConstProcessorRcPtr processor; + OCIO_NAMESPACE::ConstProcessorRcPtr processor; try { if (direction == OAKRENDER_COLOR_DIRECTION_NORMAL) { processor = config->getProcessor(src.c_str(), dst_transform); } else { processor = config->getProcessor(dst_transform, src.c_str()); } - } catch (ocio::Exception &) { + } catch (OCIO_NAMESPACE::Exception &) { processor = nullptr; } @@ -169,7 +170,7 @@ int oakrender_color_manager_display_transform(const char *display, return OAKRENDER_E_INVALID; } try { - ocio::ConstConfigRcPtr config = olive::ColorManager::get_default_config(); + OCIO_NAMESPACE::ConstConfigRcPtr config = olive::ColorManager::get_default_config(); if (!config) { return OAKRENDER_E_STATE; } @@ -197,25 +198,44 @@ int oakrender_color_manager_display_transform(const char *display, } // Source = the config's reference colorspace (role lookup). - ocio::ConstColorSpaceRcPtr ref_cs = - config->getColorSpace(ocio::ROLE_REFERENCE); + OCIO_NAMESPACE::ConstColorSpaceRcPtr ref_cs = + config->getColorSpace(OCIO_NAMESPACE::ROLE_REFERENCE); if (!ref_cs) { return OAKRENDER_E_STATE; } - auto dvt = ocio::DisplayViewTransform::Create(); + auto dvt = OCIO_NAMESPACE::DisplayViewTransform::Create(); dvt->setSrc(ref_cs->getName()); dvt->setDisplay(display); dvt->setView(view); - ocio::ConstProcessorRcPtr processor = config->getProcessor(dvt); + OCIO_NAMESPACE::ConstProcessorRcPtr processor = config->getProcessor(dvt); if (!processor) { return OAKRENDER_E_NOT_FOUND; } return write_string(processor->getCacheID(), buf, n); - } catch (ocio::Exception &) { + } catch (OCIO_NAMESPACE::Exception &) { return OAKRENDER_E_NOT_FOUND; } catch (...) { return OAKRENDER_E_FAILED; } } + +int oakrender_color_processor_convert_frame(OakColorProcessor *processor, + OakCodecFrame *frame) +{ + if (!processor || !processor->ptr || !frame || !frame->ptr) { + return OAKRENDER_E_INVALID; + } + try { + // In-place: ColorProcessor::convert_frame() applies the CPU + // processor to the frame's pixel buffer through an + // OCIO::PackedImageDesc view. A processor whose underlying OCIO + // processor is null (creation failure was non-fatal) is a + // pass-through and still reports success, mirroring the C++ API. + processor->ptr->convert_frame(frame->ptr); + return OAKRENDER_OK; + } catch (...) { + return OAKRENDER_E_FAILED; + } +} diff --git a/src/render/src/CMakeLists.txt b/src/render/src/CMakeLists.txt index df06b4e17..502a52e25 100644 --- a/src/render/src/CMakeLists.txt +++ b/src/render/src/CMakeLists.txt @@ -30,9 +30,9 @@ add_library(oakrender SHARED ${OAKRENDER_SOURCES}) # Dynamically loaded render backends (oak_renderer_* C ABI). Loaded via # dlopen by DynamicRenderer from the app render_backends/ dir. -add_library(oakgl SHARED opengl/openglbackend_c.cpp) +add_library(oakgl2 SHARED opengl/openglbackend_c.cpp) add_library(oakvulkan SHARED vulkan/vulkanbackend_c.cpp) -foreach(backend oakgl oakvulkan) +foreach(backend oakgl2 oakvulkan) target_link_libraries(${backend} PRIVATE oakrender) endforeach() diff --git a/src/render/src/colorprocessor.cpp b/src/render/src/colorprocessor.cpp index 60728accc..2d9561eae 100644 --- a/src/render/src/colorprocessor.cpp +++ b/src/render/src/colorprocessor.cpp @@ -134,6 +134,11 @@ void ColorProcessor::convert_frame(Frame *f) cpu_processor_->apply(img); } +ocio::ConstProcessorRcPtr ColorProcessor::get_processor() +{ + return processor_; +} + Color ColorProcessor::convert_color(const Color &in) { if (!cpu_processor_) { @@ -163,10 +168,6 @@ ColorProcessorPtr ColorProcessor::create(ocio::ConstProcessorRcPtr processor) return std::make_shared(processor); } -ocio::ConstProcessorRcPtr ColorProcessor::get_processor() -{ - return processor_; -} void ColorProcessor::convert_frame(FramePtr f) { diff --git a/src/render/standalone/CMakeLists.txt b/src/render/standalone/CMakeLists.txt index de918aff2..783694860 100644 --- a/src/render/standalone/CMakeLists.txt +++ b/src/render/standalone/CMakeLists.txt @@ -116,10 +116,12 @@ endif() # Symbols of the not-yet-split engine modules (codec/audio/task/config/ # pluginSupport/...) dangle by design. The backend libraries resolve most # symbols from liboakrender at load time and dangle the same way. -foreach(t oakrender oakgl oakvulkan) - target_link_options(${t} PRIVATE - "-undefined" "dynamic_lookup" - ) +foreach(t oakrender oakgl oakgl2 oakvulkan) + if(TARGET ${t}) + target_link_options(${t} PRIVATE + "-undefined" "dynamic_lookup" + ) + endif() endforeach() target_link_libraries(oakrender PRIVATE diff --git a/src/render/tests/CMakeLists.txt b/src/render/tests/CMakeLists.txt index 0c7dffa51..eb71c3dd6 100644 --- a/src/render/tests/CMakeLists.txt +++ b/src/render/tests/CMakeLists.txt @@ -17,6 +17,7 @@ endif() add_executable(oakrender-gtest cache_test.cpp + cancelatom_test.cpp color_test.cpp manager_test.cpp renderer_test.cpp diff --git a/src/render/tests/cancelatom_test.cpp b/src/render/tests/cancelatom_test.cpp new file mode 100644 index 000000000..80db5d431 --- /dev/null +++ b/src/render/tests/cancelatom_test.cpp @@ -0,0 +1,136 @@ +/*** + + 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 . + +***/ + +// Same-dir quoted include would hit the src/node/transition/render/ +// bridge header (olive::CancelAtom) first on this build's include path; +// reference the public header relative to this file instead. +#include "../../../include/render/cancelatom.h" + +#include + +#include "render/cache.h" /* oakrender_debug_alive_count */ + +TEST(OakCancelAtomTest, InitFree) +{ + const int alive_before = oakrender_debug_alive_count(); + + OakCancelAtom atom = oakrender_cancelatom_init(); + ASSERT_NE(atom.ctx, nullptr); + EXPECT_NE(atom.addref, nullptr); + EXPECT_NE(atom.release, nullptr); + EXPECT_EQ(atom.abi_version, OAKRENDER_ABI_VERSION); + EXPECT_EQ(oakrender_debug_alive_count(), alive_before + 1); + + oakrender_cancelatom_free(&atom); + EXPECT_EQ(atom.ctx, nullptr); + EXPECT_EQ(oakrender_debug_alive_count(), alive_before); + + // NULL and empty handles are no-ops + oakrender_cancelatom_free(nullptr); + oakrender_cancelatom_free(&atom); + EXPECT_EQ(oakrender_debug_alive_count(), alive_before); +} + +TEST(OakCancelAtomTest, CancelStateMachine) +{ + OakCancelAtom atom = oakrender_cancelatom_init(); + ASSERT_NE(atom.ctx, nullptr); + + int flag = -1; + EXPECT_EQ(oakrender_cancelatom_is_cancelled(atom, &flag), OAKRENDER_OK); + EXPECT_EQ(flag, 0); + + EXPECT_EQ(oakrender_cancelatom_cancel(atom), OAKRENDER_OK); + + // Cancel must not be heard until a consumer reads the flag + int heard = -1; + EXPECT_EQ(oakrender_cancelatom_heard_cancel(atom, &heard), OAKRENDER_OK); + EXPECT_EQ(heard, 0); + + EXPECT_EQ(oakrender_cancelatom_is_cancelled(atom, &flag), OAKRENDER_OK); + EXPECT_EQ(flag, 1); + EXPECT_EQ(oakrender_cancelatom_heard_cancel(atom, &heard), OAKRENDER_OK); + EXPECT_EQ(heard, 1); + + oakrender_cancelatom_free(&atom); +} + +TEST(OakCancelAtomTest, InvalidArgs) +{ + OakCancelAtom empty = {}; + + int flag = 7; + EXPECT_EQ(oakrender_cancelatom_cancel(empty), OAKRENDER_E_INVALID); + EXPECT_EQ(oakrender_cancelatom_is_cancelled(empty, &flag), + OAKRENDER_E_INVALID); + EXPECT_EQ(flag, 7); + EXPECT_EQ(oakrender_cancelatom_heard_cancel(empty, &flag), + OAKRENDER_E_INVALID); + EXPECT_EQ(flag, 7); + + OakCancelAtom atom = oakrender_cancelatom_init(); + ASSERT_NE(atom.ctx, nullptr); + EXPECT_EQ(oakrender_cancelatom_is_cancelled(atom, nullptr), + OAKRENDER_E_INVALID); + EXPECT_EQ(oakrender_cancelatom_heard_cancel(atom, nullptr), + OAKRENDER_E_INVALID); + + oakrender_cancelatom_free(&atom); +} + +TEST(OakCancelAtomTest, AddrefReleaseCountSemantics) +{ + const int alive_before = oakrender_debug_alive_count(); + + OakCancelAtom atom = oakrender_cancelatom_init(); + ASSERT_NE(atom.ctx, nullptr); + + // Copy the struct and take an extra reference; both copies share the + // same underlying object and cancel state + OakCancelAtom copy = atom; + copy.addref(copy.ctx); + + EXPECT_EQ(oakrender_cancelatom_cancel(atom), OAKRENDER_OK); + int flag = 0; + EXPECT_EQ(oakrender_cancelatom_is_cancelled(copy, &flag), OAKRENDER_OK); + EXPECT_EQ(flag, 1); + + // Releasing one reference keeps the object alive for the other + atom.release(atom.ctx); + EXPECT_EQ(oakrender_debug_alive_count(), alive_before + 1); + flag = 0; + EXPECT_EQ(oakrender_cancelatom_is_cancelled(copy, &flag), OAKRENDER_OK); + EXPECT_EQ(flag, 1); + + // The final reference destroys the object + oakrender_cancelatom_free(©); + EXPECT_EQ(copy.ctx, nullptr); + EXPECT_EQ(oakrender_debug_alive_count(), alive_before); +} + +TEST(OakCancelAtomTest, AddrefReleaseNullCtxIsSafe) +{ + // NULL ctx must not crash the thunks + OakCancelAtom atom = oakrender_cancelatom_init(); + ASSERT_NE(atom.ctx, nullptr); + atom.addref(nullptr); + atom.release(nullptr); + oakrender_cancelatom_free(&atom); +}