diff --git a/CMakeLists.txt b/CMakeLists.txt
index 994733995..be67eb457 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -223,6 +223,10 @@ list(APPEND OLIVE_LIBRARIES
FFMPEG::swresample
)
+# FFmpeg isolation: all FFmpeg access is being moved behind this shared
+# library's pure C API. Built early so the app can link it.
+add_subdirectory(ffmpeg_bridge)
+
# Detect FFmpeg pixel formats that may not exist in all versions
include(CheckCXXSourceCompiles)
set(CMAKE_REQUIRED_INCLUDES ${FFMPEG_INCLUDE_DIRS})
diff --git a/ffmpeg_bridge/CMakeLists.txt b/ffmpeg_bridge/CMakeLists.txt
new file mode 100644
index 000000000..0badf5afb
--- /dev/null
+++ b/ffmpeg_bridge/CMakeLists.txt
@@ -0,0 +1,102 @@
+# 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 .
+
+# ffmpeg_bridge: a self-contained shared library that isolates all FFmpeg
+# access behind a pure C API (see include/ffmpeg_bridge/ffmpeg_bridge.h).
+# The editor and the render worker link only against this library.
+
+find_package(FFMPEG 6.0 REQUIRED
+ COMPONENTS
+ avutil
+ avcodec
+ avformat
+ avfilter
+ swscale
+ swresample
+)
+
+add_library(ffmpeg_bridge SHARED
+ src/audiograph.cpp
+ src/decoder.cpp
+ src/encoder.cpp
+ src/frame.cpp
+ src/packet.cpp
+ src/probe.cpp
+ src/sws.cpp
+ src/swr.cpp
+ src/utils.cpp
+ src/internal.h
+ include/ffmpeg_bridge/ffmpeg_bridge.h
+)
+
+target_compile_features(ffmpeg_bridge PRIVATE cxx_std_17)
+
+target_include_directories(ffmpeg_bridge
+ PUBLIC
+ ${CMAKE_CURRENT_SOURCE_DIR}/include
+ PRIVATE
+ ${FFMPEG_INCLUDE_DIRS}
+)
+
+target_link_libraries(ffmpeg_bridge
+ PRIVATE
+ FFMPEG::avutil
+ FFMPEG::avcodec
+ FFMPEG::avformat
+ FFMPEG::avfilter
+ FFMPEG::swscale
+ FFMPEG::swresample
+)
+
+# Static FFmpeg (e.g. the Linux CI build) needs system libs for
+# libavcodec/libavformat.
+find_package(ZLIB REQUIRED)
+target_link_libraries(ffmpeg_bridge PRIVATE ZLIB::ZLIB)
+
+find_package(BZip2 REQUIRED)
+target_link_libraries(ffmpeg_bridge PRIVATE BZip2::BZip2)
+
+find_package(LibLZMA)
+if (LIBLZMA_FOUND)
+ target_link_libraries(ffmpeg_bridge PRIVATE ${LIBLZMA_LIBRARIES})
+endif()
+
+target_compile_definitions(ffmpeg_bridge PRIVATE FFMPEG_BRIDGE_BUILD)
+
+# Only export the FB_API functions
+set_target_properties(ffmpeg_bridge PROPERTIES
+ C_VISIBILITY_PRESET hidden
+ CXX_VISIBILITY_PRESET hidden
+ VISIBILITY_INLINES_HIDDEN ON
+)
+
+# The shared library lives in its own directory outside of app/
+set_target_properties(ffmpeg_bridge PROPERTIES
+ LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/ffmpeg_bridge/bin"
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/ffmpeg_bridge/bin"
+ ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/ffmpeg_bridge/lib"
+)
+
+if (WIN32)
+ set_target_properties(ffmpeg_bridge PROPERTIES PREFIX "")
+endif ()
+
+install(TARGETS ffmpeg_bridge
+ RUNTIME DESTINATION ffmpeg_bridge/bin
+ LIBRARY DESTINATION ffmpeg_bridge/bin
+ ARCHIVE DESTINATION ffmpeg_bridge/lib
+)
diff --git a/ffmpeg_bridge/include/ffmpeg_bridge/ffmpeg_bridge.h b/ffmpeg_bridge/include/ffmpeg_bridge/ffmpeg_bridge.h
new file mode 100644
index 000000000..36ba2ac2d
--- /dev/null
+++ b/ffmpeg_bridge/include/ffmpeg_bridge/ffmpeg_bridge.h
@@ -0,0 +1,593 @@
+/***
+
+ 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 FFMPEG_BRIDGE_H
+#define FFMPEG_BRIDGE_H
+
+/**
+ * ffmpeg_bridge - pure C API isolating all FFmpeg access from the editor.
+ *
+ * All objects (frames, packets, decoders, encoders, scalers, resamplers,
+ * audio graphs) are identified by opaque handles and always live inside the
+ * shared library; callers never see or dereference an FFmpeg structure.
+ *
+ * Error convention: functions returning int return 0 (or a non-negative
+ * value) on success and a negative error code on failure. Negative codes are
+ * FFmpeg error codes and can be converted to text with fb_error_string().
+ * End-of-file is reported as FB_ERROR_EOF.
+ */
+
+#include
+#include
+
+#if defined(_WIN32)
+#if defined(FFMPEG_BRIDGE_BUILD)
+#define FB_API __declspec(dllexport)
+#else
+#define FB_API __declspec(dllimport)
+#endif
+#else
+#define FB_API __attribute__((visibility("default")))
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* ------------------------------------------------------------------------- */
+/* Constants */
+/* ------------------------------------------------------------------------- */
+
+/** Pass-through of AVERROR_EOF (verified by static_assert in the library). */
+#define FB_ERROR_EOF (-541478725)
+
+/** Pass-through of AV_NOPTS_VALUE. */
+#define FB_NOPTS_VALUE INT64_MIN
+
+/** Pass-through of AV_TIME_BASE. */
+#define FB_TIME_BASE 1000000
+
+/** Scaler algorithm flags (values mirror libswscale). */
+#define FB_SCALER_POINT 0x10
+
+/**
+ * Pixel formats. Values deliberately mirror AVPixelFormat so that the library
+ * can use them directly; every value is static_assert'ed against the real
+ * FFmpeg headers inside the library. Callers must treat them as opaque.
+ */
+typedef enum FBPixelFormat {
+ FB_PIX_FMT_NONE = -1,
+ FB_PIX_FMT_YUV420P = 0,
+ FB_PIX_FMT_RGB24 = 2,
+ FB_PIX_FMT_YUV422P = 4,
+ FB_PIX_FMT_YUV444P = 5,
+ FB_PIX_FMT_YUV410P = 6,
+ FB_PIX_FMT_YUV411P = 7,
+ FB_PIX_FMT_GRAY8 = 8,
+ FB_PIX_FMT_YUVJ420P = 12,
+ FB_PIX_FMT_YUVJ422P = 13,
+ FB_PIX_FMT_YUVJ444P = 14,
+ FB_PIX_FMT_RGBA = 26,
+ FB_PIX_FMT_GRAY16LE = 30,
+ FB_PIX_FMT_YUVJ440P = 32,
+ FB_PIX_FMT_RGB48LE = 35,
+ FB_PIX_FMT_YUV420P10LE = 62,
+ FB_PIX_FMT_YUV422P10LE = 64,
+ FB_PIX_FMT_YUV444P10LE = 68,
+ FB_PIX_FMT_RGBA64LE = 105,
+ FB_PIX_FMT_YUV420P12LE = 123,
+ FB_PIX_FMT_YUV422P12LE = 127,
+ FB_PIX_FMT_YUV444P12LE = 131,
+ FB_PIX_FMT_YUVJ411P = 138,
+ FB_PIX_FMT_GRAYF32LE = 183,
+ FB_PIX_FMT_RGBAF16LE = 207,
+ FB_PIX_FMT_RGBF32LE = 218,
+ FB_PIX_FMT_RGBAF32LE = 220,
+ FB_PIX_FMT_RGBF16LE = 234,
+ FB_PIX_FMT_GRAYF16LE = 248
+} FBPixelFormat;
+
+/**
+ * Sample formats. Values mirror AVSampleFormat (static_assert'ed).
+ */
+typedef enum FBSampleFormat {
+ FB_SAMPLE_FMT_NONE = -1,
+ FB_SAMPLE_FMT_U8 = 0,
+ FB_SAMPLE_FMT_S16 = 1,
+ FB_SAMPLE_FMT_S32 = 2,
+ FB_SAMPLE_FMT_FLT = 3,
+ FB_SAMPLE_FMT_DBL = 4,
+ FB_SAMPLE_FMT_U8P = 5,
+ FB_SAMPLE_FMT_S16P = 6,
+ FB_SAMPLE_FMT_S32P = 7,
+ FB_SAMPLE_FMT_FLTP = 8,
+ FB_SAMPLE_FMT_DBLP = 9,
+ FB_SAMPLE_FMT_S64 = 10,
+ FB_SAMPLE_FMT_S64P = 11
+} FBSampleFormat;
+
+/** Color ranges. Values mirror AVColorRange (static_assert'ed). */
+typedef enum FBColorRange {
+ FB_COLOR_RANGE_UNSPEC = 0,
+ FB_COLOR_RANGE_MPEG = 1,
+ FB_COLOR_RANGE_JPEG = 2
+} FBColorRange;
+
+/** Color spaces. Values mirror AVColorSpace (static_assert'ed). */
+typedef enum FBColorSpace {
+ FB_COL_SPC_RGB = 0,
+ FB_COL_SPC_BT709 = 1,
+ FB_COL_SPC_UNSPEC = 2,
+ FB_COL_SPC_FCC = 4,
+ FB_COL_SPC_BT470BG = 5,
+ FB_COL_SPC_SMPTE170M = 6,
+ FB_COL_SPC_SMPTE240M = 7,
+ FB_COL_SPC_BT2020_NCL = 9
+} FBColorSpace;
+
+/** Media types. Values mirror AVMediaType (static_assert'ed). */
+typedef enum FBMediaType {
+ FB_MEDIA_TYPE_VIDEO = 0,
+ FB_MEDIA_TYPE_AUDIO = 1,
+ FB_MEDIA_TYPE_DATA = 2,
+ FB_MEDIA_TYPE_SUBTITLE = 3
+} FBMediaType;
+
+/** Field orders. Values mirror AVFieldOrder (static_assert'ed). */
+typedef enum FBFieldOrder {
+ FB_FIELD_ORDER_UNKNOWN = 0,
+ FB_FIELD_ORDER_PROGRESSIVE = 1,
+ FB_FIELD_ORDER_TT = 2,
+ FB_FIELD_ORDER_BB = 3,
+ FB_FIELD_ORDER_TB = 4,
+ FB_FIELD_ORDER_BT = 5
+} FBFieldOrder;
+
+/**
+ * Channel layout masks. Values mirror AV_CH_LAYOUT_* (static_assert'ed).
+ */
+#define FB_CH_LAYOUT_MONO ((uint64_t)0x4)
+#define FB_CH_LAYOUT_STEREO ((uint64_t)0x3)
+#define FB_CH_LAYOUT_2_1 ((uint64_t)0x103)
+#define FB_CH_LAYOUT_5POINT1 ((uint64_t)0x60F)
+#define FB_CH_LAYOUT_7POINT1 ((uint64_t)0x63F)
+
+/**
+ * Codecs the encoder supports. Opaque ordering; mapped explicitly by the
+ * caller's adapter layer (no value relationship with any app-side enum).
+ */
+typedef enum FBCodec {
+ FB_CODEC_NONE = -1,
+ FB_CODEC_H264 = 0,
+ FB_CODEC_H264RGB,
+ FB_CODEC_DNXHD,
+ FB_CODEC_PRORES,
+ FB_CODEC_CINEFORM,
+ FB_CODEC_H265,
+ FB_CODEC_VP9,
+ FB_CODEC_AV1,
+ FB_CODEC_OPENEXR,
+ FB_CODEC_PNG,
+ FB_CODEC_TIFF,
+ FB_CODEC_MP2,
+ FB_CODEC_MP3,
+ FB_CODEC_AAC,
+ FB_CODEC_PCM,
+ FB_CODEC_FLAC,
+ FB_CODEC_OPUS,
+ FB_CODEC_VORBIS,
+ FB_CODEC_SRT
+} FBCodec;
+
+/** Cancellation callback: return non-zero to request cancellation. */
+typedef int (*FBCancelCallback)(void *userdata);
+
+/* ------------------------------------------------------------------------- */
+/* Opaque handles */
+/* ------------------------------------------------------------------------- */
+
+typedef struct FBFrame FBFrame;
+typedef struct FBPacket FBPacket;
+typedef struct FBDecoder FBDecoder;
+typedef struct FBProbe FBProbe;
+typedef struct FBEncoder FBEncoder;
+typedef struct FBScaler FBScaler;
+typedef struct FBResampler FBResampler;
+typedef struct FBAudioGraph FBAudioGraph;
+
+/* ------------------------------------------------------------------------- */
+/* Error strings / version */
+/* ------------------------------------------------------------------------- */
+
+FB_API void fb_error_string(int error_code, char *buffer, int buffer_size);
+FB_API const char *fb_version_string(void);
+
+/* ------------------------------------------------------------------------- */
+/* Pixel/sample format utilities */
+/* ------------------------------------------------------------------------- */
+
+FB_API const char *fb_pix_fmt_name(int pix_fmt);
+FB_API int fb_pix_fmt_from_name(const char *name);
+FB_API int fb_pix_fmt_bits_per_pixel(int pix_fmt);
+FB_API int fb_pix_fmt_has_alpha(int pix_fmt);
+FB_API int fb_pix_fmt_is_planar(int pix_fmt);
+/** Size in bytes of one sample component (1 for 8-bit formats, 2 for 9-16bit). */
+FB_API int fb_pix_fmt_component_size(int pix_fmt);
+
+/**
+ * Find the best pixel format from `list` for storing `pix_fmt` losslessly.
+ * `list` is terminated by FB_PIX_FMT_NONE. Mirrors
+ * avcodec_find_best_pix_fmt_of_list(list, fmt, has_alpha=1).
+ */
+FB_API int fb_find_best_pix_fmt_of_list(const int *list, int pix_fmt);
+
+/** Number of channels in a channel layout mask. */
+FB_API int fb_channel_layout_get_channels(uint64_t mask);
+/** Default layout mask for a channel count (mirrors av_channel_layout_default). */
+FB_API uint64_t fb_channel_layout_default(int nb_channels);
+
+/* ------------------------------------------------------------------------- */
+/* Frame (AVFrame wrapper) */
+/* ------------------------------------------------------------------------- */
+
+FB_API FBFrame *fb_frame_alloc(void);
+FB_API void fb_frame_free(FBFrame **frame);
+/** Clear the frame's contents back to defaults (mirrors av_frame_unref). */
+FB_API void fb_frame_unref(FBFrame *frame);
+/** Allocate the frame's buffer(s) from its width/height/format fields. */
+FB_API int fb_frame_get_buffer(FBFrame *frame, int align);
+FB_API int fb_frame_copy_props(FBFrame *dst, const FBFrame *src);
+/** Transfer data between a hardware frame and a software frame. */
+FB_API int fb_frame_hw_transfer_data(FBFrame *dst, const FBFrame *src);
+FB_API int fb_frame_is_hw(const FBFrame *frame);
+
+FB_API int fb_frame_get_width(const FBFrame *frame);
+FB_API void fb_frame_set_width(FBFrame *frame, int width);
+FB_API int fb_frame_get_height(const FBFrame *frame);
+FB_API void fb_frame_set_height(FBFrame *frame, int height);
+FB_API int fb_frame_get_format(const FBFrame *frame);
+FB_API void fb_frame_set_format(FBFrame *frame, int format);
+FB_API int64_t fb_frame_get_pts(const FBFrame *frame);
+FB_API void fb_frame_set_pts(FBFrame *frame, int64_t pts);
+FB_API int64_t fb_frame_get_best_effort_timestamp(const FBFrame *frame);
+FB_API int fb_frame_get_nb_samples(const FBFrame *frame);
+FB_API void fb_frame_set_nb_samples(FBFrame *frame, int nb_samples);
+FB_API int fb_frame_get_sample_rate(const FBFrame *frame);
+FB_API void fb_frame_set_sample_rate(FBFrame *frame, int sample_rate);
+FB_API int fb_frame_get_color_range(const FBFrame *frame);
+FB_API void fb_frame_set_color_range(FBFrame *frame, int color_range);
+FB_API int fb_frame_get_colorspace(const FBFrame *frame);
+FB_API void fb_frame_set_colorspace(FBFrame *frame, int colorspace);
+FB_API uint64_t fb_frame_get_channel_layout_mask(const FBFrame *frame);
+FB_API void fb_frame_set_channel_layout_mask(FBFrame *frame, uint64_t mask);
+
+FB_API uint8_t *fb_frame_get_data(FBFrame *frame, int plane);
+FB_API const uint8_t *fb_frame_get_data_const(const FBFrame *frame, int plane);
+FB_API void fb_frame_set_data(FBFrame *frame, int plane, uint8_t *data);
+FB_API int fb_frame_get_linesize(const FBFrame *frame, int plane);
+FB_API void fb_frame_set_linesize(FBFrame *frame, int plane, int linesize);
+
+/* ------------------------------------------------------------------------- */
+/* Packet (AVPacket wrapper) */
+/* ------------------------------------------------------------------------- */
+
+FB_API FBPacket *fb_packet_alloc(void);
+FB_API void fb_packet_free(FBPacket **packet);
+FB_API void fb_packet_unref(FBPacket *packet);
+
+FB_API int64_t fb_packet_get_pts(const FBPacket *packet);
+FB_API int64_t fb_packet_get_duration(const FBPacket *packet);
+FB_API int fb_packet_get_size(const FBPacket *packet);
+FB_API const uint8_t *fb_packet_get_data(const FBPacket *packet);
+FB_API int fb_packet_get_stream_index(const FBPacket *packet);
+
+/* ------------------------------------------------------------------------- */
+/* Stream info */
+/* ------------------------------------------------------------------------- */
+
+typedef struct FBStreamInfo {
+ int index;
+ int codec_type; /* FBMediaType */
+ int codec_id; /* opaque FFmpeg codec id */
+ int has_decoder; /* non-zero if a decoder exists for this stream */
+
+ int width;
+ int height;
+ int pixel_format; /* FBPixelFormat */
+ int field_order; /* FBFieldOrder */
+ int color_range; /* FBColorRange */
+
+ int sample_rate;
+ int sample_format; /* FBSampleFormat */
+ uint64_t channel_layout_mask; /* validated (never zero for valid audio) */
+
+ int64_t start_time;
+ int64_t duration;
+ int time_base_num;
+ int time_base_den;
+ int avg_frame_rate_num;
+ int avg_frame_rate_den;
+} FBStreamInfo;
+
+/* ------------------------------------------------------------------------- */
+/* Decoder */
+/* ------------------------------------------------------------------------- */
+
+FB_API FBDecoder *fb_decoder_create(void);
+FB_API void fb_decoder_free(FBDecoder **decoder);
+
+FB_API int fb_decoder_open(FBDecoder *decoder, const char *filename,
+ int stream_index);
+FB_API void fb_decoder_close(FBDecoder *decoder);
+
+/**
+ * Retrieve the next decoded frame. Returns 0 on success, FB_ERROR_EOF at end
+ * of stream, or another negative error code.
+ */
+FB_API int fb_decoder_get_frame(FBDecoder *decoder, FBPacket *packet,
+ FBFrame *frame);
+/** Retrieve the next raw packet of the opened stream. Same return contract. */
+FB_API int fb_decoder_get_packet(FBDecoder *decoder, FBPacket *packet);
+FB_API void fb_decoder_seek(FBDecoder *decoder, int64_t timestamp);
+
+FB_API int fb_decoder_get_stream_info(const FBDecoder *decoder,
+ FBStreamInfo *out);
+FB_API int64_t fb_decoder_get_format_start_time(const FBDecoder *decoder);
+
+FB_API int fb_decoder_guess_sample_aspect_ratio(const FBDecoder *decoder,
+ FBFrame *frame, int *num,
+ int *den);
+FB_API int fb_decoder_guess_frame_rate(const FBDecoder *decoder,
+ FBFrame *frame, int *num, int *den);
+
+FB_API int fb_decoder_hwaccel_enabled(const FBDecoder *decoder);
+FB_API int fb_decoder_hw_pix_fmt(const FBDecoder *decoder);
+
+/* ------------------------------------------------------------------------- */
+/* Probe */
+/* ------------------------------------------------------------------------- */
+
+FB_API FBProbe *fb_probe_create(void);
+FB_API void fb_probe_free(FBProbe **probe);
+
+FB_API int fb_probe_open(FBProbe *probe, const char *filename);
+FB_API void fb_probe_close(FBProbe *probe);
+
+FB_API int fb_probe_get_stream_count(const FBProbe *probe);
+FB_API int fb_probe_get_stream_info(const FBProbe *probe, int stream_index,
+ FBStreamInfo *out);
+FB_API int64_t fb_probe_get_duration(const FBProbe *probe);
+FB_API int64_t fb_probe_get_start_time(const FBProbe *probe);
+FB_API int fb_probe_duration_from_bitrate(const FBProbe *probe);
+
+/**
+ * Read a metadata value from the format (stream_index = -1) or from a stream.
+ * Returns 1 if found (buffer filled), 0 otherwise.
+ */
+FB_API int fb_probe_get_metadata(FBProbe *probe, int stream_index,
+ const char *key, char *buffer,
+ int buffer_size);
+
+typedef struct FBVideoStreamDetails {
+ int field_order; /* FBFieldOrder */
+ int pixel_aspect_num;
+ int pixel_aspect_den;
+ int frame_rate_num;
+ int frame_rate_den;
+ int is_still;
+ int64_t decoded_duration; /* last timestamp seen, FB_NOPTS_VALUE if none */
+} FBVideoStreamDetails;
+
+/**
+ * Open the file and decode the start of a video stream to determine
+ * interlacing, aspect ratio, frame rate, whether it is a still image, and
+ * (when `decode_full_duration` is non-zero, by decoding to the end) its true
+ * duration.
+ */
+FB_API int fb_probe_video_stream_details(const char *filename, int stream_index,
+ FBVideoStreamDetails *out,
+ int decode_full_duration,
+ FBCancelCallback cancel,
+ void *cancel_userdata);
+
+/** Decode an audio stream to its end to determine its true duration. */
+FB_API int fb_probe_audio_stream_duration(const char *filename,
+ int stream_index,
+ int64_t *out_duration,
+ FBCancelCallback cancel,
+ void *cancel_userdata);
+
+/** Called once per subtitle packet. */
+typedef void (*FBSubtitleCallback)(int64_t pts, int64_t duration,
+ const char *text, int text_size,
+ void *userdata);
+
+/** Read all subtitle packets of a stream (e.g. SRT) via callback. */
+FB_API int fb_probe_read_subtitle_stream(const char *filename, int stream_index,
+ FBSubtitleCallback callback,
+ void *userdata);
+
+/* ------------------------------------------------------------------------- */
+/* Scaler (libswscale wrapper) */
+/* ------------------------------------------------------------------------- */
+
+FB_API FBScaler *fb_scaler_create(int src_width, int src_height, int src_format,
+ int dst_width, int dst_height, int dst_format,
+ int flags);
+FB_API void fb_scaler_free(FBScaler **scaler);
+/**
+ * Set colorspace details from an AVColorSpace value; the appropriate swscale
+ * coefficient table is looked up internally. `jpeg_range` is non-zero for
+ * full-range (JPEG) content.
+ */
+FB_API int fb_scaler_set_colorspace(FBScaler *scaler, int colorspace,
+ int jpeg_range);
+FB_API int fb_scaler_scale_frame(FBScaler *scaler, FBFrame *dst,
+ const FBFrame *src);
+FB_API int fb_scaler_scale_slices(FBScaler *scaler,
+ const uint8_t *const *src_data,
+ const int *src_linesize, int src_height,
+ uint8_t *const *dst_data,
+ const int *dst_linesize);
+
+/**
+ * YUV->RGB conversion coefficients for `colorspace`, normalized to [0,1].
+ * Output order: crv, cbu, cgu, cgv.
+ */
+FB_API void fb_get_yuv_coefficients(int colorspace, double out[4]);
+
+/* ------------------------------------------------------------------------- */
+/* Resampler (libswresample wrapper) */
+/* ------------------------------------------------------------------------- */
+
+FB_API FBResampler *fb_resampler_create(uint64_t out_layout_mask, int out_format,
+ int out_rate, uint64_t in_layout_mask,
+ int in_format, int in_rate);
+FB_API void fb_resampler_free(FBResampler **resampler);
+FB_API int fb_resampler_get_out_samples(FBResampler *resampler,
+ int in_samples);
+FB_API int fb_resampler_convert(FBResampler *resampler, uint8_t **out,
+ int out_count, const uint8_t **in,
+ int in_count);
+
+/* ------------------------------------------------------------------------- */
+/* Audio filter graph (abuffer/aformat/atempo/abuffersink wrapper) */
+/* ------------------------------------------------------------------------- */
+
+typedef struct FBAudioGraphConfig {
+ int in_sample_rate;
+ uint64_t in_channel_layout_mask; /* 0 = derive default from in_channels */
+ int in_sample_format; /* FBSampleFormat (planar float in) */
+ int in_channels;
+
+ int out_sample_rate;
+ uint64_t out_channel_layout_mask; /* 0 = derive default from out_channels */
+ int out_sample_format;
+ int out_channels;
+ int out_is_planar;
+
+ double tempo;
+} FBAudioGraphConfig;
+
+FB_API FBAudioGraph *fb_audio_graph_create(const FBAudioGraphConfig *config);
+FB_API void fb_audio_graph_free(FBAudioGraph **graph);
+/** Push planar samples into the graph. channel_data == NULL flushes the graph. */
+FB_API int fb_audio_graph_push(FBAudioGraph *graph,
+ const uint8_t *const *channel_data,
+ int nb_samples);
+/** Pull converted samples. Returns 1 if a frame was produced, 0 if more input
+ * is needed, or a negative error code. */
+FB_API int fb_audio_graph_pull(FBAudioGraph *graph, FBFrame *out_frame);
+
+/* ------------------------------------------------------------------------- */
+/* Encoder */
+/* ------------------------------------------------------------------------- */
+
+typedef struct FBEncoderConfig {
+ const char *filename;
+
+ int video_enabled;
+ int video_codec; /* FBCodec */
+ int video_width;
+ int video_height;
+ int video_pixel_aspect_num;
+ int video_pixel_aspect_den;
+ int video_time_base_num; /* frame rate expressed as a time base */
+ int video_time_base_den;
+ int video_frame_rate_num;
+ int video_frame_rate_den;
+ const char *video_pix_fmt; /* encoder pixel format name, e.g. "yuv420p" */
+ int video_src_pix_fmt; /* FBPixelFormat of frames passed to write_video_frame */
+ int video_color_range; /* FBColorRange */
+ int video_field_order; /* FBFieldOrder (PROGRESSIVE/TT/BB) */
+ 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;
+ int video_color_srgb; /* non-zero: tag nclc as sRGB, else Rec.709 */
+ const char **video_opt_keys;
+ const char **video_opt_values;
+ int video_opt_count;
+
+ int audio_enabled;
+ int audio_codec; /* FBCodec */
+ int audio_sample_rate;
+ uint64_t audio_channel_layout_mask;
+ int audio_sample_format; /* FBSampleFormat */
+ int64_t audio_bit_rate;
+
+ int subtitles_enabled;
+ int subtitle_codec; /* FBCodec */
+ const uint8_t *subtitle_header;
+ int subtitle_header_size;
+} FBEncoderConfig;
+
+FB_API FBEncoder *fb_encoder_create(const FBEncoderConfig *config);
+FB_API void fb_encoder_free(FBEncoder **encoder);
+
+FB_API int fb_encoder_open(FBEncoder *encoder);
+FB_API void fb_encoder_close(FBEncoder *encoder);
+
+/**
+ * Write one video frame of raw pixel data. `time_seconds` is the presentation
+ * time in seconds. The data must remain valid until this call returns.
+ */
+FB_API int fb_encoder_write_video_frame(FBEncoder *encoder, int width,
+ int height, int pix_fmt,
+ const uint8_t *data, int linesize,
+ double time_seconds);
+
+/**
+ * Write planar audio samples. `channel_data` has `channels` pointers, each
+ * with `sample_count` samples of `sample_format`. Passing sample_count == 0
+ * flushes the audio encoder.
+ */
+FB_API int fb_encoder_write_audio(FBEncoder *encoder,
+ const uint8_t *const *channel_data,
+ int channels, int sample_format,
+ int sample_rate, uint64_t channel_layout_mask,
+ int64_t sample_count);
+
+FB_API int fb_encoder_write_subtitle(FBEncoder *encoder, const char *utf8_text,
+ double in_seconds, double duration_seconds);
+
+/** Last error message (empty string if none). Valid until the next call. */
+FB_API const char *fb_encoder_get_error(const FBEncoder *encoder);
+
+/**
+ * List the pixel formats an encoder codec supports.
+ * Writes up to `max_names` names into `names`, returns the total count.
+ */
+FB_API int fb_encoder_codec_get_pixel_formats(int codec, const char **names,
+ int max_names);
+/**
+ * List the sample formats (FBSampleFormat) an encoder codec supports.
+ * Writes up to `max_fmts` into `fmts`, returns the total count.
+ */
+FB_API int fb_encoder_codec_get_sample_formats(int codec, int *fmts,
+ int max_fmts);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // FFMPEG_BRIDGE_H
diff --git a/ffmpeg_bridge/src/audiograph.cpp b/ffmpeg_bridge/src/audiograph.cpp
new file mode 100644
index 000000000..9e62e2857
--- /dev/null
+++ b/ffmpeg_bridge/src/audiograph.cpp
@@ -0,0 +1,244 @@
+/***
+
+ 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 "internal.h"
+
+#include
+#include
+
+struct FBAudioGraph {
+ AVFilterGraph *graph = nullptr;
+ AVFilterContext *buffersrc = nullptr;
+ AVFilterContext *buffersink = nullptr;
+ AVFrame *in_frame = nullptr;
+
+ int in_channels = 0;
+ int in_sample_rate = 0;
+ int in_sample_format = FB_SAMPLE_FMT_NONE;
+ int64_t in_pts = 0;
+};
+
+static AVFilterContext *CreateTempoFilter(AVFilterGraph *graph,
+ AVFilterContext *link, double tempo)
+{
+ char speed_param[20];
+ snprintf(speed_param, sizeof(speed_param), "%f", tempo);
+
+ AVFilterContext *tempo_ctx = nullptr;
+ if (avfilter_graph_create_filter(&tempo_ctx, avfilter_get_by_name("atempo"),
+ "atempo", speed_param, nullptr, graph) >= 0 &&
+ avfilter_link(link, 0, tempo_ctx, 0) == 0) {
+ return tempo_ctx;
+ }
+
+ return nullptr;
+}
+
+FBAudioGraph *fb_audio_graph_create(const FBAudioGraphConfig *config)
+{
+ if (!config) {
+ return nullptr;
+ }
+
+ FBAudioGraph *g = new FBAudioGraph;
+
+ g->graph = avfilter_graph_alloc();
+ if (!g->graph) {
+ delete g;
+ return nullptr;
+ }
+
+ AVChannelLayout in_layout, out_layout;
+ fb::ChannelLayoutFromMask(&in_layout, config->in_channel_layout_mask,
+ config->in_channels);
+ fb::ChannelLayoutFromMask(&out_layout, config->out_channel_layout_mask,
+ config->out_channels);
+
+ char filter_args[200];
+
+ // Create buffersrc (input)
+ snprintf(filter_args, sizeof(filter_args),
+ "time_base=1/%d:sample_rate=%d:sample_fmt=%d:channel_layout=0x%" PRIx64,
+ config->in_sample_rate, config->in_sample_rate,
+ config->in_sample_format, in_layout.u.mask);
+
+ int r = avfilter_graph_create_filter(&g->buffersrc,
+ avfilter_get_by_name("abuffer"), "in",
+ filter_args, nullptr, g->graph);
+ if (r < 0) {
+ av_channel_layout_uninit(&in_layout);
+ av_channel_layout_uninit(&out_layout);
+ fb_audio_graph_free(&g);
+ return nullptr;
+ }
+
+ AVFilterContext *previous_filter = g->buffersrc;
+
+ // Create tempo filter chain: FFmpeg's atempo can only be set between 0.5
+ // and 2.0, so out-of-range speeds must be daisychained.
+ bool create_tempo = config->tempo != 1.0;
+ if (create_tempo) {
+ double base = (config->tempo > 1.0) ? 2.0 : 0.5;
+ double speed_log = log(config->tempo) / log(base);
+ int whole = int(floor(speed_log));
+ speed_log -= whole;
+
+ for (int i = 0; i <= whole; i++) {
+ double filter_tempo = (i == whole) ? pow(base, speed_log) : base;
+ previous_filter =
+ CreateTempoFilter(g->graph, previous_filter, filter_tempo);
+ if (!previous_filter) {
+ av_channel_layout_uninit(&in_layout);
+ av_channel_layout_uninit(&out_layout);
+ fb_audio_graph_free(&g);
+ return nullptr;
+ }
+ }
+ }
+
+ // Create conversion filter if the parameters differ (or if the tempo
+ // filter converted planar input to packed and planar output is desired)
+ if (config->in_sample_rate != config->out_sample_rate ||
+ av_channel_layout_compare(&in_layout, &out_layout) != 0 ||
+ config->in_sample_format != config->out_sample_format ||
+ (config->out_is_planar && create_tempo)) {
+ snprintf(filter_args, sizeof(filter_args),
+ "sample_fmts=%s:sample_rates=%d:channel_layouts=0x%" PRIx64,
+ av_get_sample_fmt_name(
+ static_cast(config->out_sample_format)),
+ config->out_sample_rate, out_layout.u.mask);
+
+ AVFilterContext *c;
+ r = avfilter_graph_create_filter(&c, avfilter_get_by_name("aformat"),
+ "fmt", filter_args, nullptr, g->graph);
+ if (r < 0 || avfilter_link(previous_filter, 0, c, 0) < 0) {
+ av_channel_layout_uninit(&in_layout);
+ av_channel_layout_uninit(&out_layout);
+ fb_audio_graph_free(&g);
+ return nullptr;
+ }
+
+ previous_filter = c;
+ }
+
+ // Create buffersink (output)
+ r = avfilter_graph_create_filter(&g->buffersink,
+ avfilter_get_by_name("abuffersink"), "out",
+ nullptr, nullptr, g->graph);
+ if (r < 0 || avfilter_link(previous_filter, 0, g->buffersink, 0) < 0 ||
+ avfilter_graph_config(g->graph, nullptr) < 0) {
+ av_channel_layout_uninit(&in_layout);
+ av_channel_layout_uninit(&out_layout);
+ fb_audio_graph_free(&g);
+ return nullptr;
+ }
+
+ // Allocate the input frame used for pushes
+ g->in_frame = av_frame_alloc();
+ if (!g->in_frame) {
+ av_channel_layout_uninit(&in_layout);
+ av_channel_layout_uninit(&out_layout);
+ fb_audio_graph_free(&g);
+ return nullptr;
+ }
+ g->in_frame->sample_rate = config->in_sample_rate;
+ g->in_frame->format = config->in_sample_format;
+ g->in_frame->ch_layout = in_layout;
+ g->in_frame->pts = 0;
+
+ av_channel_layout_uninit(&out_layout);
+
+ g->in_channels = in_layout.nb_channels;
+ g->in_sample_rate = config->in_sample_rate;
+ g->in_sample_format = config->in_sample_format;
+
+ return g;
+}
+
+void fb_audio_graph_free(FBAudioGraph **graph)
+{
+ if (graph && *graph) {
+ FBAudioGraph *g = *graph;
+ if (g->graph) {
+ avfilter_graph_free(&g->graph);
+ }
+ if (g->in_frame) {
+ // in_frame owns a reference to its ch_layout (copied at create)
+ av_channel_layout_uninit(&g->in_frame->ch_layout);
+ av_frame_free(&g->in_frame);
+ }
+ delete g;
+ *graph = nullptr;
+ }
+}
+
+int fb_audio_graph_push(FBAudioGraph *graph,
+ const uint8_t *const *channel_data, int nb_samples)
+{
+ if (!graph) {
+ return AVERROR(EINVAL);
+ }
+
+ if (channel_data && nb_samples > 0) {
+ int bytes_per_sample =
+ av_get_bytes_per_sample(static_cast(graph->in_sample_format));
+
+ graph->in_frame->nb_samples = nb_samples;
+ for (int i = 0; i < graph->in_channels; i++) {
+ graph->in_frame->data[i] =
+ const_cast(channel_data[i]);
+ graph->in_frame->linesize[i] = bytes_per_sample * nb_samples;
+ }
+
+ int r = av_buffersrc_add_frame_flags(graph->buffersrc, graph->in_frame,
+ AV_BUFFERSRC_FLAG_KEEP_REF);
+ if (r < 0) {
+ return r;
+ }
+ } else {
+ // Flush
+ int r = av_buffersrc_add_frame_flags(graph->buffersrc, nullptr,
+ AV_BUFFERSRC_FLAG_KEEP_REF);
+ if (r < 0) {
+ return r;
+ }
+ }
+
+ return 0;
+}
+
+int fb_audio_graph_pull(FBAudioGraph *graph, FBFrame *out_frame)
+{
+ if (!graph || !out_frame) {
+ return AVERROR(EINVAL);
+ }
+
+ fb_frame_unref(out_frame);
+ int r = av_buffersink_get_frame(graph->buffersink, out_frame->frame);
+ if (r < 0) {
+ if (r == AVERROR(EAGAIN)) {
+ return 0;
+ }
+ return r;
+ }
+
+ return 1;
+}
diff --git a/ffmpeg_bridge/src/decoder.cpp b/ffmpeg_bridge/src/decoder.cpp
new file mode 100644
index 000000000..4217fe54e
--- /dev/null
+++ b/ffmpeg_bridge/src/decoder.cpp
@@ -0,0 +1,495 @@
+/***
+
+ 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 "internal.h"
+
+#include
+#include
+#include
+
+#include
+
+namespace
+{
+
+constexpr int64_t kAnalyzeDurationUs = 5000000;
+constexpr int64_t kProbeSizeBytes = 20000000;
+
+void ApplyFormatOpenOptions(AVDictionary **opts)
+{
+ av_dict_set_int(opts, "analyzeduration", kAnalyzeDurationUs, 0);
+ av_dict_set_int(opts, "probesize", kProbeSizeBytes, 0);
+}
+
+void TuneFormatContext(AVFormatContext *ctx)
+{
+ if (!ctx) {
+ return;
+ }
+
+ ctx->probesize = kProbeSizeBytes;
+ ctx->max_analyze_duration = kAnalyzeDurationUs;
+}
+
+void DiscardSubtitleStreams(AVFormatContext *ctx)
+{
+ if (!ctx) {
+ return;
+ }
+
+ for (unsigned int i = 0; i < ctx->nb_streams; i++) {
+ AVStream *stream = ctx->streams[i];
+ if (stream && stream->codecpar &&
+ stream->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
+ stream->discard = AVDISCARD_ALL;
+ }
+ }
+}
+
+} // namespace
+
+struct FBDecoder {
+ AVFormatContext *fmt_ctx = nullptr;
+ AVCodecContext *codec_ctx = nullptr;
+ AVStream *avstream = nullptr;
+ AVDictionary *opts = nullptr;
+
+ AVBufferRef *hw_device_ctx = nullptr;
+ AVHWDeviceType hw_device_type = AV_HWDEVICE_TYPE_NONE;
+ AVPixelFormat hw_pix_fmt = AV_PIX_FMT_NONE;
+ bool hwaccel_enabled = false;
+
+ bool Open(const char *filename, int stream_index);
+ void Close();
+
+ static AVHWDeviceType ChooseHardwareDevice();
+ static AVPixelFormat GetHardwareFormat(AVCodecContext *ctx,
+ const AVPixelFormat *pix_fmts);
+ bool InitHardwareAcceleration(const AVCodec *codec);
+ void CleanupHardwareAcceleration();
+};
+
+FBDecoder *fb_decoder_create(void)
+{
+ return new FBDecoder;
+}
+
+void fb_decoder_free(FBDecoder **decoder)
+{
+ if (decoder && *decoder) {
+ (*decoder)->Close();
+ delete *decoder;
+ *decoder = nullptr;
+ }
+}
+
+bool FBDecoder::Open(const char *filename, int stream_index)
+{
+ // Open file in a format context
+ AVDictionary *format_opts = nullptr;
+ ApplyFormatOpenOptions(&format_opts);
+ int error_code = avformat_open_input(&fmt_ctx, filename, nullptr, &format_opts);
+ av_dict_free(&format_opts);
+ TuneFormatContext(fmt_ctx);
+ DiscardSubtitleStreams(fmt_ctx);
+
+ if (error_code != 0) {
+ fprintf(stderr, "ffmpeg_bridge: failed to open input %s (%d)\n", filename,
+ error_code);
+ return false;
+ }
+
+ // Get stream information from format
+ error_code = avformat_find_stream_info(fmt_ctx, nullptr);
+ if (error_code < 0) {
+ fprintf(stderr, "ffmpeg_bridge: failed to find stream info (%d)\n",
+ error_code);
+ return false;
+ }
+
+ // Get reference to correct AVStream
+ avstream = fmt_ctx->streams[stream_index];
+
+ // Find decoder
+ const AVCodec *codec = avcodec_find_decoder(avstream->codecpar->codec_id);
+ if (codec == nullptr) {
+ fprintf(stderr, "ffmpeg_bridge: no decoder for codec %d\n",
+ avstream->codecpar->codec_id);
+ return false;
+ }
+
+ // Allocate context for the decoder
+ codec_ctx = avcodec_alloc_context3(codec);
+ if (codec_ctx == nullptr) {
+ fprintf(stderr, "ffmpeg_bridge: failed to allocate codec context\n");
+ return false;
+ }
+
+ // Copy parameters from the AVStream to the AVCodecContext
+ error_code = avcodec_parameters_to_context(codec_ctx, avstream->codecpar);
+ if (error_code < 0) {
+ fprintf(stderr, "ffmpeg_bridge: failed to copy codec parameters\n");
+ return false;
+ }
+
+ // Set multithreading setting
+ error_code = av_dict_set(&opts, "threads", "auto", 0);
+ if (error_code < 0) {
+ fprintf(stderr,
+ "ffmpeg_bridge: failed to set codec options, performance may suffer\n");
+ }
+
+ // Attempt hardware accelerated decoding first, then fall back to software.
+ if (InitHardwareAcceleration(codec)) {
+ error_code = avcodec_open2(codec_ctx, codec, &opts);
+ if (error_code == 0) {
+ hwaccel_enabled = true;
+ return true;
+ }
+
+ fprintf(stderr,
+ "ffmpeg_bridge: failed to open hardware codec, falling back to software (%d)\n",
+ error_code);
+
+ // Free the failed context and recreate it for software decoding.
+ avcodec_free_context(&codec_ctx);
+ CleanupHardwareAcceleration();
+
+ codec_ctx = avcodec_alloc_context3(codec);
+ if (codec_ctx == nullptr) {
+ fprintf(stderr,
+ "ffmpeg_bridge: failed to allocate codec context for software fallback\n");
+ return false;
+ }
+
+ error_code = avcodec_parameters_to_context(codec_ctx, avstream->codecpar);
+ if (error_code < 0) {
+ fprintf(stderr, "ffmpeg_bridge: failed to copy codec parameters\n");
+ return false;
+ }
+ }
+
+ // Open codec (software path, or if hardware was not available)
+ error_code = avcodec_open2(codec_ctx, codec, &opts);
+ if (error_code < 0) {
+ fprintf(stderr, "ffmpeg_bridge: failed to open codec %d (%d)\n", codec->id,
+ error_code);
+ return false;
+ }
+
+ return true;
+}
+
+AVHWDeviceType FBDecoder::ChooseHardwareDevice()
+{
+ if (getenv("OAK_DISABLE_HWACCEL") != nullptr) {
+ return AV_HWDEVICE_TYPE_NONE;
+ }
+
+#if defined(__linux__)
+ // Prefer NVIDIA's NVDEC where available, then VAAPI/VDPAU.
+ for (AVHWDeviceType type :
+ { AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_VAAPI, AV_HWDEVICE_TYPE_VDPAU }) {
+ if (av_hwdevice_find_type_by_name(av_hwdevice_get_type_name(type)) !=
+ AV_HWDEVICE_TYPE_NONE) {
+ return type;
+ }
+ }
+#elif defined(_WIN32)
+ for (AVHWDeviceType type :
+ { AV_HWDEVICE_TYPE_D3D11VA, AV_HWDEVICE_TYPE_DXVA2, AV_HWDEVICE_TYPE_CUDA }) {
+ if (av_hwdevice_find_type_by_name(av_hwdevice_get_type_name(type)) !=
+ AV_HWDEVICE_TYPE_NONE) {
+ return type;
+ }
+ }
+#elif defined(__APPLE__)
+ if (av_hwdevice_find_type_by_name(
+ av_hwdevice_get_type_name(AV_HWDEVICE_TYPE_VIDEOTOOLBOX)) !=
+ AV_HWDEVICE_TYPE_NONE) {
+ return AV_HWDEVICE_TYPE_VIDEOTOOLBOX;
+ }
+#endif
+ return AV_HWDEVICE_TYPE_NONE;
+}
+
+AVPixelFormat FBDecoder::GetHardwareFormat(AVCodecContext *ctx,
+ const AVPixelFormat *pix_fmts)
+{
+ const FBDecoder *inst = static_cast(ctx->opaque);
+ for (const AVPixelFormat *p = pix_fmts; *p != AV_PIX_FMT_NONE; p++) {
+ if (*p == inst->hw_pix_fmt) {
+ return *p;
+ }
+ }
+
+ fprintf(stderr,
+ "ffmpeg_bridge: hardware pixel format not supported by decoder, using first software format\n");
+ return pix_fmts[0];
+}
+
+bool FBDecoder::InitHardwareAcceleration(const AVCodec *codec)
+{
+ const AVHWDeviceType device_type = ChooseHardwareDevice();
+ if (device_type == AV_HWDEVICE_TYPE_NONE) {
+ return false;
+ }
+
+ // Find the pixel format associated with this device type for this codec.
+ hw_pix_fmt = AV_PIX_FMT_NONE;
+ for (int i = 0;; i++) {
+ const AVCodecHWConfig *config = avcodec_get_hw_config(codec, i);
+ if (!config) {
+ break;
+ }
+ if ((config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) &&
+ config->device_type == device_type) {
+ hw_pix_fmt = config->pix_fmt;
+ break;
+ }
+ }
+
+ if (hw_pix_fmt == AV_PIX_FMT_NONE) {
+ return false;
+ }
+
+ hw_device_type = device_type;
+
+ int ret = av_hwdevice_ctx_create(&hw_device_ctx, device_type, nullptr, nullptr,
+ 0);
+ if (ret < 0) {
+ fprintf(stderr,
+ "ffmpeg_bridge: failed to create hardware device context (%d)\n",
+ ret);
+ CleanupHardwareAcceleration();
+ return false;
+ }
+
+ codec_ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx);
+ codec_ctx->opaque = this;
+ codec_ctx->get_format = GetHardwareFormat;
+ // Most hardware decoders do not support frame threading.
+ av_dict_set(&opts, "threads", "1", 0);
+
+ return true;
+}
+
+void FBDecoder::CleanupHardwareAcceleration()
+{
+ hwaccel_enabled = false;
+ hw_device_type = AV_HWDEVICE_TYPE_NONE;
+ hw_pix_fmt = AV_PIX_FMT_NONE;
+
+ if (hw_device_ctx) {
+ av_buffer_unref(&hw_device_ctx);
+ hw_device_ctx = nullptr;
+ }
+}
+
+void FBDecoder::Close()
+{
+ if (opts) {
+ av_dict_free(&opts);
+ opts = nullptr;
+ }
+
+ if (codec_ctx) {
+ avcodec_free_context(&codec_ctx);
+ codec_ctx = nullptr;
+ }
+
+ CleanupHardwareAcceleration();
+
+ if (fmt_ctx) {
+ avformat_close_input(&fmt_ctx);
+ fmt_ctx = nullptr;
+ }
+
+ avstream = nullptr;
+}
+
+int fb_decoder_open(FBDecoder *decoder, const char *filename, int stream_index)
+{
+ if (!decoder || !filename) {
+ return AVERROR(EINVAL);
+ }
+ return decoder->Open(filename, stream_index) ? 0 : AVERROR_EXTERNAL;
+}
+
+void fb_decoder_close(FBDecoder *decoder)
+{
+ if (decoder) {
+ decoder->Close();
+ }
+}
+
+int fb_decoder_get_frame(FBDecoder *decoder, FBPacket *packet, FBFrame *frame)
+{
+ if (!decoder || !decoder->codec_ctx || !packet || !frame) {
+ return AVERROR(EINVAL);
+ }
+
+ AVPacket *pkt = packet->pkt;
+ AVFrame *frm = frame->frame;
+
+ bool eof = false;
+ int ret;
+
+ // Clear any previous frames
+ av_frame_unref(frm);
+
+ while ((ret = avcodec_receive_frame(decoder->codec_ctx, frm)) ==
+ AVERROR(EAGAIN) &&
+ !eof) {
+ // Find next packet in the correct stream index
+ ret = fb_decoder_get_packet(decoder, packet);
+
+ if (ret == AVERROR_EOF) {
+ // Don't break so that receive gets called again, but don't try to read again
+ eof = true;
+
+ // Send a null packet to signal end of stream
+ avcodec_send_packet(decoder->codec_ctx, nullptr);
+ } else if (ret < 0) {
+ // Handle other error by breaking loop and returning the code we received
+ break;
+ } else {
+ // Successful read, send the packet
+ ret = avcodec_send_packet(decoder->codec_ctx, pkt);
+
+ // We don't need the packet anymore, so free it
+ av_packet_unref(pkt);
+
+ if (ret < 0) {
+ break;
+ }
+ }
+ }
+
+ return ret;
+}
+
+int fb_decoder_get_packet(FBDecoder *decoder, FBPacket *packet)
+{
+ if (!decoder || !decoder->fmt_ctx || !packet) {
+ return AVERROR(EINVAL);
+ }
+
+ AVPacket *pkt = packet->pkt;
+ int ret;
+
+ do {
+ av_packet_unref(pkt);
+ ret = av_read_frame(decoder->fmt_ctx, pkt);
+ } while (pkt->stream_index != decoder->avstream->index && ret >= 0);
+
+ return ret;
+}
+
+void fb_decoder_seek(FBDecoder *decoder, int64_t timestamp)
+{
+ if (!decoder || !decoder->fmt_ctx) {
+ return;
+ }
+
+ avcodec_flush_buffers(decoder->codec_ctx);
+ av_seek_frame(decoder->fmt_ctx, decoder->avstream->index, timestamp,
+ AVSEEK_FLAG_BACKWARD);
+}
+
+int fb_decoder_get_stream_info(const FBDecoder *decoder, FBStreamInfo *out)
+{
+ if (!decoder || !decoder->avstream || !out) {
+ return AVERROR(EINVAL);
+ }
+
+ const AVStream *s = decoder->avstream;
+ const AVCodecParameters *par = s->codecpar;
+
+ memset(out, 0, sizeof(*out));
+ out->index = s->index;
+ out->codec_type = par->codec_type;
+ out->codec_id = par->codec_id;
+ out->has_decoder = 1; // stream is open, so a decoder was found
+ out->width = par->width;
+ out->height = par->height;
+ out->pixel_format = par->format;
+ out->field_order = decoder->codec_ctx ? decoder->codec_ctx->field_order :
+ AV_FIELD_UNKNOWN;
+ out->color_range = par->color_range;
+ out->sample_rate = par->sample_rate;
+ out->sample_format = par->format;
+ out->channel_layout_mask = fb::ValidateStreamChannelLayoutMask(s);
+ out->start_time = s->start_time;
+ out->duration = s->duration;
+ out->time_base_num = s->time_base.num;
+ out->time_base_den = s->time_base.den;
+ out->avg_frame_rate_num = s->avg_frame_rate.num;
+ out->avg_frame_rate_den = s->avg_frame_rate.den;
+
+ return 0;
+}
+
+int64_t fb_decoder_get_format_start_time(const FBDecoder *decoder)
+{
+ if (!decoder || !decoder->fmt_ctx) {
+ return FB_NOPTS_VALUE;
+ }
+ return decoder->fmt_ctx->start_time;
+}
+
+int fb_decoder_guess_sample_aspect_ratio(const FBDecoder *decoder,
+ FBFrame *frame, int *num, int *den)
+{
+ if (!decoder || !decoder->fmt_ctx || !num || !den) {
+ return AVERROR(EINVAL);
+ }
+
+ AVRational r = av_guess_sample_aspect_ratio(
+ decoder->fmt_ctx, decoder->avstream, frame ? frame->frame : nullptr);
+ *num = r.num;
+ *den = r.den;
+ return 0;
+}
+
+int fb_decoder_guess_frame_rate(const FBDecoder *decoder, FBFrame *frame,
+ int *num, int *den)
+{
+ if (!decoder || !decoder->fmt_ctx || !num || !den) {
+ return AVERROR(EINVAL);
+ }
+
+ AVRational r = av_guess_frame_rate(decoder->fmt_ctx, decoder->avstream,
+ frame ? frame->frame : nullptr);
+ *num = r.num;
+ *den = r.den;
+ return 0;
+}
+
+int fb_decoder_hwaccel_enabled(const FBDecoder *decoder)
+{
+ return decoder && decoder->hwaccel_enabled;
+}
+
+int fb_decoder_hw_pix_fmt(const FBDecoder *decoder)
+{
+ return decoder ? int(decoder->hw_pix_fmt) : FB_PIX_FMT_NONE;
+}
diff --git a/ffmpeg_bridge/src/encoder.cpp b/ffmpeg_bridge/src/encoder.cpp
new file mode 100644
index 000000000..89aa0e7e6
--- /dev/null
+++ b/ffmpeg_bridge/src/encoder.cpp
@@ -0,0 +1,1117 @@
+/***
+
+ 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 "internal.h"
+
+#ifdef __GNUC__
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+#endif
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+namespace
+{
+
+AVPixelFormat ConvertJPEGSpaceToRegularSpace(AVPixelFormat f)
+{
+ switch (f) {
+ case AV_PIX_FMT_YUVJ420P:
+ return AV_PIX_FMT_YUV420P;
+ case AV_PIX_FMT_YUVJ422P:
+ return AV_PIX_FMT_YUV422P;
+ case AV_PIX_FMT_YUVJ444P:
+ return AV_PIX_FMT_YUV444P;
+ case AV_PIX_FMT_YUVJ440P:
+ return AV_PIX_FMT_YUV440P;
+ case AV_PIX_FMT_YUVJ411P:
+ return AV_PIX_FMT_YUV411P;
+ default:
+ break;
+ }
+
+ return f;
+}
+
+const AVCodec *FindEncoder(int codec, int sample_format)
+{
+ switch (codec) {
+ case FB_CODEC_H264:
+ return avcodec_find_encoder_by_name("libx264");
+ case FB_CODEC_H264RGB:
+ return avcodec_find_encoder_by_name("libx264rgb");
+ case FB_CODEC_DNXHD:
+ return avcodec_find_encoder(AV_CODEC_ID_DNXHD);
+ case FB_CODEC_PRORES:
+ return avcodec_find_encoder(AV_CODEC_ID_PRORES);
+ case FB_CODEC_CINEFORM:
+ return avcodec_find_encoder(AV_CODEC_ID_CFHD);
+ case FB_CODEC_H265:
+ return avcodec_find_encoder(AV_CODEC_ID_HEVC);
+ case FB_CODEC_VP9:
+ return avcodec_find_encoder(AV_CODEC_ID_VP9);
+ case FB_CODEC_AV1: {
+ const AVCodec *encoder = avcodec_find_encoder_by_name("libsvtav1");
+ if (!encoder) {
+ encoder = avcodec_find_encoder(AV_CODEC_ID_AV1);
+ }
+ return encoder;
+ }
+ case FB_CODEC_OPENEXR:
+ return avcodec_find_encoder(AV_CODEC_ID_EXR);
+ case FB_CODEC_PNG:
+ return avcodec_find_encoder(AV_CODEC_ID_PNG);
+ case FB_CODEC_TIFF:
+ return avcodec_find_encoder(AV_CODEC_ID_TIFF);
+ case FB_CODEC_MP2:
+ return avcodec_find_encoder(AV_CODEC_ID_MP2);
+ case FB_CODEC_MP3:
+ return avcodec_find_encoder(AV_CODEC_ID_MP3);
+ case FB_CODEC_AAC:
+ return avcodec_find_encoder(AV_CODEC_ID_AAC);
+ case FB_CODEC_PCM:
+ switch (sample_format) {
+ case FB_SAMPLE_FMT_U8:
+ return avcodec_find_encoder(AV_CODEC_ID_PCM_U8);
+ case FB_SAMPLE_FMT_S16:
+ return avcodec_find_encoder(AV_CODEC_ID_PCM_S16LE);
+ case FB_SAMPLE_FMT_S32:
+ return avcodec_find_encoder(AV_CODEC_ID_PCM_S32LE);
+ case FB_SAMPLE_FMT_S64:
+ return avcodec_find_encoder(AV_CODEC_ID_PCM_S64LE);
+ case FB_SAMPLE_FMT_FLT:
+ return avcodec_find_encoder(AV_CODEC_ID_PCM_F32LE);
+ case FB_SAMPLE_FMT_DBL:
+ return avcodec_find_encoder(AV_CODEC_ID_PCM_F64LE);
+ default:
+ break;
+ }
+ break;
+ case FB_CODEC_FLAC:
+ return avcodec_find_encoder(AV_CODEC_ID_FLAC);
+ case FB_CODEC_OPUS:
+ return avcodec_find_encoder(AV_CODEC_ID_OPUS);
+ case FB_CODEC_VORBIS:
+ return avcodec_find_encoder(AV_CODEC_ID_VORBIS);
+ case FB_CODEC_SRT:
+ return avcodec_find_encoder(AV_CODEC_ID_SUBRIP);
+ default:
+ break;
+ }
+
+ return nullptr;
+}
+
+} // namespace
+
+struct FBEncoder {
+ // Deep-copied configuration
+ std::string filename;
+
+ int video_enabled = 0;
+ int video_codec = FB_CODEC_NONE;
+ int video_width = 0;
+ int video_height = 0;
+ int video_pixel_aspect_num = 1;
+ int video_pixel_aspect_den = 1;
+ int video_time_base_num = 0;
+ int video_time_base_den = 1;
+ int video_frame_rate_num = 0;
+ int video_frame_rate_den = 1;
+ std::string video_pix_fmt;
+ int video_src_pix_fmt = FB_PIX_FMT_NONE;
+ int video_color_range = FB_COLOR_RANGE_UNSPEC;
+ int video_field_order = FB_FIELD_ORDER_PROGRESSIVE;
+ int64_t video_bit_rate = 0;
+ int64_t video_min_bit_rate = 0;
+ int64_t video_max_bit_rate = 0;
+ int64_t video_buffer_size = 0;
+ int video_threads = 0;
+ int video_color_srgb = 0;
+ std::vector> video_opts;
+
+ int audio_enabled = 0;
+ int audio_codec = FB_CODEC_NONE;
+ int audio_sample_rate = 0;
+ uint64_t audio_channel_layout_mask = 0;
+ int audio_sample_format = FB_SAMPLE_FMT_NONE;
+ int64_t audio_bit_rate = 0;
+
+ int subtitles_enabled = 0;
+ int subtitle_codec = FB_CODEC_NONE;
+ std::vector subtitle_header;
+
+ // Runtime state
+ AVFormatContext *fmt_ctx = nullptr;
+
+ AVStream *video_stream = nullptr;
+ AVCodecContext *video_codec_ctx = nullptr;
+ AVFilterGraph *video_scale_ctx = nullptr;
+ AVFilterContext *video_buffersrc_ctx = nullptr;
+ AVFilterContext *video_buffersink_ctx = nullptr;
+
+ AVStream *audio_stream = nullptr;
+ AVCodecContext *audio_codec_ctx = nullptr;
+ SwrContext *audio_resample_ctx = nullptr;
+ AVFrame *audio_frame = nullptr;
+ int audio_max_samples = 0;
+ int audio_frame_offset = 0;
+ int64_t audio_write_count = 0;
+
+ AVStream *subtitle_stream = nullptr;
+ AVCodecContext *subtitle_codec_ctx = nullptr;
+
+ bool open = false;
+
+ char error[1024] = { 0 };
+
+ void SetError(const char *context, int error_code)
+ {
+ fb::SetError(error, sizeof(error), context, error_code);
+ }
+
+ void SetError(const char *message)
+ {
+ snprintf(error, sizeof(error), "%s", message);
+ }
+
+ bool WriteAVFrame(AVFrame *frame, AVCodecContext *codec_ctx,
+ AVStream *stream);
+ bool InitializeStream(AVMediaType type, AVStream **stream,
+ AVCodecContext **codec_ctx, int codec);
+ bool InitializeCodecContext(AVStream **stream, AVCodecContext **codec_ctx,
+ const AVCodec *codec);
+ bool SetupCodecContext(AVStream *stream, AVCodecContext *codec_ctx,
+ const AVCodec *codec);
+ void FlushEncoders();
+ void FlushCodecCtx(AVCodecContext *codec_ctx, AVStream *stream);
+ bool InitializeResampleContext(int sample_format, int sample_rate,
+ uint64_t channel_layout_mask);
+ bool WriteAudioData(int sample_format, int sample_rate,
+ uint64_t channel_layout_mask, const uint8_t **input_data,
+ int input_sample_count);
+};
+
+FBEncoder *fb_encoder_create(const FBEncoderConfig *config)
+{
+ if (!config || !config->filename) {
+ return nullptr;
+ }
+
+ FBEncoder *e = new FBEncoder;
+
+ e->filename = config->filename;
+
+ e->video_enabled = config->video_enabled;
+ e->video_codec = config->video_codec;
+ e->video_width = config->video_width;
+ e->video_height = config->video_height;
+ e->video_pixel_aspect_num = config->video_pixel_aspect_num;
+ e->video_pixel_aspect_den = config->video_pixel_aspect_den;
+ e->video_time_base_num = config->video_time_base_num;
+ e->video_time_base_den = config->video_time_base_den;
+ e->video_frame_rate_num = config->video_frame_rate_num;
+ e->video_frame_rate_den = config->video_frame_rate_den;
+ if (config->video_pix_fmt) {
+ e->video_pix_fmt = config->video_pix_fmt;
+ }
+ e->video_src_pix_fmt = config->video_src_pix_fmt;
+ e->video_color_range = config->video_color_range;
+ e->video_field_order = config->video_field_order;
+ e->video_bit_rate = config->video_bit_rate;
+ e->video_min_bit_rate = config->video_min_bit_rate;
+ e->video_max_bit_rate = config->video_max_bit_rate;
+ e->video_buffer_size = config->video_buffer_size;
+ e->video_threads = config->video_threads;
+ e->video_color_srgb = config->video_color_srgb;
+ for (int i = 0; i < config->video_opt_count; i++) {
+ if (config->video_opt_keys[i] && config->video_opt_values[i]) {
+ e->video_opts.emplace_back(config->video_opt_keys[i],
+ config->video_opt_values[i]);
+ }
+ }
+
+ e->audio_enabled = config->audio_enabled;
+ e->audio_codec = config->audio_codec;
+ e->audio_sample_rate = config->audio_sample_rate;
+ e->audio_channel_layout_mask = config->audio_channel_layout_mask;
+ e->audio_sample_format = config->audio_sample_format;
+ e->audio_bit_rate = config->audio_bit_rate;
+
+ e->subtitles_enabled = config->subtitles_enabled;
+ e->subtitle_codec = config->subtitle_codec;
+ if (config->subtitle_header && config->subtitle_header_size > 0) {
+ e->subtitle_header.assign(config->subtitle_header,
+ config->subtitle_header +
+ config->subtitle_header_size);
+ }
+
+ return e;
+}
+
+void fb_encoder_free(FBEncoder **encoder)
+{
+ if (encoder && *encoder) {
+ fb_encoder_close(*encoder);
+ delete *encoder;
+ *encoder = nullptr;
+ }
+}
+
+int fb_encoder_open(FBEncoder *e)
+{
+ if (!e) {
+ return AVERROR(EINVAL);
+ }
+
+ if (e->open) {
+ return 0;
+ }
+
+ int error_code;
+
+ // Create output format context
+ error_code = avformat_alloc_output_context2(&e->fmt_ctx, nullptr, nullptr,
+ e->filename.c_str());
+ if (error_code < 0) {
+ e->SetError("Failed to allocate output context", error_code);
+ return error_code;
+ }
+
+ // Initialize a video stream if it's enabled
+ if (e->video_enabled) {
+ if (!e->InitializeStream(AVMEDIA_TYPE_VIDEO, &e->video_stream,
+ &e->video_codec_ctx, e->video_codec)) {
+ return AVERROR_EXTERNAL;
+ }
+
+ // This is the pixel format the encoder wants to encode to
+ AVPixelFormat encoder_pix_fmt = e->video_codec_ctx->pix_fmt;
+
+ e->video_scale_ctx = avfilter_graph_alloc();
+ if (!e->video_scale_ctx) {
+ e->SetError("Failed to allocate filter graph");
+ return AVERROR_EXTERNAL;
+ }
+
+ static const int FILTER_ARG_SZ = 1024;
+ char filter_args[FILTER_ARG_SZ];
+
+ snprintf(filter_args, FILTER_ARG_SZ,
+ "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
+ e->video_width, e->video_height, e->video_src_pix_fmt,
+ e->video_time_base_num, e->video_time_base_den,
+ e->video_pixel_aspect_num, e->video_pixel_aspect_den);
+
+ avfilter_graph_create_filter(&e->video_buffersrc_ctx,
+ avfilter_get_by_name("buffer"), "in",
+ filter_args, nullptr, e->video_scale_ctx);
+ avfilter_graph_create_filter(&e->video_buffersink_ctx,
+ avfilter_get_by_name("buffersink"), "out",
+ nullptr, nullptr, e->video_scale_ctx);
+
+ AVFilterContext *last_filter = e->video_buffersrc_ctx;
+
+ {
+ // Set color range
+ AVFilterContext *range_filter;
+
+ snprintf(filter_args, FILTER_ARG_SZ, "in_range=full:out_range=%s",
+ e->video_color_range == FB_COLOR_RANGE_JPEG ? "full" :
+ "limited");
+
+ avfilter_graph_create_filter(&range_filter,
+ avfilter_get_by_name("scale"), "range",
+ filter_args, nullptr,
+ e->video_scale_ctx);
+
+ avfilter_link(last_filter, 0, range_filter, 0);
+ last_filter = range_filter;
+ }
+
+ if (e->video_src_pix_fmt != encoder_pix_fmt) {
+ // Transform pixel format
+ AVFilterContext *format_filter;
+
+ snprintf(filter_args, FILTER_ARG_SZ, "pix_fmts=%u", encoder_pix_fmt);
+
+ avfilter_graph_create_filter(&format_filter,
+ avfilter_get_by_name("format"),
+ "format", filter_args, nullptr,
+ e->video_scale_ctx);
+
+ avfilter_link(last_filter, 0, format_filter, 0);
+ last_filter = format_filter;
+ }
+
+ avfilter_link(last_filter, 0, e->video_buffersink_ctx, 0);
+
+ if (avfilter_graph_config(e->video_scale_ctx, nullptr) < 0) {
+ e->SetError("Failed to configure filter graph");
+ return AVERROR_EXTERNAL;
+ }
+ }
+
+ // Initialize an audio stream if it's enabled
+ if (e->audio_enabled) {
+ if (!e->InitializeStream(AVMEDIA_TYPE_AUDIO, &e->audio_stream,
+ &e->audio_codec_ctx, e->audio_codec)) {
+ return AVERROR_EXTERNAL;
+ }
+ }
+
+ // Initialize a subtitle stream if it's enabled
+ if (e->subtitles_enabled) {
+ if (!e->InitializeStream(AVMEDIA_TYPE_SUBTITLE, &e->subtitle_stream,
+ &e->subtitle_codec_ctx, e->subtitle_codec)) {
+ return AVERROR_EXTERNAL;
+ }
+ }
+
+ av_dump_format(e->fmt_ctx, 0, e->filename.c_str(), 1);
+
+ // Open output file for writing
+ error_code = avio_open(&e->fmt_ctx->pb, e->filename.c_str(), AVIO_FLAG_WRITE);
+ if (error_code < 0) {
+ e->SetError("Failed to open IO context", error_code);
+ return error_code;
+ }
+
+ // Write header
+ error_code = avformat_write_header(e->fmt_ctx, nullptr);
+ if (error_code < 0) {
+ e->SetError("Failed to write format header", error_code);
+ return error_code;
+ }
+
+ e->open = true;
+ return 0;
+}
+
+int fb_encoder_write_video_frame(FBEncoder *e, int width, int height,
+ int pix_fmt, const uint8_t *data, int linesize,
+ double time_seconds)
+{
+ if (!e || !e->open || !data) {
+ return AVERROR(EINVAL);
+ }
+
+ // Use the filter graph to convert formats/linesizes
+ AVFrame *input_frame = av_frame_alloc();
+ if (!input_frame) {
+ e->SetError("Failed to allocate input frame");
+ return AVERROR(ENOMEM);
+ }
+
+ input_frame->width = width;
+ input_frame->height = height;
+ input_frame->format = pix_fmt;
+ input_frame->data[0] = const_cast(data);
+ input_frame->linesize[0] = linesize;
+
+ input_frame->color_primaries = e->video_codec_ctx->color_primaries;
+ input_frame->color_trc = e->video_codec_ctx->color_trc;
+ input_frame->colorspace = e->video_codec_ctx->colorspace;
+ input_frame->color_range = e->video_codec_ctx->color_range;
+
+ int r = av_buffersrc_add_frame_flags(e->video_buffersrc_ctx, input_frame,
+ AV_BUFFERSRC_FLAG_KEEP_REF);
+ av_frame_free(&input_frame);
+ if (r < 0) {
+ e->SetError("Failed to add frame to filter graph", r);
+ return r;
+ }
+
+ AVFrame *encoded_frame = av_frame_alloc();
+ if (!encoded_frame) {
+ e->SetError("Failed to allocate encode frame");
+ return AVERROR(ENOMEM);
+ }
+
+ r = av_buffersink_get_frame(e->video_buffersink_ctx, encoded_frame);
+ if (r < 0) {
+ av_frame_free(&encoded_frame);
+ e->SetError("Failed to retrieve frame from buffer sink", r);
+ return r;
+ }
+
+ encoded_frame->pts =
+ llround(time_seconds / av_q2d(e->video_codec_ctx->time_base));
+
+ bool result =
+ e->WriteAVFrame(encoded_frame, e->video_codec_ctx, e->video_stream);
+
+ av_frame_free(&encoded_frame);
+
+ return result ? 0 : AVERROR_EXTERNAL;
+}
+
+bool FBEncoder::WriteAudioData(int sample_format, int sample_rate,
+ uint64_t channel_layout_mask,
+ const uint8_t **input_data,
+ int input_sample_count)
+{
+ if (!InitializeResampleContext(sample_format, sample_rate,
+ channel_layout_mask)) {
+ SetError("Failed to initialize resample context");
+ return false;
+ }
+
+ bool result = true;
+
+ // Create output buffer
+ int output_sample_count =
+ input_sample_count ?
+ swr_get_out_samples(audio_resample_ctx, input_sample_count) :
+ 102400;
+ uint8_t **output_data = nullptr;
+ int output_linesize;
+ av_samples_alloc_array_and_samples(
+ &output_data, &output_linesize,
+ audio_stream->codecpar->ch_layout.nb_channels, output_sample_count,
+ static_cast(audio_stream->codecpar->format), 0);
+
+ // Perform conversion
+ int converted = swr_convert(audio_resample_ctx, output_data,
+ output_sample_count, input_data,
+ input_sample_count);
+ if (converted > 0) {
+ // Split sample buffer into frames
+ for (int i = 0; i < converted;) {
+ int frame_remaining_samples = audio_max_samples - audio_frame_offset;
+ int converted_remaining_samples = converted - i;
+
+ int copy_length =
+ frame_remaining_samples < converted_remaining_samples ?
+ frame_remaining_samples :
+ converted_remaining_samples;
+
+ av_samples_copy(audio_frame->data, output_data, audio_frame_offset,
+ i, copy_length, audio_frame->ch_layout.nb_channels,
+ static_cast(audio_frame->format));
+
+ audio_frame_offset += copy_length;
+ i += copy_length;
+
+ if (audio_frame_offset == audio_max_samples ||
+ (i == converted && !input_data)) {
+ // Got all the samples we needed, write the frame
+ audio_frame->pts = av_rescale_q(
+ audio_write_count, { 1, audio_codec_ctx->sample_rate },
+ audio_codec_ctx->time_base);
+
+ WriteAVFrame(audio_frame, audio_codec_ctx, audio_stream);
+ audio_write_count += audio_frame_offset;
+ audio_frame_offset = 0;
+ }
+ }
+ } else if (converted < 0) {
+ SetError("Failed to resample audio", converted);
+ result = false;
+ }
+
+ if (!input_data && audio_frame_offset > 0) {
+ audio_frame->nb_samples = audio_frame_offset;
+ audio_frame->pts =
+ av_rescale_q(audio_write_count, { 1, audio_codec_ctx->sample_rate },
+ audio_codec_ctx->time_base);
+ WriteAVFrame(audio_frame, audio_codec_ctx, audio_stream);
+ }
+
+ // Free buffers created
+ if (output_data) {
+ av_freep(&output_data[0]);
+ av_freep(&output_data);
+ }
+
+ return result;
+}
+
+int fb_encoder_write_audio(FBEncoder *e, const uint8_t *const *channel_data,
+ int channels, int sample_format, int sample_rate,
+ uint64_t channel_layout_mask, int64_t sample_count)
+{
+ if (!e || !e->open) {
+ return AVERROR(EINVAL);
+ }
+
+ if (!channel_data || sample_count == 0) {
+ // Nothing to write (matches the historical empty-buffer early-out)
+ return 0;
+ }
+
+ int bytes_per_sample =
+ av_get_bytes_per_sample(static_cast(sample_format));
+
+ bool result = true;
+
+ size_t start = 0;
+ size_t end = size_t(sample_count);
+ const size_t max_frame = 48000;
+
+ while (result && start < end) {
+ // Create input buffer
+ uint8_t **input_data = nullptr;
+ size_t input_sample_count =
+ (end - start) < max_frame ? (end - start) : max_frame;
+ int input_linesize;
+
+ int r = av_samples_alloc_array_and_samples(
+ &input_data, &input_linesize, channels, int(input_sample_count),
+ static_cast(sample_format), 0);
+
+ if (r < 0) {
+ e->SetError("Failed to allocate sample array", r);
+ return r;
+ } else {
+ for (int i = 0; i < channels; i++) {
+ memcpy(input_data[i],
+ channel_data[i] + start * size_t(bytes_per_sample),
+ input_sample_count * size_t(bytes_per_sample));
+ }
+
+ start += input_sample_count;
+ }
+
+ result = e->WriteAudioData(sample_format, sample_rate,
+ channel_layout_mask,
+ const_cast(input_data),
+ int(input_sample_count));
+
+ if (input_data) {
+ av_freep(&input_data[0]);
+ av_freep(&input_data);
+ }
+ }
+
+ return result ? 0 : AVERROR_EXTERNAL;
+}
+
+int fb_encoder_write_subtitle(FBEncoder *e, const char *utf8_text,
+ double in_seconds, double duration_seconds)
+{
+ if (!e || !e->open || !utf8_text) {
+ return AVERROR(EINVAL);
+ }
+
+ AVPacket *pkt = av_packet_alloc();
+ if (!pkt) {
+ return AVERROR(ENOMEM);
+ }
+
+ pkt->stream_index = e->subtitle_stream->index;
+ pkt->data = reinterpret_cast(const_cast(utf8_text));
+ pkt->size = int(strlen(utf8_text));
+
+ // Convert seconds to the codec timebase, rounding down
+ double d = in_seconds / av_q2d(e->subtitle_codec_ctx->time_base);
+ const double eps = 0.000000000001;
+ int64_t pts;
+ if (d > ceil(d) - eps) {
+ pts = int64_t(ceil(d));
+ } else {
+ pts = int64_t(floor(d));
+ }
+ pkt->pts = pts;
+
+ pkt->duration = av_rescale_q(llround(duration_seconds * 1000), { 1, 1000 },
+ e->subtitle_codec_ctx->time_base);
+ pkt->dts = pkt->pts;
+ av_packet_rescale_ts(pkt, e->subtitle_codec_ctx->time_base,
+ e->subtitle_stream->time_base);
+
+ int err = av_interleaved_write_frame(e->fmt_ctx, pkt);
+ bool ret = true;
+
+ if (err < 0) {
+ e->SetError("Failed to write interleaved packet", err);
+ ret = false;
+ }
+
+ av_packet_free(&pkt);
+
+ return ret ? 0 : err;
+}
+
+void fb_encoder_close(FBEncoder *e)
+{
+ if (!e) {
+ return;
+ }
+
+ if (e->open) {
+ // Flush encoders
+ e->FlushEncoders();
+
+ // We've written a header, so we'll write a trailer
+ av_write_trailer(e->fmt_ctx);
+ avio_closep(&e->fmt_ctx->pb);
+
+ e->open = false;
+ }
+
+ if (e->audio_resample_ctx) {
+ swr_free(&e->audio_resample_ctx);
+ e->audio_resample_ctx = nullptr;
+ }
+
+ if (e->audio_frame) {
+ av_frame_free(&e->audio_frame);
+ e->audio_frame = nullptr;
+ }
+
+ if (e->video_scale_ctx) {
+ avfilter_graph_free(&e->video_scale_ctx);
+ e->video_scale_ctx = nullptr;
+ e->video_buffersrc_ctx = nullptr;
+ e->video_buffersink_ctx = nullptr;
+ }
+
+ if (e->video_codec_ctx) {
+ avcodec_free_context(&e->video_codec_ctx);
+ e->video_codec_ctx = nullptr;
+ }
+
+ if (e->audio_codec_ctx) {
+ avcodec_free_context(&e->audio_codec_ctx);
+ e->audio_codec_ctx = nullptr;
+ }
+
+ if (e->subtitle_codec_ctx) {
+ avcodec_free_context(&e->subtitle_codec_ctx);
+ e->subtitle_codec_ctx = nullptr;
+ }
+
+ if (e->fmt_ctx) {
+ // NOTE: This also frees the streams
+ avformat_free_context(e->fmt_ctx);
+ e->fmt_ctx = nullptr;
+ e->video_stream = nullptr;
+ e->audio_stream = nullptr;
+ e->subtitle_stream = nullptr;
+ }
+}
+
+const char *fb_encoder_get_error(const FBEncoder *encoder)
+{
+ return encoder ? encoder->error : "";
+}
+
+bool FBEncoder::WriteAVFrame(AVFrame *frame, AVCodecContext *codec_ctx,
+ AVStream *stream)
+{
+ // Send raw frame to the encoder
+ int error_code = avcodec_send_frame(codec_ctx, frame);
+ if (error_code < 0) {
+ SetError("Failed to send frame to encoder", error_code);
+ return false;
+ }
+
+ bool succeeded = false;
+
+ AVPacket *pkt = av_packet_alloc();
+
+ // Retrieve packets from encoder
+ while (error_code >= 0) {
+ error_code = avcodec_receive_packet(codec_ctx, pkt);
+
+ // EAGAIN just means the encoder wants another frame before encoding
+ if (error_code == AVERROR(EAGAIN)) {
+ break;
+ } else if (error_code < 0) {
+ SetError("Failed to receive packet from decoder", error_code);
+ goto fail;
+ }
+
+ // Set packet stream index
+ pkt->stream_index = stream->index;
+
+ av_packet_rescale_ts(pkt, codec_ctx->time_base, stream->time_base);
+
+ // Write packet to file
+ error_code = av_interleaved_write_frame(fmt_ctx, pkt);
+ if (error_code < 0) {
+ SetError("Failed to write interleaved packet", error_code);
+ goto fail;
+ }
+
+ // Unref packet in case we're getting another
+ av_packet_unref(pkt);
+ }
+
+ succeeded = true;
+
+fail:
+ av_packet_free(&pkt);
+
+ return succeeded;
+}
+
+bool FBEncoder::InitializeStream(AVMediaType type, AVStream **stream_ptr,
+ AVCodecContext **codec_ctx_ptr, int codec)
+{
+ if (type != AVMEDIA_TYPE_VIDEO && type != AVMEDIA_TYPE_AUDIO &&
+ type != AVMEDIA_TYPE_SUBTITLE) {
+ SetError("Cannot initialize a stream that is not a video, audio, or subtitle type");
+ return false;
+ }
+
+ // Find encoder
+ const AVCodec *encoder = FindEncoder(codec, audio_sample_format);
+ if (!encoder) {
+ char msg[128];
+ snprintf(msg, sizeof(msg), "Failed to find codec for 0x%x", codec);
+ SetError(msg);
+ return false;
+ }
+
+ if (encoder->type != type) {
+ SetError("Retrieved unexpected codec type for codec");
+ return false;
+ }
+
+ if (!InitializeCodecContext(stream_ptr, codec_ctx_ptr, encoder)) {
+ return false;
+ }
+
+ // Set codec parameters
+ AVCodecContext *codec_ctx = *codec_ctx_ptr;
+ AVStream *stream = *stream_ptr;
+
+ if (type == AVMEDIA_TYPE_VIDEO) {
+ codec_ctx->width = video_width;
+ codec_ctx->height = video_height;
+ codec_ctx->sample_aspect_ratio = { video_pixel_aspect_num,
+ video_pixel_aspect_den };
+ codec_ctx->time_base = { video_time_base_num, video_time_base_den };
+ codec_ctx->framerate = { video_frame_rate_num, video_frame_rate_den };
+ codec_ctx->pix_fmt = av_get_pix_fmt(video_pix_fmt.c_str());
+ codec_ctx->color_range = video_color_range == FB_COLOR_RANGE_JPEG ?
+ AVCOL_RANGE_JPEG :
+ AVCOL_RANGE_MPEG;
+
+ if (video_field_order != FB_FIELD_ORDER_PROGRESSIVE) {
+ // FIXME: I actually don't know what these flags do, the documentation helpfully doesn't
+ // explain them at all. I hope using both of them is the right thing to do.
+ codec_ctx->flags |= AV_CODEC_FLAG_INTERLACED_DCT |
+ AV_CODEC_FLAG_INTERLACED_ME;
+
+ if (video_field_order == FB_FIELD_ORDER_TT) {
+ codec_ctx->field_order = AV_FIELD_TT;
+ } else {
+ codec_ctx->field_order = AV_FIELD_BB;
+
+ if (video_codec == FB_CODEC_H264 ||
+ video_codec == FB_CODEC_H264RGB) {
+ // For some reason, FFmpeg doesn't set libx264's bff flag so we have to do it ourselves
+ av_opt_set(codec_ctx->priv_data, "x264opts", "bff=1",
+ AV_OPT_SEARCH_CHILDREN);
+ }
+ }
+ }
+
+ // Set custom options
+ for (const auto &opt : video_opts) {
+ av_opt_set(codec_ctx->priv_data, opt.first.c_str(),
+ opt.second.c_str(), AV_OPT_SEARCH_CHILDREN);
+ }
+
+ if (video_bit_rate > 0) {
+ codec_ctx->bit_rate = video_bit_rate;
+ }
+
+ if (video_min_bit_rate > 0) {
+ codec_ctx->rc_min_rate = video_min_bit_rate;
+ }
+
+ if (video_max_bit_rate > 0) {
+ codec_ctx->rc_max_rate = video_max_bit_rate;
+ }
+
+ if (video_buffer_size > 0) {
+ codec_ctx->rc_buffer_size = static_cast(video_buffer_size);
+ }
+
+ // nclc tags. See https://ffmpeg.org/doxygen/4.0/pixfmt_8h.html#ad384ee5a840bafd73daef08e6d9cafe7
+ if (video_color_srgb) {
+ codec_ctx->color_primaries = AVCOL_PRI_BT709;
+ codec_ctx->color_trc = AVCOL_TRC_IEC61966_2_1;
+ codec_ctx->colorspace = AVCOL_SPC_BT709;
+ } else { // Assume Rec.709
+ codec_ctx->color_primaries = AVCOL_PRI_BT709;
+ codec_ctx->color_trc = AVCOL_TRC_BT709;
+ codec_ctx->colorspace = AVCOL_SPC_BT709;
+ }
+
+ } else if (type == AVMEDIA_TYPE_AUDIO) {
+ codec_ctx->sample_rate = audio_sample_rate;
+ av_channel_layout_from_mask(&codec_ctx->ch_layout,
+ audio_channel_layout_mask);
+ codec_ctx->sample_fmt =
+ static_cast(audio_sample_format);
+ codec_ctx->time_base = { 1, codec_ctx->sample_rate };
+
+ if (audio_bit_rate > 0) {
+ codec_ctx->bit_rate = audio_bit_rate;
+ }
+
+ } else if (type == AVMEDIA_TYPE_SUBTITLE) {
+ codec_ctx->time_base = av_get_time_base_q();
+
+ if (!subtitle_header.empty()) {
+ codec_ctx->subtitle_header =
+ new uint8_t[subtitle_header.size()];
+ memcpy(codec_ctx->subtitle_header, subtitle_header.data(),
+ subtitle_header.size());
+ codec_ctx->subtitle_header_size = int(subtitle_header.size());
+ }
+ }
+
+ if (!SetupCodecContext(stream, codec_ctx, encoder)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool FBEncoder::InitializeCodecContext(AVStream **stream,
+ AVCodecContext **codec_ctx,
+ const AVCodec *codec)
+{
+ *stream = avformat_new_stream(fmt_ctx, nullptr);
+ if (!(*stream)) {
+ SetError("Failed to allocate AVStream");
+ return false;
+ }
+
+ // Allocate a codec context
+ *codec_ctx = avcodec_alloc_context3(codec);
+ if (!(*codec_ctx)) {
+ SetError("Failed to allocate AVCodecContext");
+ return false;
+ }
+
+ return true;
+}
+
+bool FBEncoder::SetupCodecContext(AVStream *stream, AVCodecContext *codec_ctx,
+ const AVCodec *codec)
+{
+ int error_code;
+
+ if (fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
+ codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+ }
+
+ AVDictionary *codec_opts = nullptr;
+
+ // Set thread count
+ if (video_threads == 0) {
+ av_dict_set(&codec_opts, "threads", "auto", 0);
+ } else {
+ char thread_val[16];
+ snprintf(thread_val, sizeof(thread_val), "%d", video_threads);
+ av_dict_set(&codec_opts, "threads", thread_val, 0);
+ }
+
+ // Try to open encoder
+ error_code = avcodec_open2(codec_ctx, codec, &codec_opts);
+ av_dict_free(&codec_opts);
+ if (error_code < 0) {
+ SetError("Failed to open encoder", error_code);
+ return false;
+ }
+
+ // Copy context settings to codecpar object
+ error_code = avcodec_parameters_from_context(stream->codecpar, codec_ctx);
+ if (error_code < 0) {
+ SetError("Failed to copy codec parameters to stream", error_code);
+ return false;
+ }
+
+ if (codec->type == AVMEDIA_TYPE_VIDEO) {
+ stream->avg_frame_rate = codec_ctx->framerate;
+ }
+
+ return true;
+}
+
+void FBEncoder::FlushEncoders()
+{
+ if (video_codec_ctx) {
+ FlushCodecCtx(video_codec_ctx, video_stream);
+ }
+
+ if (audio_codec_ctx) {
+ FlushCodecCtx(audio_codec_ctx, audio_stream);
+ }
+
+ if (fmt_ctx) {
+ if (fmt_ctx->oformat->flags) {
+ int r = av_interleaved_write_frame(fmt_ctx, nullptr);
+ if (r < 0) {
+ SetError("Failed to write interleaved packet", r);
+ }
+ }
+ }
+}
+
+void FBEncoder::FlushCodecCtx(AVCodecContext *codec_ctx, AVStream *stream)
+{
+ avcodec_send_frame(codec_ctx, nullptr);
+ AVPacket *pkt = av_packet_alloc();
+
+ int error_code;
+ do {
+ error_code = avcodec_receive_packet(codec_ctx, pkt);
+
+ if (error_code < 0) {
+ break;
+ }
+
+ pkt->stream_index = stream->index;
+ av_packet_rescale_ts(pkt, codec_ctx->time_base, stream->time_base);
+ int r = av_interleaved_write_frame(fmt_ctx, pkt);
+ if (r < 0) {
+ SetError("Failed to write interleaved packet", r);
+ break;
+ }
+ av_packet_unref(pkt);
+ } while (error_code >= 0);
+
+ av_packet_free(&pkt);
+}
+
+bool FBEncoder::InitializeResampleContext(int sample_format, int sample_rate,
+ uint64_t channel_layout_mask)
+{
+ if (audio_resample_ctx) {
+ return true;
+ }
+
+ AVChannelLayout layout;
+ fb::ChannelLayoutFromMask(&layout, channel_layout_mask, 0);
+
+ // Create resample context
+ swr_alloc_set_opts2(&audio_resample_ctx, &audio_codec_ctx->ch_layout,
+ audio_codec_ctx->sample_fmt,
+ audio_codec_ctx->sample_rate, &layout,
+ static_cast(sample_format), sample_rate,
+ 0, nullptr);
+ av_channel_layout_uninit(&layout);
+
+ if (!audio_resample_ctx) {
+ return false;
+ }
+
+ int err = swr_init(audio_resample_ctx);
+ if (err < 0) {
+ SetError("Failed to create resampling context", err);
+ return false;
+ }
+
+ audio_max_samples = audio_codec_ctx->frame_size;
+ if (!audio_max_samples) {
+ // If not set, use another frame size
+ if (video_enabled) {
+ // If we're encoding video, use enough samples to cover roughly one frame of video
+ audio_max_samples =
+ int(int64_t(audio_sample_rate) * video_time_base_num /
+ video_time_base_den);
+ } else {
+ // If no video, just use an arbitrary number
+ audio_max_samples = 256;
+ }
+ }
+
+ audio_frame = av_frame_alloc();
+ if (!audio_frame) {
+ return false;
+ }
+
+ audio_frame->ch_layout = audio_codec_ctx->ch_layout;
+ audio_frame->format = audio_codec_ctx->sample_fmt;
+ audio_frame->nb_samples = audio_max_samples;
+
+ err = av_frame_get_buffer(audio_frame, 0);
+ if (err < 0) {
+ SetError("Failed to create audio frame", err);
+ return false;
+ }
+
+ audio_frame_offset = 0;
+ audio_write_count = 0;
+
+ return true;
+}
+
+int fb_encoder_codec_get_pixel_formats(int codec, const char **names,
+ int max_names)
+{
+ const AVCodec *codec_info = FindEncoder(codec, FB_SAMPLE_FMT_NONE);
+ if (!codec_info || !codec_info->pix_fmts) {
+ return 0;
+ }
+
+ int count = 0;
+ for (int i = 0; codec_info->pix_fmts[i] != AV_PIX_FMT_NONE; i++) {
+ AVPixelFormat fmt = codec_info->pix_fmts[i];
+ if (ConvertJPEGSpaceToRegularSpace(fmt) != fmt) {
+ // This is a deprecated "JPEG" space, skip it
+ continue;
+ }
+
+ if (names && count < max_names) {
+ names[count] = av_get_pix_fmt_name(fmt);
+ }
+ count++;
+ }
+
+ return count;
+}
+
+int fb_encoder_codec_get_sample_formats(int codec, int *fmts, int max_fmts)
+{
+ const AVCodec *codec_info = FindEncoder(codec, FB_SAMPLE_FMT_NONE);
+ if (!codec_info || !codec_info->sample_fmts) {
+ return 0;
+ }
+
+ int count = 0;
+ for (int i = 0; codec_info->sample_fmts[i] != AV_SAMPLE_FMT_NB; i++) {
+ if (fmts && count < max_fmts) {
+ fmts[count] = codec_info->sample_fmts[i];
+ }
+ count++;
+ }
+
+ return count;
+}
+
+#ifdef __GNUC__
+#pragma GCC diagnostic pop
+#endif
diff --git a/ffmpeg_bridge/src/frame.cpp b/ffmpeg_bridge/src/frame.cpp
new file mode 100644
index 000000000..9df99c26c
--- /dev/null
+++ b/ffmpeg_bridge/src/frame.cpp
@@ -0,0 +1,239 @@
+/***
+
+ 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 "internal.h"
+
+#include
+
+FBFrame *fb_frame_alloc(void)
+{
+ AVFrame *avf = av_frame_alloc();
+ if (!avf) {
+ return nullptr;
+ }
+
+ FBFrame *f = new FBFrame;
+ f->frame = avf;
+ return f;
+}
+
+void fb_frame_free(FBFrame **frame)
+{
+ if (frame && *frame) {
+ av_frame_free(&(*frame)->frame);
+ delete *frame;
+ *frame = nullptr;
+ }
+}
+
+void fb_frame_unref(FBFrame *frame)
+{
+ if (frame && frame->frame) {
+ av_frame_unref(frame->frame);
+ }
+}
+
+int fb_frame_get_buffer(FBFrame *frame, int align)
+{
+ if (!frame || !frame->frame) {
+ return AVERROR(EINVAL);
+ }
+ return av_frame_get_buffer(frame->frame, align);
+}
+
+int fb_frame_copy_props(FBFrame *dst, const FBFrame *src)
+{
+ if (!dst || !src) {
+ return AVERROR(EINVAL);
+ }
+ return av_frame_copy_props(dst->frame, src->frame);
+}
+
+int fb_frame_hw_transfer_data(FBFrame *dst, const FBFrame *src)
+{
+ if (!dst || !src) {
+ return AVERROR(EINVAL);
+ }
+ return av_hwframe_transfer_data(dst->frame, src->frame, 0);
+}
+
+int fb_frame_is_hw(const FBFrame *frame)
+{
+ return frame && frame->frame && frame->frame->hw_frames_ctx != nullptr;
+}
+
+int fb_frame_get_width(const FBFrame *frame)
+{
+ return frame ? frame->frame->width : 0;
+}
+
+void fb_frame_set_width(FBFrame *frame, int width)
+{
+ if (frame) {
+ frame->frame->width = width;
+ }
+}
+
+int fb_frame_get_height(const FBFrame *frame)
+{
+ return frame ? frame->frame->height : 0;
+}
+
+void fb_frame_set_height(FBFrame *frame, int height)
+{
+ if (frame) {
+ frame->frame->height = height;
+ }
+}
+
+int fb_frame_get_format(const FBFrame *frame)
+{
+ return frame ? frame->frame->format : FB_PIX_FMT_NONE;
+}
+
+void fb_frame_set_format(FBFrame *frame, int format)
+{
+ if (frame) {
+ frame->frame->format = format;
+ }
+}
+
+int64_t fb_frame_get_pts(const FBFrame *frame)
+{
+ return frame ? frame->frame->pts : FB_NOPTS_VALUE;
+}
+
+void fb_frame_set_pts(FBFrame *frame, int64_t pts)
+{
+ if (frame) {
+ frame->frame->pts = pts;
+ }
+}
+
+int64_t fb_frame_get_best_effort_timestamp(const FBFrame *frame)
+{
+ return frame ? frame->frame->best_effort_timestamp : FB_NOPTS_VALUE;
+}
+
+int fb_frame_get_nb_samples(const FBFrame *frame)
+{
+ return frame ? frame->frame->nb_samples : 0;
+}
+
+void fb_frame_set_nb_samples(FBFrame *frame, int nb_samples)
+{
+ if (frame) {
+ frame->frame->nb_samples = nb_samples;
+ }
+}
+
+int fb_frame_get_sample_rate(const FBFrame *frame)
+{
+ return frame ? frame->frame->sample_rate : 0;
+}
+
+void fb_frame_set_sample_rate(FBFrame *frame, int sample_rate)
+{
+ if (frame) {
+ frame->frame->sample_rate = sample_rate;
+ }
+}
+
+int fb_frame_get_color_range(const FBFrame *frame)
+{
+ return frame ? int(frame->frame->color_range) : FB_COLOR_RANGE_UNSPEC;
+}
+
+void fb_frame_set_color_range(FBFrame *frame, int color_range)
+{
+ if (frame) {
+ frame->frame->color_range = static_cast(color_range);
+ }
+}
+
+int fb_frame_get_colorspace(const FBFrame *frame)
+{
+ return frame ? int(frame->frame->colorspace) : FB_COL_SPC_UNSPEC;
+}
+
+void fb_frame_set_colorspace(FBFrame *frame, int colorspace)
+{
+ if (frame) {
+ frame->frame->colorspace = static_cast(colorspace);
+ }
+}
+
+uint64_t fb_frame_get_channel_layout_mask(const FBFrame *frame)
+{
+ if (!frame) {
+ return 0;
+ }
+ if (frame->frame->ch_layout.order == AV_CHANNEL_ORDER_NATIVE) {
+ return frame->frame->ch_layout.u.mask;
+ }
+ return 0;
+}
+
+void fb_frame_set_channel_layout_mask(FBFrame *frame, uint64_t mask)
+{
+ if (frame) {
+ av_channel_layout_uninit(&frame->frame->ch_layout);
+ av_channel_layout_from_mask(&frame->frame->ch_layout, mask);
+ }
+}
+
+uint8_t *fb_frame_get_data(FBFrame *frame, int plane)
+{
+ if (!frame || plane < 0 || plane >= AV_NUM_DATA_POINTERS) {
+ return nullptr;
+ }
+ return frame->frame->data[plane];
+}
+
+const uint8_t *fb_frame_get_data_const(const FBFrame *frame, int plane)
+{
+ if (!frame || plane < 0 || plane >= AV_NUM_DATA_POINTERS) {
+ return nullptr;
+ }
+ return frame->frame->data[plane];
+}
+
+void fb_frame_set_data(FBFrame *frame, int plane, uint8_t *data)
+{
+ if (frame && plane >= 0 && plane < AV_NUM_DATA_POINTERS) {
+ frame->frame->data[plane] = data;
+ }
+}
+
+int fb_frame_get_linesize(const FBFrame *frame, int plane)
+{
+ if (!frame || plane < 0 || plane >= AV_NUM_DATA_POINTERS) {
+ return 0;
+ }
+ return frame->frame->linesize[plane];
+}
+
+void fb_frame_set_linesize(FBFrame *frame, int plane, int linesize)
+{
+ if (frame && plane >= 0 && plane < AV_NUM_DATA_POINTERS) {
+ frame->frame->linesize[plane] = linesize;
+ }
+}
diff --git a/ffmpeg_bridge/src/internal.h b/ffmpeg_bridge/src/internal.h
new file mode 100644
index 000000000..2fd259c20
--- /dev/null
+++ b/ffmpeg_bridge/src/internal.h
@@ -0,0 +1,169 @@
+/***
+
+ 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 FFMPEG_BRIDGE_INTERNAL_H
+#define FFMPEG_BRIDGE_INTERNAL_H
+
+// Fixes weird define issue when including
+#include
+
+extern "C" {
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+}
+
+#include "ffmpeg_bridge/ffmpeg_bridge.h"
+
+/* ------------------------------------------------------------------------- */
+/* Compile-time verification that the public constants match FFmpeg */
+/* ------------------------------------------------------------------------- */
+
+#ifdef __GNUC__
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wenum-compare"
+#endif
+
+static_assert(FB_ERROR_EOF == AVERROR_EOF, "FB_ERROR_EOF mismatch");
+static_assert(FB_NOPTS_VALUE == AV_NOPTS_VALUE, "FB_NOPTS_VALUE mismatch");
+static_assert(FB_TIME_BASE == AV_TIME_BASE, "FB_TIME_BASE mismatch");
+static_assert(FB_SCALER_POINT == SWS_POINT, "FB_SCALER_POINT mismatch");
+
+static_assert(FB_PIX_FMT_NONE == AV_PIX_FMT_NONE, "FB_PIX_FMT_NONE mismatch");
+static_assert(FB_PIX_FMT_YUV420P == AV_PIX_FMT_YUV420P, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_RGB24 == AV_PIX_FMT_RGB24, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUV422P == AV_PIX_FMT_YUV422P, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUV444P == AV_PIX_FMT_YUV444P, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUV410P == AV_PIX_FMT_YUV410P, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUV411P == AV_PIX_FMT_YUV411P, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_GRAY8 == AV_PIX_FMT_GRAY8, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUVJ420P == AV_PIX_FMT_YUVJ420P, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUVJ422P == AV_PIX_FMT_YUVJ422P, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUVJ444P == AV_PIX_FMT_YUVJ444P, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_RGBA == AV_PIX_FMT_RGBA, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_GRAY16LE == AV_PIX_FMT_GRAY16LE, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUVJ440P == AV_PIX_FMT_YUVJ440P, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_RGB48LE == AV_PIX_FMT_RGB48LE, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUV420P10LE == AV_PIX_FMT_YUV420P10LE, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUV422P10LE == AV_PIX_FMT_YUV422P10LE, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUV444P10LE == AV_PIX_FMT_YUV444P10LE, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_RGBA64LE == AV_PIX_FMT_RGBA64LE, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUV420P12LE == AV_PIX_FMT_YUV420P12LE, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUV422P12LE == AV_PIX_FMT_YUV422P12LE, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUV444P12LE == AV_PIX_FMT_YUV444P12LE, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_YUVJ411P == AV_PIX_FMT_YUVJ411P, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_GRAYF32LE == AV_PIX_FMT_GRAYF32LE, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_RGBAF16LE == AV_PIX_FMT_RGBAF16LE, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_RGBF32LE == AV_PIX_FMT_RGBF32LE, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_RGBAF32LE == AV_PIX_FMT_RGBAF32LE, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_RGBF16LE == AV_PIX_FMT_RGBF16LE, "pixfmt mismatch");
+static_assert(FB_PIX_FMT_GRAYF16LE == AV_PIX_FMT_GRAYF16LE, "pixfmt mismatch");
+
+static_assert(FB_SAMPLE_FMT_NONE == AV_SAMPLE_FMT_NONE, "samplefmt mismatch");
+static_assert(FB_SAMPLE_FMT_U8 == AV_SAMPLE_FMT_U8, "samplefmt mismatch");
+static_assert(FB_SAMPLE_FMT_S16 == AV_SAMPLE_FMT_S16, "samplefmt mismatch");
+static_assert(FB_SAMPLE_FMT_S32 == AV_SAMPLE_FMT_S32, "samplefmt mismatch");
+static_assert(FB_SAMPLE_FMT_FLT == AV_SAMPLE_FMT_FLT, "samplefmt mismatch");
+static_assert(FB_SAMPLE_FMT_DBL == AV_SAMPLE_FMT_DBL, "samplefmt mismatch");
+static_assert(FB_SAMPLE_FMT_U8P == AV_SAMPLE_FMT_U8P, "samplefmt mismatch");
+static_assert(FB_SAMPLE_FMT_S16P == AV_SAMPLE_FMT_S16P, "samplefmt mismatch");
+static_assert(FB_SAMPLE_FMT_S32P == AV_SAMPLE_FMT_S32P, "samplefmt mismatch");
+static_assert(FB_SAMPLE_FMT_FLTP == AV_SAMPLE_FMT_FLTP, "samplefmt mismatch");
+static_assert(FB_SAMPLE_FMT_DBLP == AV_SAMPLE_FMT_DBLP, "samplefmt mismatch");
+static_assert(FB_SAMPLE_FMT_S64 == AV_SAMPLE_FMT_S64, "samplefmt mismatch");
+static_assert(FB_SAMPLE_FMT_S64P == AV_SAMPLE_FMT_S64P, "samplefmt mismatch");
+
+static_assert(FB_COLOR_RANGE_UNSPEC == AVCOL_RANGE_UNSPECIFIED, "range mismatch");
+static_assert(FB_COLOR_RANGE_MPEG == AVCOL_RANGE_MPEG, "range mismatch");
+static_assert(FB_COLOR_RANGE_JPEG == AVCOL_RANGE_JPEG, "range mismatch");
+
+static_assert(FB_COL_SPC_RGB == AVCOL_SPC_RGB, "colspace mismatch");
+static_assert(FB_COL_SPC_BT709 == AVCOL_SPC_BT709, "colspace mismatch");
+static_assert(FB_COL_SPC_UNSPEC == AVCOL_SPC_UNSPECIFIED, "colspace mismatch");
+static_assert(FB_COL_SPC_FCC == AVCOL_SPC_FCC, "colspace mismatch");
+static_assert(FB_COL_SPC_BT470BG == AVCOL_SPC_BT470BG, "colspace mismatch");
+static_assert(FB_COL_SPC_SMPTE170M == AVCOL_SPC_SMPTE170M, "colspace mismatch");
+static_assert(FB_COL_SPC_SMPTE240M == AVCOL_SPC_SMPTE240M, "colspace mismatch");
+static_assert(FB_COL_SPC_BT2020_NCL == AVCOL_SPC_BT2020_NCL, "colspace mismatch");
+
+static_assert(FB_MEDIA_TYPE_VIDEO == AVMEDIA_TYPE_VIDEO, "mediatype mismatch");
+static_assert(FB_MEDIA_TYPE_AUDIO == AVMEDIA_TYPE_AUDIO, "mediatype mismatch");
+static_assert(FB_MEDIA_TYPE_DATA == AVMEDIA_TYPE_DATA, "mediatype mismatch");
+static_assert(FB_MEDIA_TYPE_SUBTITLE == AVMEDIA_TYPE_SUBTITLE, "mediatype mismatch");
+
+static_assert(FB_FIELD_ORDER_UNKNOWN == AV_FIELD_UNKNOWN, "fieldorder mismatch");
+static_assert(FB_FIELD_ORDER_PROGRESSIVE == AV_FIELD_PROGRESSIVE, "fieldorder mismatch");
+static_assert(FB_FIELD_ORDER_TT == AV_FIELD_TT, "fieldorder mismatch");
+static_assert(FB_FIELD_ORDER_BB == AV_FIELD_BB, "fieldorder mismatch");
+static_assert(FB_FIELD_ORDER_TB == AV_FIELD_TB, "fieldorder mismatch");
+static_assert(FB_FIELD_ORDER_BT == AV_FIELD_BT, "fieldorder mismatch");
+
+static_assert(FB_CH_LAYOUT_MONO == AV_CH_LAYOUT_MONO, "layout mismatch");
+static_assert(FB_CH_LAYOUT_STEREO == AV_CH_LAYOUT_STEREO, "layout mismatch");
+static_assert(FB_CH_LAYOUT_2_1 == AV_CH_LAYOUT_2_1, "layout mismatch");
+static_assert(FB_CH_LAYOUT_5POINT1 == AV_CH_LAYOUT_5POINT1, "layout mismatch");
+static_assert(FB_CH_LAYOUT_7POINT1 == AV_CH_LAYOUT_7POINT1, "layout mismatch");
+
+#ifdef __GNUC__
+#pragma GCC diagnostic pop
+#endif
+
+/* ------------------------------------------------------------------------- */
+/* Handle bodies */
+/* ------------------------------------------------------------------------- */
+
+struct FBFrame {
+ AVFrame *frame;
+};
+
+struct FBPacket {
+ AVPacket *pkt;
+};
+
+namespace fb
+{
+
+/** Allocate a channel layout from a mask, falling back to a default layout
+ * derived from `fallback_channels` when the mask is zero. */
+void ChannelLayoutFromMask(AVChannelLayout *layout, uint64_t mask,
+ int fallback_channels);
+
+/** Validate a stream's channel layout, returning a usable mask (never zero
+ * unless the stream truly has no channels). */
+uint64_t ValidateStreamChannelLayoutMask(const AVStream *stream);
+
+/** Map an AVColorSpace to the corresponding SWS_CS_* constant. */
+int SwsColorspaceFromAVColorSpace(AVColorSpace cs);
+
+void SetError(char *error_buffer, size_t error_buffer_size, const char *context,
+ int error_code);
+
+} // namespace fb
+
+#endif // FFMPEG_BRIDGE_INTERNAL_H
diff --git a/ffmpeg_bridge/src/packet.cpp b/ffmpeg_bridge/src/packet.cpp
new file mode 100644
index 000000000..21f334332
--- /dev/null
+++ b/ffmpeg_bridge/src/packet.cpp
@@ -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 .
+
+***/
+
+#include "internal.h"
+
+FBPacket *fb_packet_alloc(void)
+{
+ AVPacket *avp = av_packet_alloc();
+ if (!avp) {
+ return nullptr;
+ }
+
+ FBPacket *p = new FBPacket;
+ p->pkt = avp;
+ return p;
+}
+
+void fb_packet_free(FBPacket **packet)
+{
+ if (packet && *packet) {
+ av_packet_free(&(*packet)->pkt);
+ delete *packet;
+ *packet = nullptr;
+ }
+}
+
+void fb_packet_unref(FBPacket *packet)
+{
+ if (packet && packet->pkt) {
+ av_packet_unref(packet->pkt);
+ }
+}
+
+int64_t fb_packet_get_pts(const FBPacket *packet)
+{
+ return packet ? packet->pkt->pts : FB_NOPTS_VALUE;
+}
+
+int64_t fb_packet_get_duration(const FBPacket *packet)
+{
+ return packet ? packet->pkt->duration : 0;
+}
+
+int fb_packet_get_size(const FBPacket *packet)
+{
+ return packet ? packet->pkt->size : 0;
+}
+
+const uint8_t *fb_packet_get_data(const FBPacket *packet)
+{
+ return packet ? packet->pkt->data : nullptr;
+}
+
+int fb_packet_get_stream_index(const FBPacket *packet)
+{
+ return packet ? packet->pkt->stream_index : -1;
+}
diff --git a/ffmpeg_bridge/src/probe.cpp b/ffmpeg_bridge/src/probe.cpp
new file mode 100644
index 000000000..65ff9b639
--- /dev/null
+++ b/ffmpeg_bridge/src/probe.cpp
@@ -0,0 +1,324 @@
+/***
+
+ 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 "internal.h"
+
+#include
+#include
+
+struct FBProbe {
+ AVFormatContext *fmt_ctx = nullptr;
+};
+
+namespace
+{
+
+constexpr int64_t kAnalyzeDurationUs = 5000000;
+constexpr int64_t kProbeSizeBytes = 20000000;
+
+void FillStreamInfo(const AVStream *s, int has_decoder, FBStreamInfo *out)
+{
+ const AVCodecParameters *par = s->codecpar;
+
+ memset(out, 0, sizeof(*out));
+ out->index = s->index;
+ out->codec_type = par->codec_type;
+ out->codec_id = par->codec_id;
+ out->has_decoder = has_decoder;
+ out->width = par->width;
+ out->height = par->height;
+ out->pixel_format = par->format;
+ out->field_order = FB_FIELD_ORDER_UNKNOWN;
+ out->color_range = par->color_range;
+ out->sample_rate = par->sample_rate;
+ out->sample_format = par->format;
+ out->channel_layout_mask = fb::ValidateStreamChannelLayoutMask(s);
+ out->start_time = s->start_time;
+ out->duration = s->duration;
+ out->time_base_num = s->time_base.num;
+ out->time_base_den = s->time_base.den;
+ out->avg_frame_rate_num = s->avg_frame_rate.num;
+ out->avg_frame_rate_den = s->avg_frame_rate.den;
+}
+
+bool IsCancelled(FBCancelCallback cancel, void *userdata)
+{
+ return cancel && cancel(userdata);
+}
+
+} // namespace
+
+FBProbe *fb_probe_create(void)
+{
+ return new FBProbe;
+}
+
+void fb_probe_free(FBProbe **probe)
+{
+ if (probe && *probe) {
+ fb_probe_close(*probe);
+ delete *probe;
+ *probe = nullptr;
+ }
+}
+
+int fb_probe_open(FBProbe *probe, const char *filename)
+{
+ if (!probe || !filename) {
+ return AVERROR(EINVAL);
+ }
+
+ AVDictionary *format_opts = nullptr;
+ av_dict_set_int(&format_opts, "analyzeduration", kAnalyzeDurationUs, 0);
+ av_dict_set_int(&format_opts, "probesize", kProbeSizeBytes, 0);
+
+ AVFormatContext *ctx = nullptr;
+ int error_code = avformat_open_input(&ctx, filename, nullptr, &format_opts);
+ av_dict_free(&format_opts);
+
+ if (ctx) {
+ ctx->probesize = kProbeSizeBytes;
+ ctx->max_analyze_duration = kAnalyzeDurationUs;
+
+ // Subtitle streams are read on demand, don't let them slow down probing
+ for (unsigned int i = 0; i < ctx->nb_streams; i++) {
+ AVStream *stream = ctx->streams[i];
+ if (stream && stream->codecpar &&
+ stream->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
+ stream->discard = AVDISCARD_ALL;
+ }
+ }
+ }
+
+ if (error_code != 0) {
+ return error_code;
+ }
+
+ avformat_find_stream_info(ctx, nullptr);
+
+ probe->fmt_ctx = ctx;
+ return 0;
+}
+
+void fb_probe_close(FBProbe *probe)
+{
+ if (probe && probe->fmt_ctx) {
+ avformat_close_input(&probe->fmt_ctx);
+ probe->fmt_ctx = nullptr;
+ }
+}
+
+int fb_probe_get_stream_count(const FBProbe *probe)
+{
+ if (!probe || !probe->fmt_ctx) {
+ return 0;
+ }
+ return int(probe->fmt_ctx->nb_streams);
+}
+
+int fb_probe_get_stream_info(const FBProbe *probe, int stream_index,
+ FBStreamInfo *out)
+{
+ if (!probe || !probe->fmt_ctx || !out || stream_index < 0 ||
+ stream_index >= int(probe->fmt_ctx->nb_streams)) {
+ return AVERROR(EINVAL);
+ }
+
+ const AVStream *s = probe->fmt_ctx->streams[stream_index];
+ int has_decoder = avcodec_find_decoder(s->codecpar->codec_id) != nullptr;
+ FillStreamInfo(s, has_decoder, out);
+ return 0;
+}
+
+int64_t fb_probe_get_duration(const FBProbe *probe)
+{
+ if (!probe || !probe->fmt_ctx) {
+ return FB_NOPTS_VALUE;
+ }
+ return probe->fmt_ctx->duration;
+}
+
+int64_t fb_probe_get_start_time(const FBProbe *probe)
+{
+ if (!probe || !probe->fmt_ctx) {
+ return FB_NOPTS_VALUE;
+ }
+ return probe->fmt_ctx->start_time;
+}
+
+int fb_probe_duration_from_bitrate(const FBProbe *probe)
+{
+ if (!probe || !probe->fmt_ctx) {
+ return 0;
+ }
+ return probe->fmt_ctx->duration_estimation_method ==
+ AVFMT_DURATION_FROM_BITRATE;
+}
+
+int fb_probe_get_metadata(FBProbe *probe, int stream_index, const char *key,
+ char *buffer, int buffer_size)
+{
+ if (!probe || !probe->fmt_ctx || !key || !buffer || buffer_size <= 0) {
+ return 0;
+ }
+
+ AVDictionary *metadata = nullptr;
+ if (stream_index < 0) {
+ metadata = probe->fmt_ctx->metadata;
+ } else if (stream_index < int(probe->fmt_ctx->nb_streams)) {
+ metadata = probe->fmt_ctx->streams[stream_index]->metadata;
+ } else {
+ return 0;
+ }
+
+ AVDictionaryEntry *entry =
+ av_dict_get(metadata, key, nullptr, AV_DICT_IGNORE_SUFFIX);
+ if (!entry) {
+ return 0;
+ }
+
+ snprintf(buffer, size_t(buffer_size), "%s", entry->value);
+ return 1;
+}
+
+int fb_probe_video_stream_details(const char *filename, int stream_index,
+ FBVideoStreamDetails *out,
+ int decode_full_duration,
+ FBCancelCallback cancel,
+ void *cancel_userdata)
+{
+ if (!filename || !out) {
+ return AVERROR(EINVAL);
+ }
+
+ memset(out, 0, sizeof(*out));
+ out->field_order = FB_FIELD_ORDER_PROGRESSIVE;
+ out->pixel_aspect_num = 1;
+ out->pixel_aspect_den = 1;
+ out->decoded_duration = FB_NOPTS_VALUE;
+
+ FBDecoder *decoder = fb_decoder_create();
+ if (fb_decoder_open(decoder, filename, stream_index) < 0) {
+ fb_decoder_free(&decoder);
+ return AVERROR_EXTERNAL;
+ }
+
+ FBStreamInfo info;
+ if (fb_decoder_get_stream_info(decoder, &info) == 0) {
+ out->field_order = info.field_order;
+ out->frame_rate_num = info.avg_frame_rate_num;
+ out->frame_rate_den = info.avg_frame_rate_den;
+ }
+
+ FBPacket *pkt = fb_packet_alloc();
+ FBFrame *frame = fb_frame_alloc();
+
+ int ret = 0;
+
+ // Read at least one frame to get more information about this video stream
+ if (fb_decoder_get_frame(decoder, pkt, frame) >= 0) {
+ fb_decoder_guess_sample_aspect_ratio(decoder, frame,
+ &out->pixel_aspect_num,
+ &out->pixel_aspect_den);
+ fb_decoder_guess_frame_rate(decoder, frame, &out->frame_rate_num,
+ &out->frame_rate_den);
+ }
+
+ ret = fb_decoder_get_frame(decoder, pkt, frame);
+ if (ret == FB_ERROR_EOF) {
+ // Only one frame exists: this is a still image
+ out->is_still = 1;
+ } else if (decode_full_duration) {
+ // Decode until the end to determine the true duration
+ int64_t last_ts = fb_frame_get_best_effort_timestamp(frame);
+ while (fb_decoder_get_frame(decoder, pkt, frame) >= 0 &&
+ !IsCancelled(cancel, cancel_userdata)) {
+ last_ts = fb_frame_get_best_effort_timestamp(frame);
+ }
+ out->decoded_duration = last_ts;
+ }
+
+ fb_frame_free(&frame);
+ fb_packet_free(&pkt);
+ fb_decoder_free(&decoder);
+
+ return 0;
+}
+
+int fb_probe_audio_stream_duration(const char *filename, int stream_index,
+ int64_t *out_duration,
+ FBCancelCallback cancel,
+ void *cancel_userdata)
+{
+ if (!filename || !out_duration) {
+ return AVERROR(EINVAL);
+ }
+
+ FBDecoder *decoder = fb_decoder_create();
+ if (fb_decoder_open(decoder, filename, stream_index) < 0) {
+ fb_decoder_free(&decoder);
+ return AVERROR_EXTERNAL;
+ }
+
+ FBPacket *pkt = fb_packet_alloc();
+ FBFrame *frame = fb_frame_alloc();
+
+ int64_t duration = 0;
+ do {
+ duration = fb_frame_get_best_effort_timestamp(frame);
+ } while (fb_decoder_get_frame(decoder, pkt, frame) >= 0 &&
+ !IsCancelled(cancel, cancel_userdata));
+
+ fb_frame_free(&frame);
+ fb_packet_free(&pkt);
+ fb_decoder_free(&decoder);
+
+ *out_duration = duration;
+ return 0;
+}
+
+int fb_probe_read_subtitle_stream(const char *filename, int stream_index,
+ FBSubtitleCallback callback,
+ void *userdata)
+{
+ if (!filename || !callback) {
+ return AVERROR(EINVAL);
+ }
+
+ FBDecoder *decoder = fb_decoder_create();
+ if (fb_decoder_open(decoder, filename, stream_index) < 0) {
+ fb_decoder_free(&decoder);
+ return AVERROR_EXTERNAL;
+ }
+
+ FBPacket *pkt = fb_packet_alloc();
+
+ while (fb_decoder_get_packet(decoder, pkt) >= 0) {
+ callback(fb_packet_get_pts(pkt), fb_packet_get_duration(pkt),
+ reinterpret_cast(fb_packet_get_data(pkt)),
+ fb_packet_get_size(pkt), userdata);
+ }
+
+ fb_packet_free(&pkt);
+ fb_decoder_free(&decoder);
+
+ return 0;
+}
diff --git a/ffmpeg_bridge/src/swr.cpp b/ffmpeg_bridge/src/swr.cpp
new file mode 100644
index 000000000..9e854fd80
--- /dev/null
+++ b/ffmpeg_bridge/src/swr.cpp
@@ -0,0 +1,83 @@
+/***
+
+ 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 "internal.h"
+
+struct FBResampler {
+ SwrContext *ctx;
+};
+
+FBResampler *fb_resampler_create(uint64_t out_layout_mask, int out_format,
+ int out_rate, uint64_t in_layout_mask,
+ int in_format, int in_rate)
+{
+ AVChannelLayout out_layout, in_layout;
+ fb::ChannelLayoutFromMask(&out_layout, out_layout_mask, 0);
+ fb::ChannelLayoutFromMask(&in_layout, in_layout_mask, 0);
+
+ SwrContext *ctx = nullptr;
+ int r = swr_alloc_set_opts2(&ctx, &out_layout,
+ static_cast(out_format), out_rate,
+ &in_layout, static_cast(in_format),
+ in_rate, 0, nullptr);
+
+ av_channel_layout_uninit(&out_layout);
+ av_channel_layout_uninit(&in_layout);
+
+ if (r < 0 || !ctx) {
+ return nullptr;
+ }
+
+ if (swr_init(ctx) < 0) {
+ swr_free(&ctx);
+ return nullptr;
+ }
+
+ FBResampler *resampler = new FBResampler;
+ resampler->ctx = ctx;
+ return resampler;
+}
+
+void fb_resampler_free(FBResampler **resampler)
+{
+ if (resampler && *resampler) {
+ swr_free(&(*resampler)->ctx);
+ delete *resampler;
+ *resampler = nullptr;
+ }
+}
+
+int fb_resampler_get_out_samples(FBResampler *resampler, int in_samples)
+{
+ if (!resampler) {
+ return AVERROR(EINVAL);
+ }
+ return swr_get_out_samples(resampler->ctx, in_samples);
+}
+
+int fb_resampler_convert(FBResampler *resampler, uint8_t **out, int out_count,
+ const uint8_t **in, int in_count)
+{
+ if (!resampler) {
+ return AVERROR(EINVAL);
+ }
+ return swr_convert(resampler->ctx, out, out_count, in, in_count);
+}
diff --git a/ffmpeg_bridge/src/sws.cpp b/ffmpeg_bridge/src/sws.cpp
new file mode 100644
index 000000000..2c19362b1
--- /dev/null
+++ b/ffmpeg_bridge/src/sws.cpp
@@ -0,0 +1,94 @@
+/***
+
+ 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 "internal.h"
+
+struct FBScaler {
+ SwsContext *ctx;
+};
+
+FBScaler *fb_scaler_create(int src_width, int src_height, int src_format,
+ int dst_width, int dst_height, int dst_format,
+ int flags)
+{
+ SwsContext *ctx = sws_getContext(
+ src_width, src_height, static_cast(src_format),
+ dst_width, dst_height, static_cast(dst_format), flags,
+ nullptr, nullptr, nullptr);
+ if (!ctx) {
+ return nullptr;
+ }
+
+ FBScaler *s = new FBScaler;
+ s->ctx = ctx;
+ return s;
+}
+
+void fb_scaler_free(FBScaler **scaler)
+{
+ if (scaler && *scaler) {
+ sws_freeContext((*scaler)->ctx);
+ delete *scaler;
+ *scaler = nullptr;
+ }
+}
+
+int fb_scaler_set_colorspace(FBScaler *scaler, int colorspace, int jpeg_range)
+{
+ if (!scaler) {
+ return AVERROR(EINVAL);
+ }
+
+ const int *coeffs = sws_getCoefficients(
+ fb::SwsColorspaceFromAVColorSpace(static_cast(colorspace)));
+ return sws_setColorspaceDetails(scaler->ctx, coeffs, jpeg_range, coeffs,
+ jpeg_range, 0, 0x10000, 0x10000);
+}
+
+int fb_scaler_scale_frame(FBScaler *scaler, FBFrame *dst, const FBFrame *src)
+{
+ if (!scaler || !dst || !src) {
+ return AVERROR(EINVAL);
+ }
+ return sws_scale_frame(scaler->ctx, dst->frame, src->frame);
+}
+
+int fb_scaler_scale_slices(FBScaler *scaler, const uint8_t *const *src_data,
+ const int *src_linesize, int src_height,
+ uint8_t *const *dst_data, const int *dst_linesize)
+{
+ if (!scaler) {
+ return AVERROR(EINVAL);
+ }
+ return sws_scale(scaler->ctx, src_data, src_linesize, 0, src_height,
+ dst_data, dst_linesize);
+}
+
+void fb_get_yuv_coefficients(int colorspace, double out[4])
+{
+ const int *coeffs = sws_getCoefficients(
+ fb::SwsColorspaceFromAVColorSpace(static_cast(colorspace)));
+ // Matches the historical usage order: crv, cbu, cgu, cgv
+ out[0] = coeffs[0] / 65536.0; // crv
+ out[1] = coeffs[1] / 65536.0; // cbu
+ out[2] = coeffs[2] / 65536.0; // cgu
+ out[3] = coeffs[3] / 65536.0; // cgv
+}
diff --git a/ffmpeg_bridge/src/utils.cpp b/ffmpeg_bridge/src/utils.cpp
new file mode 100644
index 000000000..e885ddf0e
--- /dev/null
+++ b/ffmpeg_bridge/src/utils.cpp
@@ -0,0 +1,210 @@
+/***
+
+ 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 "internal.h"
+
+#include
+#include
+#include
+
+namespace fb
+{
+
+void ChannelLayoutFromMask(AVChannelLayout *layout, uint64_t mask,
+ int fallback_channels)
+{
+ if (mask != 0) {
+ av_channel_layout_from_mask(layout, mask);
+ return;
+ }
+
+ if (fallback_channels <= 0) {
+ fallback_channels = 2;
+ }
+ av_channel_layout_default(layout, fallback_channels);
+}
+
+uint64_t ValidateStreamChannelLayoutMask(const AVStream *stream)
+{
+ if (!stream || !stream->codecpar) {
+ return 0;
+ }
+
+ const AVChannelLayout &layout = stream->codecpar->ch_layout;
+ if (av_channel_layout_check(&layout) &&
+ layout.order == AV_CHANNEL_ORDER_NATIVE && layout.u.mask != 0) {
+ return layout.u.mask;
+ }
+
+ // Fall back to a default layout for the stream's channel count
+ if (layout.nb_channels <= 0) {
+ return 0;
+ }
+
+ AVChannelLayout fallback;
+ av_channel_layout_default(&fallback, layout.nb_channels);
+ uint64_t mask = 0;
+ if (fallback.order == AV_CHANNEL_ORDER_NATIVE) {
+ mask = fallback.u.mask;
+ }
+ av_channel_layout_uninit(&fallback);
+ return mask;
+}
+
+int SwsColorspaceFromAVColorSpace(AVColorSpace cs)
+{
+ switch (cs) {
+ case AVCOL_SPC_BT709:
+ return SWS_CS_ITU709;
+ case AVCOL_SPC_FCC:
+ return SWS_CS_FCC;
+ case AVCOL_SPC_BT470BG:
+ return SWS_CS_ITU624;
+ case AVCOL_SPC_SMPTE170M:
+ return SWS_CS_SMPTE170M;
+ case AVCOL_SPC_SMPTE240M:
+ return SWS_CS_SMPTE240M;
+ case AVCOL_SPC_BT2020_NCL:
+ return SWS_CS_BT2020;
+ default:
+ break;
+ }
+
+ return SWS_CS_DEFAULT;
+}
+
+void SetError(char *error_buffer, size_t error_buffer_size, const char *context,
+ int error_code)
+{
+ if (!error_buffer || error_buffer_size == 0) {
+ return;
+ }
+
+ char ffmpeg_err[512];
+ av_strerror(error_code, ffmpeg_err, sizeof(ffmpeg_err));
+ snprintf(error_buffer, error_buffer_size, "%s: %s (%d)", context,
+ ffmpeg_err, error_code);
+}
+
+} // namespace fb
+
+void fb_error_string(int error_code, char *buffer, int buffer_size)
+{
+ if (!buffer || buffer_size <= 0) {
+ return;
+ }
+
+ if (error_code == FB_ERROR_EOF) {
+ snprintf(buffer, size_t(buffer_size), "End of file");
+ return;
+ }
+
+ if (av_strerror(error_code, buffer, size_t(buffer_size)) < 0) {
+ snprintf(buffer, size_t(buffer_size), "Unknown error %d", error_code);
+ }
+}
+
+const char *fb_version_string(void)
+{
+ static char version[128];
+ snprintf(version, sizeof(version), "libavcodec %s, libavformat %s, libavutil %s, libswscale %s, libswresample %s, libavfilter %s",
+ LIBAVCODEC_IDENT, LIBAVFORMAT_IDENT, LIBAVUTIL_IDENT,
+ LIBSWSCALE_IDENT, LIBSWRESAMPLE_IDENT, LIBAVFILTER_IDENT);
+ return version;
+}
+
+const char *fb_pix_fmt_name(int pix_fmt)
+{
+ return av_get_pix_fmt_name(static_cast(pix_fmt));
+}
+
+int fb_pix_fmt_from_name(const char *name)
+{
+ return av_get_pix_fmt(name);
+}
+
+int fb_pix_fmt_bits_per_pixel(int pix_fmt)
+{
+ const AVPixFmtDescriptor *desc =
+ av_pix_fmt_desc_get(static_cast(pix_fmt));
+ if (!desc) {
+ return 0;
+ }
+ return av_get_bits_per_pixel(desc);
+}
+
+int fb_pix_fmt_has_alpha(int pix_fmt)
+{
+ const AVPixFmtDescriptor *desc =
+ av_pix_fmt_desc_get(static_cast(pix_fmt));
+ return desc && (desc->flags & AV_PIX_FMT_FLAG_ALPHA);
+}
+
+int fb_pix_fmt_is_planar(int pix_fmt)
+{
+ const AVPixFmtDescriptor *desc =
+ av_pix_fmt_desc_get(static_cast(pix_fmt));
+ return desc && (desc->flags & AV_PIX_FMT_FLAG_PLANAR);
+}
+
+int fb_pix_fmt_component_size(int pix_fmt)
+{
+ const AVPixFmtDescriptor *desc =
+ av_pix_fmt_desc_get(static_cast(pix_fmt));
+ if (!desc || desc->nb_components == 0) {
+ return 0;
+ }
+ return desc->comp[0].step;
+}
+
+int fb_find_best_pix_fmt_of_list(const int *list, int pix_fmt)
+{
+ // Count the list
+ int count = 0;
+ while (list[count] != FB_PIX_FMT_NONE) {
+ count++;
+ }
+
+ return avcodec_find_best_pix_fmt_of_list(
+ reinterpret_cast(list),
+ static_cast(pix_fmt), 1, nullptr);
+}
+
+int fb_channel_layout_get_channels(uint64_t mask)
+{
+ AVChannelLayout layout;
+ av_channel_layout_from_mask(&layout, mask);
+ int channels = layout.nb_channels;
+ av_channel_layout_uninit(&layout);
+ return channels;
+}
+
+uint64_t fb_channel_layout_default(int nb_channels)
+{
+ AVChannelLayout layout;
+ av_channel_layout_default(&layout, nb_channels);
+ uint64_t mask = 0;
+ if (layout.order == AV_CHANNEL_ORDER_NATIVE) {
+ mask = layout.u.mask;
+ }
+ av_channel_layout_uninit(&layout);
+ return mask;
+}