#include #include #include #include #include #include #ifdef _WIN32 #include #define BL_VIDEO_EXPORT __declspec(dllexport) #else #include #define BL_VIDEO_EXPORT __attribute__((visibility("default"))) #endif // ============================================================================ // Exact FFmpeg C Types (Matching FFmpeg 6.x / 7.x ABI) // ============================================================================ #define AV_PIX_FMT_YUV420P 0 #define AV_PIX_FMT_ARGB 25 #define AV_PIX_FMT_RGBA 26 #define AV_PIX_FMT_BGRA 28 #define SWS_FAST_BILINEAR 1 #define SWS_BILINEAR 2 #define SWS_BICUBIC 4 #define AVIO_FLAG_WRITE 2 #define AVFMT_NOFILE 0x0001 #define AVFMT_GLOBALHEADER 0x0040 #define AV_CODEC_FLAG_GLOBAL_HEADER (1 << 22) #define AVMEDIA_TYPE_VIDEO 0 #define AVERROR_EAGAIN -11 #define AVERROR_EOF -541478725 struct AVRational { int num; int den; }; struct AVStream { int index; int id; void* codecpar; AVRational time_base; int64_t start_time; int64_t duration; int64_t nb_frames; int64_t cur_dts; void* metadata; AVRational avg_frame_rate; AVRational r_frame_rate; AVRational sample_aspect_ratio; int nb_side_data; void* side_data; }; struct AVOutputFormat { const char* name; const char* long_name; const char* mime_type; const char* extensions; int audio_codec; int video_codec; int subtitle_codec; int flags; }; struct AVFormatContext { const void* av_class; void* iformat; AVOutputFormat* oformat; void* priv_data; void* pb; int ctx_flags; unsigned int nb_streams; AVStream** streams; }; struct AVCodecContext { const void* av_class; int log_level_offset; int codec_type; const void* codec; int codec_id; unsigned int codec_tag; void* priv_data; void* internal; void* opaque; int64_t bit_rate; int bit_rate_tolerance; int global_quality; int compression_level; int flags; int flags2; uint8_t* extradata; int extradata_size; AVRational time_base; int ticks_per_frame; int delay; int width, height; int coded_width, coded_height; AVRational sample_aspect_ratio; int pix_fmt; }; struct AVFrame { uint8_t* data[8]; int linesize[8]; uint8_t** extended_data; int width, height; int nb_samples; int format; int key_frame; int pict_type; AVRational sample_aspect_ratio; int64_t pts; int64_t pkt_dts; AVRational time_base; }; struct AVPacket { void* buf; int64_t pts; int64_t dts; uint8_t* data; int size; int stream_index; int flags; void* side_data; int side_data_elems; int64_t duration; int64_t pos; void* opaque; void* opaque_ref; AVRational time_base; }; // ============================================================================ // FFmpeg Function Pointer Declarations // ============================================================================ typedef int (*fn_avformat_alloc_output_context2)(AVFormatContext** ctx, void* oformat, const char* format_name, const char* filename); typedef AVStream* (*fn_avformat_new_stream)(AVFormatContext* s, const void* c); typedef int (*fn_avformat_write_header)(AVFormatContext* s, void** options); typedef int (*fn_av_write_trailer)(AVFormatContext* s); typedef int (*fn_avio_open)(void** s, const char* url, int flags); typedef int (*fn_avio_closep)(void** s); typedef int (*fn_av_interleaved_write_frame)(AVFormatContext* s, AVPacket* pkt); typedef void (*fn_avformat_free_context)(AVFormatContext* s); typedef void* (*fn_avcodec_find_encoder_by_name)(const char* name); typedef void* (*fn_avcodec_find_encoder)(int id); typedef void* (*fn_avcodec_alloc_context3)(const void* codec); typedef void (*fn_avcodec_free_context)(void** avctx); typedef int (*fn_avcodec_open2)(void* avctx, const void* codec, void** options); typedef int (*fn_avcodec_send_frame)(void* avctx, const AVFrame* frame); typedef int (*fn_avcodec_receive_packet)(void* avctx, AVPacket* pkt); typedef int (*fn_avcodec_parameters_from_context)(void* par, const void* codec); typedef AVFrame* (*fn_av_frame_alloc)(); typedef void (*fn_av_frame_free)(AVFrame** frame); typedef int (*fn_av_frame_get_buffer)(AVFrame* frame, int align); typedef int (*fn_av_frame_make_writable)(AVFrame* frame); typedef AVPacket* (*fn_av_packet_alloc)(); typedef void (*fn_av_packet_free)(AVPacket** pkt); typedef void (*fn_av_packet_rescale_ts)(AVPacket* pkt, AVRational tb_src, AVRational tb_dst); typedef void (*fn_av_packet_unref)(AVPacket* pkt); typedef int (*fn_av_opt_set)(void* obj, const char* name, const char* val, int search_flags); typedef int (*fn_av_opt_set_int)(void* obj, const char* name, int64_t val, int search_flags); typedef int (*fn_av_opt_set_q)(void* obj, const char* name, AVRational val, int search_flags); typedef int (*fn_av_opt_set_pixel_fmt)(void* obj, const char* name, int fmt, int search_flags); typedef int (*fn_av_dict_set)(void** pm, const char* key, const char* value, int flags); typedef void (*fn_av_dict_free)(void** pm); typedef void* (*fn_sws_getContext)(int srcW, int srcH, int srcFormat, int dstW, int dstH, int dstFormat, int flags, void* srcFilter, void* dstFilter, const double* param); typedef int (*fn_sws_scale)(void* c, const uint8_t* const srcSlice[], const int srcStride[], int srcSliceY, int srcSliceH, uint8_t* const dst[], const int dstStride[]); typedef void (*fn_sws_freeContext)(void* swsContext); struct FFmpegAPI { bool loaded = false; fn_avformat_alloc_output_context2 avformat_alloc_output_context2 = nullptr; fn_avformat_new_stream avformat_new_stream = nullptr; fn_avformat_write_header avformat_write_header = nullptr; fn_av_write_trailer av_write_trailer = nullptr; fn_avio_open avio_open = nullptr; fn_avio_closep avio_closep = nullptr; fn_av_interleaved_write_frame av_interleaved_write_frame = nullptr; fn_avformat_free_context avformat_free_context = nullptr; fn_avcodec_find_encoder_by_name avcodec_find_encoder_by_name = nullptr; fn_avcodec_find_encoder avcodec_find_encoder = nullptr; fn_avcodec_alloc_context3 avcodec_alloc_context3 = nullptr; fn_avcodec_free_context avcodec_free_context = nullptr; fn_avcodec_open2 avcodec_open2 = nullptr; fn_avcodec_send_frame avcodec_send_frame = nullptr; fn_avcodec_receive_packet avcodec_receive_packet = nullptr; fn_avcodec_parameters_from_context avcodec_parameters_from_context = nullptr; fn_av_frame_alloc av_frame_alloc = nullptr; fn_av_frame_free av_frame_free = nullptr; fn_av_frame_get_buffer av_frame_get_buffer = nullptr; fn_av_frame_make_writable av_frame_make_writable = nullptr; fn_av_packet_alloc av_packet_alloc = nullptr; fn_av_packet_free av_packet_free = nullptr; fn_av_packet_rescale_ts av_packet_rescale_ts = nullptr; fn_av_packet_unref av_packet_unref = nullptr; fn_av_opt_set av_opt_set = nullptr; fn_av_opt_set_int av_opt_set_int = nullptr; fn_av_opt_set_q av_opt_set_q = nullptr; fn_av_opt_set_pixel_fmt av_opt_set_pixel_fmt = nullptr; fn_av_dict_set av_dict_set = nullptr; fn_av_dict_free av_dict_free = nullptr; fn_sws_getContext sws_getContext = nullptr; fn_sws_scale sws_scale = nullptr; fn_sws_freeContext sws_freeContext = nullptr; }; static FFmpegAPI g_ff; #ifdef _WIN32 static void* load_sym(HMODULE mod, const char* name) { return (void*)GetProcAddress(mod, name); } #else static void* load_sym(void* mod, const char* name) { return dlsym(mod, name); } #endif extern "C" { BL_VIDEO_EXPORT int bl_video_init_ffmpeg( const char* avcodec_path, const char* avformat_path, const char* avutil_path, const char* swscale_path ) { if (g_ff.loaded) return 0; #ifdef _WIN32 HMODULE h_util = LoadLibraryA(avutil_path ? avutil_path : "avutil-59.dll"); HMODULE h_codec = LoadLibraryA(avcodec_path ? avcodec_path : "avcodec-61.dll"); HMODULE h_format = LoadLibraryA(avformat_path ? avformat_path : "avformat-61.dll"); HMODULE h_sws = LoadLibraryA(swscale_path ? swscale_path : "swscale-8.dll"); #else void* h_util = dlopen(avutil_path ? avutil_path : "libavutil.so", RTLD_NOW | RTLD_GLOBAL); void* h_codec = dlopen(avcodec_path ? avcodec_path : "libavcodec.so", RTLD_NOW | RTLD_GLOBAL); void* h_format = dlopen(avformat_path ? avformat_path : "libavformat.so", RTLD_NOW | RTLD_GLOBAL); void* h_sws = dlopen(swscale_path ? swscale_path : "libswscale.so", RTLD_NOW | RTLD_GLOBAL); #endif if (!h_util || !h_codec || !h_format || !h_sws) { return -1; } g_ff.avformat_alloc_output_context2 = (fn_avformat_alloc_output_context2)load_sym(h_format, "avformat_alloc_output_context2"); g_ff.avformat_new_stream = (fn_avformat_new_stream)load_sym(h_format, "avformat_new_stream"); g_ff.avformat_write_header = (fn_avformat_write_header)load_sym(h_format, "avformat_write_header"); g_ff.av_write_trailer = (fn_av_write_trailer)load_sym(h_format, "av_write_trailer"); g_ff.avio_open = (fn_avio_open)load_sym(h_format, "avio_open"); g_ff.avio_closep = (fn_avio_closep)load_sym(h_format, "avio_closep"); g_ff.av_interleaved_write_frame = (fn_av_interleaved_write_frame)load_sym(h_format, "av_interleaved_write_frame"); g_ff.avformat_free_context = (fn_avformat_free_context)load_sym(h_format, "avformat_free_context"); g_ff.avcodec_find_encoder_by_name = (fn_avcodec_find_encoder_by_name)load_sym(h_codec, "avcodec_find_encoder_by_name"); g_ff.avcodec_find_encoder = (fn_avcodec_find_encoder)load_sym(h_codec, "avcodec_find_encoder"); g_ff.avcodec_alloc_context3 = (fn_avcodec_alloc_context3)load_sym(h_codec, "avcodec_alloc_context3"); g_ff.avcodec_free_context = (fn_avcodec_free_context)load_sym(h_codec, "avcodec_free_context"); g_ff.avcodec_open2 = (fn_avcodec_open2)load_sym(h_codec, "avcodec_open2"); g_ff.avcodec_send_frame = (fn_avcodec_send_frame)load_sym(h_codec, "avcodec_send_frame"); g_ff.avcodec_receive_packet = (fn_avcodec_receive_packet)load_sym(h_codec, "avcodec_receive_packet"); g_ff.avcodec_parameters_from_context = (fn_avcodec_parameters_from_context)load_sym(h_codec, "avcodec_parameters_from_context"); g_ff.av_frame_alloc = (fn_av_frame_alloc)load_sym(h_util, "av_frame_alloc"); g_ff.av_frame_free = (fn_av_frame_free)load_sym(h_util, "av_frame_free"); g_ff.av_frame_get_buffer = (fn_av_frame_get_buffer)load_sym(h_util, "av_frame_get_buffer"); g_ff.av_frame_make_writable = (fn_av_frame_make_writable)load_sym(h_util, "av_frame_make_writable"); g_ff.av_packet_alloc = (fn_av_packet_alloc)load_sym(h_codec, "av_packet_alloc"); if (!g_ff.av_packet_alloc) g_ff.av_packet_alloc = (fn_av_packet_alloc)load_sym(h_util, "av_packet_alloc"); g_ff.av_packet_free = (fn_av_packet_free)load_sym(h_codec, "av_packet_free"); if (!g_ff.av_packet_free) g_ff.av_packet_free = (fn_av_packet_free)load_sym(h_util, "av_packet_free"); g_ff.av_packet_rescale_ts = (fn_av_packet_rescale_ts)load_sym(h_codec, "av_packet_rescale_ts"); if (!g_ff.av_packet_rescale_ts) g_ff.av_packet_rescale_ts = (fn_av_packet_rescale_ts)load_sym(h_util, "av_packet_rescale_ts"); g_ff.av_packet_unref = (fn_av_packet_unref)load_sym(h_codec, "av_packet_unref"); if (!g_ff.av_packet_unref) g_ff.av_packet_unref = (fn_av_packet_unref)load_sym(h_util, "av_packet_unref"); g_ff.av_opt_set = (fn_av_opt_set)load_sym(h_util, "av_opt_set"); g_ff.av_opt_set_int = (fn_av_opt_set_int)load_sym(h_util, "av_opt_set_int"); g_ff.av_opt_set_q = (fn_av_opt_set_q)load_sym(h_util, "av_opt_set_q"); g_ff.av_opt_set_pixel_fmt = (fn_av_opt_set_pixel_fmt)load_sym(h_util, "av_opt_set_pixel_fmt"); g_ff.av_dict_set = (fn_av_dict_set)load_sym(h_util, "av_dict_set"); g_ff.av_dict_free = (fn_av_dict_free)load_sym(h_util, "av_dict_free"); g_ff.sws_getContext = (fn_sws_getContext)load_sym(h_sws, "sws_getContext"); g_ff.sws_scale = (fn_sws_scale)load_sym(h_sws, "sws_scale"); g_ff.sws_freeContext = (fn_sws_freeContext)load_sym(h_sws, "sws_freeContext"); g_ff.loaded = (g_ff.avformat_alloc_output_context2 && g_ff.avcodec_send_frame && g_ff.sws_scale); return g_ff.loaded ? 0 : -2; } // ============================================================================ // Blend2D Native C++ Video Encoder // ============================================================================ struct BLVideoEncoder { AVFormatContext* format_ctx = nullptr; void* codec_ctx = nullptr; AVStream* stream = nullptr; void* sws_ctx = nullptr; AVFrame* frame = nullptr; AVPacket* pkt = nullptr; int width = 0; int height = 0; double fps = 30.0; int64_t pts = 0; int64_t frame_count = 0; bool is_open = false; char last_error[256] = {0}; }; BL_VIDEO_EXPORT BLVideoEncoder* bl_video_encoder_create( const char* filename, int width, int height, double fps, const char* codec_name, int crf, const char* preset, int bitrate ) { if (!g_ff.loaded || !filename || width <= 0 || height <= 0 || fps <= 0.0) { return nullptr; } auto* enc = new BLVideoEncoder(); enc->width = (width % 2 == 0) ? width : width - 1; enc->height = (height % 2 == 0) ? height : height - 1; enc->fps = fps; enc->pts = 0; enc->frame_count = 0; // 1. Allocate output format context int ret = g_ff.avformat_alloc_output_context2(&enc->format_ctx, nullptr, nullptr, filename); if (ret < 0 || !enc->format_ctx) { snprintf(enc->last_error, sizeof(enc->last_error), "Could not deduce format for: %s", filename); delete enc; return nullptr; } // 2. Find video encoder const void* codec = nullptr; if (codec_name && strlen(codec_name) > 0) { codec = g_ff.avcodec_find_encoder_by_name(codec_name); } if (!codec) { codec = g_ff.avcodec_find_encoder_by_name("libx264"); } if (!codec) { codec = g_ff.avcodec_find_encoder(enc->format_ctx->oformat->video_codec); } if (!codec) { snprintf(enc->last_error, sizeof(enc->last_error), "No suitable video encoder found"); g_ff.avformat_free_context(enc->format_ctx); delete enc; return nullptr; } // 3. Create video stream enc->stream = g_ff.avformat_new_stream(enc->format_ctx, nullptr); if (!enc->stream) { snprintf(enc->last_error, sizeof(enc->last_error), "Failed to allocate stream"); g_ff.avformat_free_context(enc->format_ctx); delete enc; return nullptr; } enc->stream->id = enc->format_ctx->nb_streams - 1; // 4. Allocate codec context enc->codec_ctx = g_ff.avcodec_alloc_context3(codec); if (!enc->codec_ctx) { snprintf(enc->last_error, sizeof(enc->last_error), "Failed to allocate codec context"); g_ff.avformat_free_context(enc->format_ctx); delete enc; return nullptr; } // Set properties via AVOptions g_ff.av_opt_set_int(enc->codec_ctx, "width", enc->width, 0); g_ff.av_opt_set_int(enc->codec_ctx, "height", enc->height, 0); AVRational tb = {1, (int)(fps * 1000)}; g_ff.av_opt_set_q(enc->codec_ctx, "time_base", tb, 0); enc->stream->time_base = tb; AVRational fr = {(int)fps, 1}; g_ff.av_opt_set_q(enc->codec_ctx, "framerate", fr, 0); // Set pixel format if (g_ff.av_opt_set_pixel_fmt) { g_ff.av_opt_set_pixel_fmt(enc->codec_ctx, "pix_fmt", AV_PIX_FMT_YUV420P, 0); } else { g_ff.av_opt_set(enc->codec_ctx, "pix_fmt", "yuv420p", 0); } if (bitrate > 0) { g_ff.av_opt_set_int(enc->codec_ctx, "b", bitrate, 0); } // CRF and Preset void* opts = nullptr; if (preset && strlen(preset) > 0) { g_ff.av_dict_set(&opts, "preset", preset, 0); } else { g_ff.av_dict_set(&opts, "preset", "fast", 0); } if (crf >= 0) { char crf_str[16]; snprintf(crf_str, sizeof(crf_str), "%d", crf); g_ff.av_dict_set(&opts, "crf", crf_str, 0); } // 5. Open codec ret = g_ff.avcodec_open2(enc->codec_ctx, codec, &opts); g_ff.av_dict_free(&opts); if (ret < 0) { snprintf(enc->last_error, sizeof(enc->last_error), "Failed to open video codec"); g_ff.avcodec_free_context(&enc->codec_ctx); g_ff.avformat_free_context(enc->format_ctx); delete enc; return nullptr; } if (enc->stream->codecpar) { g_ff.avcodec_parameters_from_context(enc->stream->codecpar, enc->codec_ctx); } // 6. Open file if (!(enc->format_ctx->oformat->flags & AVFMT_NOFILE)) { ret = g_ff.avio_open(&enc->format_ctx->pb, filename, AVIO_FLAG_WRITE); if (ret < 0) { snprintf(enc->last_error, sizeof(enc->last_error), "Could not open file: %s", filename); g_ff.avcodec_free_context(&enc->codec_ctx); g_ff.avformat_free_context(enc->format_ctx); delete enc; return nullptr; } } // 7. Write header ret = g_ff.avformat_write_header(enc->format_ctx, nullptr); if (ret < 0) { snprintf(enc->last_error, sizeof(enc->last_error), "Failed to write header"); if (enc->format_ctx->pb) g_ff.avio_closep(&enc->format_ctx->pb); g_ff.avcodec_free_context(&enc->codec_ctx); g_ff.avformat_free_context(enc->format_ctx); delete enc; return nullptr; } // 8. Allocate frame enc->frame = g_ff.av_frame_alloc(); enc->frame->format = AV_PIX_FMT_YUV420P; enc->frame->width = enc->width; enc->frame->height = enc->height; g_ff.av_frame_get_buffer(enc->frame, 0); // 9. Allocate SIMD SwsContext enc->sws_ctx = g_ff.sws_getContext( enc->width, enc->height, AV_PIX_FMT_BGRA, enc->width, enc->height, AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr ); enc->pkt = g_ff.av_packet_alloc(); enc->is_open = true; return enc; } BL_VIDEO_EXPORT int bl_video_encoder_write_raw( BLVideoEncoder* enc, const uint8_t* bgra_data, int stride ) { if (!enc || !enc->is_open || !bgra_data) return -1; if (g_ff.av_frame_make_writable(enc->frame) < 0) { return -2; } const uint8_t* src_slices[4] = { bgra_data, nullptr, nullptr, nullptr }; int src_strides[4] = { stride, 0, 0, 0 }; g_ff.sws_scale( enc->sws_ctx, src_slices, src_strides, 0, enc->height, enc->frame->data, enc->frame->linesize ); enc->frame->pts = enc->frame_count * 1000; enc->frame_count++; int ret = g_ff.avcodec_send_frame(enc->codec_ctx, enc->frame); if (ret < 0) return ret; while (ret >= 0) { ret = g_ff.avcodec_receive_packet(enc->codec_ctx, enc->pkt); if (ret == AVERROR_EAGAIN || ret == AVERROR_EOF) { break; } else if (ret < 0) { return ret; } g_ff.av_packet_rescale_ts(enc->pkt, enc->stream->time_base, enc->stream->time_base); enc->pkt->stream_index = enc->stream->id; ret = g_ff.av_interleaved_write_frame(enc->format_ctx, enc->pkt); g_ff.av_packet_unref(enc->pkt); if (ret < 0) return ret; } return 0; } BL_VIDEO_EXPORT int bl_video_encoder_close(BLVideoEncoder* enc) { if (!enc || !enc->is_open) return 0; g_ff.avcodec_send_frame(enc->codec_ctx, nullptr); while (g_ff.avcodec_receive_packet(enc->codec_ctx, enc->pkt) == 0) { g_ff.av_packet_rescale_ts(enc->pkt, enc->stream->time_base, enc->stream->time_base); enc->pkt->stream_index = enc->stream->id; g_ff.av_interleaved_write_frame(enc->format_ctx, enc->pkt); g_ff.av_packet_unref(enc->pkt); } if (enc->format_ctx) { g_ff.av_write_trailer(enc->format_ctx); } if (enc->codec_ctx) { g_ff.avcodec_free_context(&enc->codec_ctx); } if (enc->frame) { g_ff.av_frame_free(&enc->frame); } if (enc->pkt) { g_ff.av_packet_free(&enc->pkt); } if (enc->sws_ctx) { g_ff.sws_freeContext(enc->sws_ctx); enc->sws_ctx = nullptr; } if (enc->format_ctx) { if (!(enc->format_ctx->oformat->flags & AVFMT_NOFILE) && enc->format_ctx->pb) { g_ff.avio_closep(&enc->format_ctx->pb); } g_ff.avformat_free_context(enc->format_ctx); enc->format_ctx = nullptr; } enc->is_open = false; return 0; } BL_VIDEO_EXPORT void bl_video_encoder_destroy(BLVideoEncoder* enc) { if (enc) { if (enc->is_open) { bl_video_encoder_close(enc); } delete enc; } } BL_VIDEO_EXPORT int64_t bl_video_encoder_get_frame_count(const BLVideoEncoder* enc) { return enc ? enc->frame_count : 0; } BL_VIDEO_EXPORT const char* bl_video_encoder_get_last_error(const BLVideoEncoder* enc) { return enc ? enc->last_error : "Null encoder"; } } // extern "C"