engine: add the footage family to the C ABI facade; oak-cli probe

- oakengine_footage_probe inspects media without a project (decoder
  probe): stream counts, per-stream video info (dimensions, rate,
  duration, color tags, interlacing), audio info, duration, decoder
  name, source start time; thread_local last_error for the NULL-handle
  failure paths
- oakengine_project_import_footage adds probed footage to a project
  through the same undoable command path as the app
- oak-cli probe prints decoder/duration/per-stream details and runs as
  a ctest everywhere (no GL)
- dual ownership documented: probe handles are owned, imported footage
  is borrowed from its project
This commit is contained in:
2026-07-20 06:03:16 +08:00
parent 6118260e01
commit 1a7029fd8f
7 changed files with 919 additions and 1 deletions
+207
View File
@@ -0,0 +1,207 @@
/***
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 <http://www.gnu.org/licenses/>.
***/
#ifndef OAKENGINE_FOOTAGE_H
#define OAKENGINE_FOOTAGE_H
#include <stdint.h>
#include "export.h"
#include "init.h"
#include "project.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @file footage.h
* @brief C ABI for media probing and project footage import
*
* Two models share the OakEngineFootage opaque handle:
*
* - Probing (oakengine_footage_probe()): inspect a media file without any
* project, directly through the engine's decoder probe path
* (Decoder::probe(), the same code the import task uses, minus the
* task/UI). The handle wraps an owned olive::FootageDescription and must
* be released with oakengine_footage_free().
*
* - Importing (oakengine_project_import_footage()): probe a file and add
* it as a Footage node to a project's root folder (the non-UI core of
* ProjectImportTask: probe via Footage::set_filename(), then an undoable
* NodeAddCommand + FolderAddChild). The returned handle is BORROWED --
* the underlying node is owned by the project (QObject parent chain) and
* the handle becomes invalid when the project is freed.
* oakengine_footage_free() on a borrowed handle only releases the handle
* wrapper, never the node.
*
* Image sequences: the application's import asks the user whether numbered
* stills form a sequence (EngineCore::confirm_image_sequence_handler). No
* such handler exists behind this facade, so imported stills are always
* treated as single frames and are never merged into an image sequence.
*
* Errors: oakengine_footage_probe() and oakengine_project_import_footage()
* return NULL on failure and record a human-readable reason retrievable
* with oakengine_footage_last_error() (thread-local). Information functions
* follow the usual conventions: booleans are int, 0 (OAKENGINE_OK) /
* negative OAKENGINE_E_* codes, buf/size string output, NULL handles yield
* no-ops / zero results / OAKENGINE_E_INVALID.
*
* Stream info uses the same timestamp/timebase convention as the timeline
* family: `duration_ts` counts units of the stream's time base
* (`time_base_num`/`time_base_den` seconds per unit), so
* seconds = duration_ts * time_base_num / time_base_den.
*/
/**
* @brief Opaque media handle (owned for probes, borrowed for imports; see
* the file comment above).
*/
typedef struct OakEngineFootage OakEngineFootage;
/**
* @brief POD description of one video stream (olive::VideoParams).
*
* color_primaries/color_trc carry the ISO/IEC 23001-8 code points the
* decoder reports (1 = BT.709), 0 when unknown. interlaced is 1 when the
* stream is interlaced (VideoParams::Interlacing != k_interlace_none).
*/
typedef struct oak_footage_video_info {
int stream_index;
int width;
int height;
int frame_rate_num;
int frame_rate_den;
int64_t duration_ts; /**< Duration in time-base units. */
int time_base_num; /**< Seconds per time-base unit (numerator). */
int time_base_den; /**< Seconds per time-base unit (denominator). */
int color_primaries;
int color_trc;
int interlaced;
} oak_footage_video_info;
/**
* @brief POD description of one audio stream (olive::AudioParams).
*
* channel_layout is the ffmpeg-style channel mask (e.g. 0x3 = stereo).
*/
typedef struct oak_footage_audio_info {
int stream_index;
int sample_rate;
uint64_t channel_layout;
int channel_count;
int64_t duration_ts; /**< Duration in time-base units. */
int time_base_num; /**< Seconds per time-base unit (numerator). */
int time_base_den; /**< Seconds per time-base unit (denominator). */
} oak_footage_audio_info;
/**
* @brief Probe a media file (decoder, streams, durations, color tags).
*
* Runs Decoder::create_from_id("ffmpeg")->probe() directly; requires the
* engine to be initialized (OAKENGINE_INIT_HEADLESS is sufficient, no GL
* needed). Returns an owned handle, or NULL on failure (see
* oakengine_footage_last_error()).
*/
OAKENGINE_API OakEngineFootage *oakengine_footage_probe(const char *path);
/**
* @brief Release a handle. For probe handles this frees the description;
* for borrowed import handles it only frees the wrapper (the node stays
* with its project). NULL-safe.
*/
OAKENGINE_API void oakengine_footage_free(OakEngineFootage *self);
/**
* @brief Human-readable reason for the last failed probe/import on this
* thread (buf/size convention). Empty when the last call succeeded.
*/
OAKENGINE_API int oakengine_footage_last_error(char *buf, int buf_size);
/**
* @brief ID of the decoder that owns the media (e.g. "ffmpeg"), buf/size
* convention.
*/
OAKENGINE_API int oakengine_footage_get_decoder_name(OakEngineFootage *self,
char *buf, int buf_size);
OAKENGINE_API int
oakengine_footage_get_video_stream_count(const OakEngineFootage *self);
OAKENGINE_API int
oakengine_footage_get_audio_stream_count(const OakEngineFootage *self);
OAKENGINE_API int
oakengine_footage_get_subtitle_stream_count(const OakEngineFootage *self);
/**
* @brief Fill `out` with the video stream at `index`. Returns OAKENGINE_OK
* or OAKENGINE_E_NOT_FOUND for an out-of-range index.
*/
OAKENGINE_API int oakengine_footage_get_video_stream_info(
OakEngineFootage *self, int index, oak_footage_video_info *out);
/**
* @brief Fill `out` with the audio stream at `index`. Returns OAKENGINE_OK
* or OAKENGINE_E_NOT_FOUND for an out-of-range index.
*/
OAKENGINE_API int oakengine_footage_get_audio_stream_info(
OakEngineFootage *self, int index, oak_footage_audio_info *out);
/**
* @brief Media duration in seconds: the longest stream duration across all
* video and audio streams.
*/
OAKENGINE_API int oakengine_footage_get_duration(OakEngineFootage *self,
double *seconds);
/**
* @brief 1 if the media file exists on disk, 0 otherwise.
*/
OAKENGINE_API int oakengine_footage_is_online(OakEngineFootage *self);
/**
* @brief Source start time as a rational (FootageDescription::
* source_start_time(), e.g. from a timecode track). Returns 1 when the
* media carries one, 0 when it does not, or a negative error code.
*/
OAKENGINE_API int oakengine_footage_get_source_start_time(
OakEngineFootage *self, int *num, int *den);
/**
* @brief Probe `path` and import it into `project`'s root folder.
*
* Mirrors the non-UI core of ProjectImportTask: the footage is probed on
* assignment (Footage::set_filename()), invalid/unreadable media is
* rejected, and the add is pushed onto the global undo stack as an undoable
* command (direct, non-undoable application when the engine is not
* initialized). Stills are imported as single frames (see the file comment
* about image sequences).
*
* Returns a BORROWED handle (owned by the project; do not free the node,
* oakengine_footage_free() only releases the wrapper), or NULL on failure
* (see oakengine_footage_last_error()).
*/
OAKENGINE_API OakEngineFootage *oakengine_project_import_footage(
OakEngineProject *project, const char *path);
#ifdef __cplusplus
}
#endif
#endif /* OAKENGINE_FOOTAGE_H */