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; +}